Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f3a548d
Add QueueConfig to config.py (Task 1)
coordt Apr 22, 2026
73cf3cb
Implement TaskQueue and tests (Tasks 2 & 3)
coordt Apr 22, 2026
a3633be
Add threading lock to TaskQueue for improved concurrency safety
coordt Apr 25, 2026
99b02c8
Update messaging protocol design spec to propose queue-mediated agent…
coordt Apr 25, 2026
89316f3
Implement Phase 2: queue HTTP endpoints and harness result nudge
coordt Apr 25, 2026
adffcef
Implement Phase 3: foreman-client package with ForemanClient
coordt Apr 26, 2026
354e594
Merge pull request #16 from callowayproject/Corey-Oordt/message-updat…
coordt Apr 26, 2026
38c72c0
Address Phase 3 code review: fix resource leak, export types, clean u…
coordt Apr 26, 2026
e59100e
Add initial `.superset/config.json` and `.memsearch/memory/` tooling …
coordt Apr 26, 2026
6b84d42
Merge pull request #17 from callowayproject/Corey-Oordt/message-updat…
coordt Apr 26, 2026
16cc083
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Apr 27, 2026
1e2283e
Implement Phase 4 Task 10: refactor Dispatcher to enqueue + nudge
coordt May 1, 2026
3ba1ceb
Implement Phase 4 Task 11: drain and requeue background loops in life…
coordt May 1, 2026
170d707
Implement Phase 4 Task 12: add --queue-db CLI arg and wire TaskQueue
coordt May 2, 2026
a0ba8dd
Convert TaskQueue tests to use context manager and update installed p…
coordt May 2, 2026
02ae42f
Fix ResourceWarning: close TaskQueue and sqlite3 connections properly
coordt May 2, 2026
4ce2bec
Implement Phase 5: update issue-triage agent to use ForemanClient
coordt May 3, 2026
1156699
Mark Phase 5 tasks complete in plan
coordt May 3, 2026
789dbc2
Add write-an-agent how-to guide (Task 16, Phase 6)
coordt May 4, 2026
1549117
Add integration test for agent restart resilience (Task 17, Phase 6)
coordt May 4, 2026
525e797
Mark Phase 6 Task 17 complete in plan
coordt May 4, 2026
f10729f
Mark verification steps complete for Phase 6 tasks in plan
coordt May 4, 2026
c6132bb
Merge remote-tracking branch 'origin/main' into noble-cupcake
coordt May 4, 2026
df11d5c
Merge pull request #18 from callowayproject/pre-commit-ci-update-config
coordt May 4, 2026
23a836e
Remove obsolete "How Tos" index and fix installation link in write-an…
coordt May 4, 2026
05d81cc
Add configurable timeout parameter to ForemanClient
coordt May 5, 2026
596e38d
Add task_id identity callout to complete_task docs
coordt May 5, 2026
4cf9097
Split drain_completed/add mark_done; wrap _drain_loop in exception ha…
coordt May 5, 2026
0712ad8
Wrap _requeue_loop body in exception handler
coordt May 5, 2026
1a9bf71
Drain all queued tasks on agent startup (loop until empty)
coordt May 5, 2026
74cf0e0
Publicize Dispatcher.executor (remove private-attribute cross-module …
coordt May 5, 2026
9499c19
Add heartbeat thread to _process_task in reference agent
coordt May 5, 2026
a7f2905
Update minimal example and Startup Poll docs to use drain loop lifespan
coordt May 5, 2026
0a36267
Mark all pr-21-fixes.md acceptance criteria complete
coordt May 5, 2026
f01758a
Resolve high-priority issues from phase-3 review:
coordt May 5, 2026
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
22 changes: 22 additions & 0 deletions .claude/skills/buildit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: buildit
description: Implement the next task incrementally — build, test, verify, commit
---

Invoke the agent-skills:incremental-implementation skill alongside agent-skills:test-driven-development.

If uncertain, ask the user where the plan to follow is.

Pick the next pending task from the plan.
For each task:

1. Read the task's acceptance criteria
2. Load relevant context (existing code, patterns, types)
3. Write a failing test for the expected behavior (RED)
4. Implement the minimum code to pass the test (GREEN)
5. Run the full test suite to check for regressions
6. Run the build to verify compilation
7. Commit with a descriptive message
8. Mark the task complete and move to the next one

If any step fails, follow the agent-skills:debugging-and-error-recovery skill.
61 changes: 61 additions & 0 deletions .memsearch/memory/2026-04-26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@

## Session 07:30

## Session 07:34

### 07:34
<!-- session:06abd52a-1fc5-4466-957c-79f98aa06c5c turn:431f0c00-34fe-4d80-a6b3-b7a13a1fa7ca transcript:/Users/coordt/.claude/projects/-Users-coordt--superset-worktrees-foreman-Corey-Oordt-message-update-phase-3/06abd52a-1fc5-4466-957c-79f98aa06c5c.jsonl -->

- User asked Claude Code to address the issues outlined in the Phase 3 code review (phase-3-review.md).
- Claude Code explored the foreman-client package files, drafted an implementation plan,
and received user approval via ExitPlanMode.
- Claude Code edited `client.py` to add `close()`, `__enter__`, and `__exit__` methods to `ForemanClient`,
and updated the docstring example to demonstrate context manager usage.
- Claude Code edited `__init__.py` to import and export `LLMBackendRef` and `TaskContext` in the `__all__` list.
- Claude Code rewrote `test_client.py`: moved `import json` to module level,
removed the misleading comment about call ordering,
and added a new `TestForemanClientLifecycle` class with 3 tests for lifecycle behavior.
- Claude Code ran pytest in the foreman-client directory; all 19 tests passed (16 existing + 3 new).
- Claude Code ran pre-commit across the project; all linting and formatting checks passed.

## Session 07:37

### 07:37
<!-- session:06abd52a-1fc5-4466-957c-79f98aa06c5c turn:fdd73467-06f3-43ff-a433-5a6630c42c38 transcript:/Users/coordt/.claude/projects/-Users-coordt--superset-worktrees-foreman-Corey-Oordt-message-update-phase-3/06abd52a-1fc5-4466-957c-79f98aa06c5c.jsonl -->

- User asked Claude Code to update progress in the Phase 3 plan document and mark the human review checkpoint
as complete.
- Claude Code made 4 Edit tool calls to `docs/specs/02-messaging-update/plan.md`,
using `replace_all=False` to target specific sections.
- Task 8 (Implement `ForemanClient`) acceptance criteria were marked complete
(6 checkboxes changed from `[ ]` to `[x]`).
- Task 9 (Tests for `foremanclient`) acceptance criteria were marked complete
(6 checkboxes changed from `[ ]` to `[x]`).
- Phase 3 checkpoint was marked complete, including the human review step (4 checkboxes changed from `[ ]` to `[x]`).
- All edits succeeded; Claude Code reported Phase 3 fully checked off and ready for Phase 4 implementation.

## Session 07:44

## Session 07:48

### 07:48
<!-- session:e3823ffd-cc0d-4530-9e1e-0cb5b914281d turn:21a8201c-c02b-4822-a6e0-ba1cef955f74 transcript:/Users/coordt/.claude/projects/-Users-coordt--superset-worktrees-foreman-Corey-Oordt-message-update-phase-3/e3823ffd-cc0d-4530-9e1e-0cb5b914281d.jsonl -->

- User asked Claude Code to commit the current modified and staged files
- Claude Code ran `git status` and `git diff` to examine the state; found 8 staged files including spec docs,
foreman-client package code, and tests
- Claude Code read `.gitignore` and determined that `.claude/`, `.memsearch/`,
and `.superset/` directories were not listed and should not be committed
- Claude Code unstaged the three tooling artifact directories/files using `git restore --staged`
- Claude Code ran `git log --oneline -5` to review project commit message style from recent commits
- Claude Code attempted `git commit` but the pre-commit hook (interrogate) failed
and auto-fixed line breaks in `docs/specs/02-messaging-update/phase-3-review.md`
- Claude Code re-staged the corrected `phase-3-review.md` file and committed successfully
as commit `38c72c0` with message "Address Phase 3 code review: fix resource leak, export types, clean up tests"
- Claude Code offered to add the unstaged tooling artifact directories to `.gitignore`

### 07:48
<!-- session:e3823ffd-cc0d-4530-9e1e-0cb5b914281d turn:e1a9e77b-f07c-4dc6-b098-6b750b75b8fa transcript:/Users/coordt/.claude/projects/-Users-coordt--superset-worktrees-foreman-Corey-Oordt-message-update-phase-3/e3823ffd-cc0d-4530-9e1e-0cb5b914281d.jsonl -->

- User responded negatively with "no" to a preceding question or proposal from Claude Code.
- Claude Code acknowledged the response with "Got it."
7 changes: 4 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: 'v0.15.11'
rev: 'v0.15.12'
hooks:
- id: ruff-format
- id: ruff-check
Expand Down Expand Up @@ -37,11 +37,12 @@ repos:
- id: detect-secrets
additional_dependencies: ["gibberish-detector"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.20.1
rev: v1.20.2
hooks:
- id: mypy
args: [--no-strict-optional, --ignore-missing-imports]
additional_dependencies: ["toml", "types-PyYAML"]
exclude: ^foreman-client/
- repo: https://github.com/jsh9/pydoclint
rev: 0.8.3
hooks:
Expand All @@ -55,7 +56,7 @@ repos:
- id: interrogate
exclude: test.*
- repo: https://github.com/rvben/rumdl-pre-commit
rev: v0.1.78 # Use latest version
rev: v0.1.83 # Use latest version
hooks:
- id: rumdl # Lint + auto-fix, fails if unfixable issues remain
- id: rumdl-fmt # Pure format, always exits 0
Expand Down
7 changes: 7 additions & 0 deletions .superset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"setup": [
"uv sync"
],
"teardown": [],
"run": []
}
147 changes: 92 additions & 55 deletions agents/issue-triage/issue_triage/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,86 +2,120 @@

from __future__ import annotations

import uuid
from typing import Any, Optional
import asyncio
import os
import threading
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, AsyncIterator

from fastapi import FastAPI
from pydantic import BaseModel, Field
import structlog
from fastapi import BackgroundTasks, FastAPI
from foremanclient import ForemanClient
from pydantic import BaseModel

app = FastAPI(title="foreman-issue-triage", version="0.1.0")
if TYPE_CHECKING:
from foremanclient.models import DecisionMessage, TaskMessage

logger = structlog.get_logger(__name__)

# ---------------------------------------------------------------------------
# Protocol models (self-contained; mirrors foreman.protocol)
# ---------------------------------------------------------------------------
_HEARTBEAT_INTERVAL: float = 25.0


class LLMBackendRef(BaseModel):
"""Reference to the LLM backend the agent should use."""
def _get_client(application: FastAPI) -> ForemanClient:
"""Return the ForemanClient for *application*, creating it from env vars if needed.

provider: str
model: str
Args:
application: The FastAPI application whose state holds the client.

Returns:
The :class:`~foremanclient.ForemanClient` instance for this agent.
"""
if not hasattr(application.state, "client"):
application.state.client = ForemanClient(
harness_url=os.environ["FOREMAN_HARNESS_URL"],
agent_url=os.environ["AGENT_URL"],
)
return application.state.client

class TaskContext(BaseModel):
"""Context injected by the harness into each task."""

llm_backend: LLMBackendRef
memory_summary: Optional[str] = None
def triage(task: TaskMessage) -> DecisionMessage:
"""Run triage logic on *task* and return a decision.

Args:
task: The incoming triage task from the harness.

class TaskMessage(BaseModel):
"""Task message received from the harness."""
Returns:
A :class:`~foremanclient.models.DecisionMessage` with decision, rationale, and actions.
"""
from prompts.triage import run_triage

task_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
type: str
repo: str
payload: dict[str, Any]
context: TaskContext
return run_triage(task)


class ActionItem(BaseModel):
"""A single action the harness should execute."""
async def _process_task(client: ForemanClient, task: TaskMessage) -> None:
"""Call triage on *task* and report the completed decision to the harness.

model_config = {"extra": "allow"}
A daemon heartbeat thread fires every :data:`_HEARTBEAT_INTERVAL` seconds
while triage is running so the harness does not re-queue the task mid-flight.

type: str
Args:
client: The :class:`~foremanclient.ForemanClient` to use for completing the task.
task: The :class:`~foremanclient.models.TaskMessage` to process.
"""
stop_event = threading.Event()

def _heartbeat_loop() -> None:
while not stop_event.wait(timeout=_HEARTBEAT_INTERVAL):
client.heartbeat(task.task_id)

class DecisionMessage(BaseModel):
"""Decision returned to the harness."""
heartbeat_thread = threading.Thread(target=_heartbeat_loop, daemon=True)
heartbeat_thread.start()
try:
decision = await asyncio.to_thread(triage, task)
await asyncio.to_thread(client.complete_task, task.task_id, decision)
finally:
stop_event.set()

task_id: str
decision: str
rationale: str
actions: list[ActionItem] = []

async def _poll_and_process(client: ForemanClient) -> None:
"""Claim the next pending task from the harness and process it if one exists.

# ---------------------------------------------------------------------------
# Triage logic (implemented in Task 15; placeholder here)
# ---------------------------------------------------------------------------
Args:
client: The :class:`~foremanclient.ForemanClient` used to claim tasks.
"""
task = await asyncio.to_thread(client.next_task)
if task is not None:
await _process_task(client, task)


def triage(task: TaskMessage) -> DecisionMessage:
"""Run triage logic on *task* and return a decision.
@asynccontextmanager
async def _lifespan(application: FastAPI) -> AsyncIterator[None]:
"""FastAPI lifespan: drain all tasks queued while the agent was down.

This placeholder is replaced by the full implementation in
``prompts/triage.py`` (Task 15).
Loops calling next_task() until the queue is empty so that accumulated
pending tasks are not left stuck after an unclean restart.

Args:
task: The incoming triage task from the harness.

Returns:
A :class:`DecisionMessage` with decision, rationale, and actions.
application: The FastAPI application instance.
"""
from prompts.triage import run_triage
client = _get_client(application)
while True:
task = await asyncio.to_thread(client.next_task)
if task is None:
break
await _process_task(client, task)
yield
client.close()

return run_triage(task)

app = FastAPI(title="foreman-issue-triage", version="0.1.0", lifespan=_lifespan)

# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------

class TaskNudge(BaseModel):
"""Nudge payload sent by the harness when a new task is enqueued."""

task_id: str
"""Identifier of the newly enqueued task."""


@app.get("/health")
Expand All @@ -94,14 +128,17 @@ async def health() -> dict[str, str]:
return {"status": "ok"}


@app.post("/task", response_model=DecisionMessage)
async def handle_task(task: TaskMessage) -> DecisionMessage:
"""Receive a triage task, run triage logic, and return a decision.
@app.post("/task", status_code=202)
async def handle_task(nudge: TaskNudge, background_tasks: BackgroundTasks) -> dict[str, str]:
"""Accept a task nudge and process the task in the background.

Args:
task: The incoming :class:`TaskMessage` from the harness.
nudge: The nudge payload containing the task_id from the harness.
background_tasks: FastAPI background task queue.

Returns:
A :class:`DecisionMessage` with the triage decision and actions.
JSON body with ``{"status": "accepted"}``.
"""
return triage(task)
client = _get_client(app)
background_tasks.add_task(_poll_and_process, client)
return {"status": "accepted"}
6 changes: 3 additions & 3 deletions agents/issue-triage/issue_triage/prompts/triage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import litellm

if TYPE_CHECKING:
from agent import DecisionMessage, TaskMessage
from foremanclient.models import DecisionMessage, TaskMessage

_VALID_DECISIONS = {"label_and_respond", "close", "escalate", "skip"}

Expand Down Expand Up @@ -78,7 +78,7 @@ def parse_llm_response(
Returns:
A validated :class:`~agent.DecisionMessage`.
"""
from agent import ActionItem, DecisionMessage
from foremanclient.models import ActionItem, DecisionMessage

def _skip(rationale: str = "Could not parse LLM response") -> DecisionMessage:
return DecisionMessage(task_id=task_id, decision="skip", rationale=rationale, actions=[])
Expand Down Expand Up @@ -160,7 +160,7 @@ def run_triage(task: TaskMessage) -> DecisionMessage:
A :class:`~agent.DecisionMessage` with decision, rationale, and actions.
"""
if _recent_comment_in_memory(task.context.memory_summary):
from agent import DecisionMessage
from foremanclient.models import DecisionMessage

return DecisionMessage(
task_id=task.task_id,
Expand Down
2 changes: 2 additions & 0 deletions agents/issue-triage/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ runtime = [
"pydantic>=2.0",
"litellm>=1.0.0",
"httpx>=0.28.0",
"foreman-client>=0.1.0",
"structlog>=25.5.0",
]
7 changes: 7 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ llm:
polling:
interval_seconds: 60 # how often to poll GitHub (seconds)

# queue:
# db_path: null # defaults to ~/.agent-harness/queue.db
# claim_timeout_seconds: 300 # seconds before a stale claimed task is re-enqueued
# max_retries: 3 # max re-enqueue attempts before marking a task failed
# drain_interval_seconds: 10 # how often (seconds) the harness drains completed tasks
# requeue_interval_seconds: 60 # how often (seconds) the harness checks for stale tasks

repos:
- owner: callowayproject
name: bump-my-version
Expand Down
Loading
Loading