Skip to content

Commit 170d707

Browse files
coordtclaude
andcommitted
Implement Phase 4 Task 12: add --queue-db CLI arg and wire TaskQueue
Add --queue-db argument to the start subcommand so users can override the queue database path without changing config. Priority: --queue-db > config db_path > ~/.agent-harness/queue.db default. Update plan.md to mark Tasks 11, 12, 13 and Phase 4 checkpoint complete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3ba1ceb commit 170d707

3 files changed

Lines changed: 109 additions & 34 deletions

File tree

docs/specs/02-messaging-update/plan.md

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -362,17 +362,17 @@ The `Dispatcher` constructor gains a `task_queue: TaskQueue` parameter.
362362

363363
**Acceptance criteria:**
364364

365-
- [ ] `dispatch()` calls `task_queue.enqueue()` with correct `TaskMessage` and `agent_url`
366-
- [ ] `dispatch()` sends `POST <agent_url>/task` with body `{"task_id": ...}` and returns 202
367-
- [ ] `dispatch()` does not await agent response or parse `DecisionMessage`
368-
- [ ] Nudge HTTP errors are logged and swallowed (fire-and-forget)
369-
- [ ] All synchronous response-parsing code is deleted
370-
- [ ] `Dispatcher.__init__` accepts `task_queue: TaskQueue`
365+
- [x] `dispatch()` calls `task_queue.enqueue()` with correct `TaskMessage` and `agent_url`
366+
- [x] `dispatch()` sends `POST <agent_url>/task` with body `{"task_id": ...}` and returns 202
367+
- [x] `dispatch()` does not await agent response or parse `DecisionMessage`
368+
- [x] Nudge HTTP errors are logged and swallowed (fire-and-forget)
369+
- [x] All synchronous response-parsing code is deleted
370+
- [x] `Dispatcher.__init__` accepts `task_queue: TaskQueue`
371371

372372
**Verification:**
373373

374-
- [ ] `uv run pytest --agent-digest=term tests/test_server.py`
375-
- [ ] `pre-commit run --all-files`
374+
- [x] `uv run pytest --agent-digest=term tests/test_server.py`
375+
- [x] `pre-commit run --all-files`
376376

377377
**Dependencies:** Tasks 2, 6
378378

@@ -399,17 +399,17 @@ Both tasks are cancelled cleanly on shutdown.
399399

400400
**Acceptance criteria:**
401401

402-
- [ ] `drain_loop` calls `drain_completed()` and passes each `(TaskMessage, DecisionMessage)` to
402+
- [x] `drain_loop` calls `drain_completed()` and passes each `(TaskMessage, DecisionMessage)` to
403403
`executor.execute()` and `memory.upsert_memory_summary()`
404-
- [ ] `drain_loop` wakes immediately when `drain_event` is set
405-
- [ ] `requeue_loop` calls `requeue_stale()` and `fail_exhausted(max_retries=config.queue.max_retries)`
406-
- [ ] Both tasks log structured events on each cycle
407-
- [ ] Both tasks are cancelled without error on SIGINT/shutdown
404+
- [x] `drain_loop` wakes immediately when `drain_event` is set
405+
- [x] `requeue_loop` calls `requeue_stale()` and `fail_exhausted(max_retries=config.queue.max_retries)`
406+
- [x] Both tasks log structured events on each cycle
407+
- [x] Both tasks are cancelled without error on SIGINT/shutdown
408408

409409
**Verification:**
410410

411-
- [ ] `uv run pytest --agent-digest=term tests/test_server.py`
412-
- [ ] `pre-commit run --all-files`
411+
- [x] `uv run pytest --agent-digest=term tests/test_server.py`
412+
- [x] `pre-commit run --all-files`
413413

414414
**Dependencies:** Task 10
415415

@@ -429,16 +429,16 @@ Add `--queue-db` CLI argument (overrides `config.queue.db_path`).
429429

430430
**Acceptance criteria:**
431431

432-
- [ ] `TaskQueue` is constructed with the resolved `db_path` and `claim_timeout_seconds`
433-
- [ ] `Dispatcher` receives the `task_queue` instance
434-
- [ ] `app.state.task_queue` and `app.state.drain_event` are set before the server starts
435-
- [ ] Default `db_path` is `~/.agent-harness/queue.db` when not set in config
436-
- [ ] Existing `--db` arg for `memory.db` is unchanged
432+
- [x] `TaskQueue` is constructed with the resolved `db_path` and `claim_timeout_seconds`
433+
- [x] `Dispatcher` receives the `task_queue` instance
434+
- [x] `app.state.task_queue` and `app.state.drain_event` are set before the server starts
435+
- [x] Default `db_path` is `~/.agent-harness/queue.db` when not set in config
436+
- [x] Existing `--db` arg for `memory.db` is unchanged
437437

438438
**Verification:**
439439

440-
- [ ] `uv run pytest --agent-digest=term tests/test_main.py`
441-
- [ ] `pre-commit run --all-files`
440+
- [x] `uv run pytest --agent-digest=term tests/test_main.py`
441+
- [x] `pre-commit run --all-files`
442442

443443
**Dependencies:** Tasks 10, 11
444444

@@ -457,17 +457,17 @@ Test the drain loop by injecting a mocked `drain_completed()` return and verifyi
457457

458458
**Acceptance criteria:**
459459

460-
- [ ] `dispatch()` test: `enqueue()` called with correct task + agent_url; nudge POST is fire-and-forget
461-
- [ ] `dispatch()` test: nudge HTTP error is swallowed and logged; no exception propagated
462-
- [ ] Drain loop test: `drain_completed()` returning one task → `executor.execute()` called once
463-
- [ ] Drain loop test: `drain_event` set → drain loop wakes immediately
464-
- [ ] Requeue loop test: `requeue_stale()` and `fail_exhausted()` called on schedule
465-
- [ ] No test directly touches `queue.db`
460+
- [x] `dispatch()` test: `enqueue()` called with correct task + agent_url; nudge POST is fire-and-forget
461+
- [x] `dispatch()` test: nudge HTTP error is swallowed and logged; no exception propagated
462+
- [x] Drain loop test: `drain_completed()` returning one task → `executor.execute()` called once
463+
- [x] Drain loop test: `drain_event` set → drain loop wakes immediately
464+
- [x] Requeue loop test: `requeue_stale()` and `fail_exhausted()` called on schedule
465+
- [x] No test directly touches `queue.db`
466466

467467
**Verification:**
468468

469-
- [ ] `uv run pytest --agent-digest=term tests/test_server.py`
470-
- [ ] `pre-commit run --all-files`
469+
- [x] `uv run pytest --agent-digest=term tests/test_server.py`
470+
- [x] `pre-commit run --all-files`
471471

472472
**Dependencies:** Tasks 10, 11, 12
473473

@@ -479,9 +479,9 @@ Test the drain loop by injecting a mocked `drain_completed()` return and verifyi
479479

480480
### Checkpoint: Phase 4
481481

482-
- [ ] `uv run pytest --agent-digest=term` — all tests pass
483-
- [ ] Synchronous dispatch path is fully deleted from `server.py`
484-
- [ ] `pre-commit run --all-files` — clean
482+
- [x] `uv run pytest --agent-digest=term` — all tests pass
483+
- [x] Synchronous dispatch path is fully deleted from `server.py`
484+
- [x] `pre-commit run --all-files` — clean
485485
- [ ] Human review before proceeding
486486

487487
### Phase 5: Agent Update

foreman/__main__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ def _build_parser() -> argparse.ArgumentParser:
6060
metavar="DB_PATH",
6161
help="Path to the SQLite memory database (default: ~/.agent-harness/memory.db)",
6262
)
63+
start.add_argument(
64+
"--queue-db",
65+
default=None,
66+
metavar="QUEUE_DB_PATH",
67+
help="Path to the SQLite task queue database (default: ~/.agent-harness/queue.db)",
68+
)
6369
start.add_argument(
6470
"--host",
6571
default="0.0.0.0",
@@ -147,7 +153,12 @@ def _run_start(args: Any) -> None:
147153
memory = MemoryStore(db_path)
148154

149155
# 3. Create core components.
150-
queue_db_path = config.queue.db_path if config.queue.db_path is not None else _DEFAULT_QUEUE_DB_PATH
156+
if args.queue_db is not None:
157+
queue_db_path = Path(args.queue_db)
158+
elif config.queue.db_path is not None:
159+
queue_db_path = config.queue.db_path
160+
else:
161+
queue_db_path = _DEFAULT_QUEUE_DB_PATH
151162
task_queue = TaskQueue(queue_db_path, claim_timeout_seconds=config.queue.claim_timeout_seconds)
152163
dispatcher = Dispatcher(config=config, memory=memory, task_queue=task_queue)
153164

tests/test_main.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,70 @@ def test_start_creates_dispatcher(self, tmp_path: Path, mocker) -> None:
143143

144144
mock_dispatcher_cls.assert_called_once()
145145

146+
def test_start_creates_task_queue(self, tmp_path: Path, mocker) -> None:
147+
"""main() instantiates a TaskQueue and passes it to Dispatcher."""
148+
config_path = tmp_path / "config.yaml"
149+
_write_minimal_config(config_path)
150+
151+
mocker.patch("foreman.__main__.MemoryStore")
152+
mocker.patch("foreman.__main__.GitHubPoller")
153+
mocker.patch("foreman.__main__.Dispatcher")
154+
mock_queue_cls = mocker.patch("foreman.__main__.TaskQueue")
155+
mocker.patch("foreman.__main__.asyncio.run", side_effect=lambda c: c.close())
156+
157+
main(["start", "--config", str(config_path)])
158+
159+
mock_queue_cls.assert_called_once()
160+
161+
162+
# ---------------------------------------------------------------------------
163+
# --queue-db CLI argument
164+
# ---------------------------------------------------------------------------
165+
166+
167+
class TestQueueDbArg:
168+
"""--queue-db overrides config.queue.db_path for TaskQueue construction."""
169+
170+
def test_queue_db_arg_overrides_config_path(self, tmp_path: Path, mocker) -> None:
171+
"""--queue-db path is passed to TaskQueue when provided."""
172+
config_path = tmp_path / "config.yaml"
173+
_write_minimal_config(config_path)
174+
custom_db = tmp_path / "custom_queue.db"
175+
176+
mocker.patch("foreman.__main__.MemoryStore")
177+
mocker.patch("foreman.__main__.GitHubPoller")
178+
mocker.patch("foreman.__main__.Dispatcher")
179+
mock_queue_cls = mocker.patch("foreman.__main__.TaskQueue")
180+
mocker.patch("foreman.__main__.asyncio.run", side_effect=lambda c: c.close())
181+
182+
main(["start", "--config", str(config_path), "--queue-db", str(custom_db)])
183+
184+
call_args = mock_queue_cls.call_args
185+
assert (
186+
call_args[0][0] == custom_db
187+
or call_args.args[0] == custom_db
188+
or call_args.kwargs.get("db_path") == custom_db
189+
)
190+
191+
def test_queue_db_defaults_to_default_path_when_config_has_none(self, tmp_path: Path, mocker) -> None:
192+
"""TaskQueue uses _DEFAULT_QUEUE_DB_PATH when --queue-db is absent and config has no db_path."""
193+
from foreman.__main__ import _DEFAULT_QUEUE_DB_PATH
194+
195+
config_path = tmp_path / "config.yaml"
196+
_write_minimal_config(config_path)
197+
198+
mocker.patch("foreman.__main__.MemoryStore")
199+
mocker.patch("foreman.__main__.GitHubPoller")
200+
mocker.patch("foreman.__main__.Dispatcher")
201+
mock_queue_cls = mocker.patch("foreman.__main__.TaskQueue")
202+
mocker.patch("foreman.__main__.asyncio.run", side_effect=lambda c: c.close())
203+
204+
main(["start", "--config", str(config_path)])
205+
206+
call_args = mock_queue_cls.call_args
207+
actual_path = call_args[0][0] if call_args[0] else call_args.kwargs.get("db_path")
208+
assert actual_path == _DEFAULT_QUEUE_DB_PATH
209+
146210

147211
# ---------------------------------------------------------------------------
148212
# Helpers for container-related tests

0 commit comments

Comments
 (0)