From 6430a160500ba88b5aee9f406eadf73320126d46 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 2 Jun 2026 06:44:03 +0100 Subject: [PATCH] docs: add task for agent prompt generation --- PROJECT_STATE.md | 2 +- RUN_LOG.md | 2 + TASKS.md | 15 +++++ src/rdb_discovery/cli.py | 10 ++- src/rdb_discovery/tasks.py | 97 +++++++++++++++++++++++++++ tests/test_prompt.py | 131 +++++++++++++++++++++++++++++++++++++ 6 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 tests/test_prompt.py diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 3ef256d..19eafab 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -7,4 +7,4 @@ Next Stage: REVIEW_READY Current Task: TASK-002 Active Branch: main -Last Updated: 2026-06-01 +Last Updated: 2026-06-02 diff --git a/RUN_LOG.md b/RUN_LOG.md index 234d8a4..2b33095 100644 --- a/RUN_LOG.md +++ b/RUN_LOG.md @@ -5,3 +5,5 @@ Bootstrap project structure created. Fixed TASKS.md parser to match em/en dashes in task headings. TASK-000 marked Done — bootstrap standards complete. TASK-001 marked Done — rdb init creates all project control files. +2026-06-02 +Added TASK-002 for agent prompt generation. diff --git a/TASKS.md b/TASKS.md index 7ff8999..2cf2b5d 100644 --- a/TASKS.md +++ b/TASKS.md @@ -21,3 +21,18 @@ Acceptance Criteria: Status: Done Goal: Create the command that initialises the standard RDB project structure. + +## TASK-002 — Generate agent prompts + +Status: Todo + +Goal: Add a command that generates a ready-to-paste implementation prompt for the next Todo task. + +Acceptance Criteria: +- `rdb prompt` reads the next Todo task from TASKS.md +- The prompt tells the agent to read README.md, TASKS.md, PROJECT_STATE.md, AGENT_HANDOFF.md, and context/agent-guidelines.md +- The prompt says to implement one task only +- The prompt includes the task title, status, goal, and acceptance criteria +- The prompt includes validation instructions +- The prompt includes reporting instructions +- Add or update tests diff --git a/src/rdb_discovery/cli.py b/src/rdb_discovery/cli.py index b53f05c..cd0eb47 100644 --- a/src/rdb_discovery/cli.py +++ b/src/rdb_discovery/cli.py @@ -10,7 +10,7 @@ from rich.table import Table from .discovery import append_discovery_answer, core_questions from .handoff import build_handoff from .status import project_stage, task_counts -from .tasks import get_next_task, update_task_status +from .tasks import generate_agent_prompt, get_next_task, update_task_status from .templates import CONTEXT_FILES, write_file_if_missing app = typer.Typer(help="RDB discovery and delivery workflow CLI.") @@ -131,3 +131,11 @@ def complete(task_id: str) -> None: def handoff() -> None: """Print an AI-agent handoff summary.""" console.print(build_handoff(root_path())) + + +@app.command() +def prompt() -> None: + """Generate a ready-to-paste implementation prompt for the next Todo task.""" + root = root_path() + result = generate_agent_prompt(root) + console.print(result) diff --git a/src/rdb_discovery/tasks.py b/src/rdb_discovery/tasks.py index ac615fb..1d29d0c 100644 --- a/src/rdb_discovery/tasks.py +++ b/src/rdb_discovery/tasks.py @@ -8,6 +8,10 @@ TASK_HEADING_RE = re.compile(r"^##\s+(TASK-\d+)\s+[-–—]\s+(.+)$", re.MULTILI STATUS_RE = re.compile(r"^Status:\s*(.+)$", re.MULTILINE) +GOAL_RE = re.compile(r"^Goal:\s*(.+)", re.MULTILINE) +AC_RE = re.compile(r"^- (.+)$", re.MULTILINE) + + @dataclass class Task: task_id: str @@ -15,6 +19,15 @@ class Task: status: str body: str + def goal(self, root: Path) -> str: + match = GOAL_RE.search(self.body) + if match: + return match.group(1).strip() + return "" + + def ac_lines(self) -> list[str]: + return AC_RE.findall(self.body) + def read_tasks(root: Path) -> list[Task]: tasks_path = root / "TASKS.md" @@ -69,3 +82,87 @@ def update_task_status(root: Path, task_id: str, new_status: str) -> bool: return True return False + + +CONTEXT_FILES_TO_READ = [ + "README.md", + "TASKS.md", + "PROJECT_STATE.md", + "AGENT_HANDOFF.md", + "context/agent-guidelines.md", +] + + +def _read_file_safe(root: Path, relative: str) -> str: + path = root / relative + if path.exists(): + return path.read_text(encoding="utf-8") + return f"# {relative} — not found" + + +def generate_agent_prompt(root: Path) -> str: + task = get_next_task(root) + if not task: + return "No Todo task found." + + sections = [ + "You are an implementation agent.", + "", + "---", + "", + "# Read these files first", + "", + f"""- README.md +- TASKS.md +- PROJECT_STATE.md +- AGENT_HANDOFF.md""", + "", + "- context/agent-guidelines.md", + "", + "---", + "", + "# Task", + "", + f"## {task.task_id} — {task.title}", + "", + f"Status: {task.status}", + "", + f"Goal:\n{task.goal(root)}", + "", + "Acceptance Criteria:", + ] + + for line in task.ac_lines(): + sections.append(f"- {line}") + + sections.extend([ + "", + "---", + "", + "# Constraints", + "", + "- Implement ONE task only. Do not combine with other tasks.", + "- Make the smallest useful change possible.", + "", + "---", + "", + "# Validation", + "", + "- Run `rdb status` to confirm stage and task state.", + "- Run tests: `python -m pytest`.", + "- Verify acceptance criteria are met.", + "", + "---", + "", + "# Reporting", + "", + "When complete, provide:", + "- Summary of changes", + "- Files modified", + "- Test results", + "- Validation notes", + "- Update TASKS.md status to Done if all criteria pass", + "", + ]) + + return "\n".join(sections) diff --git a/tests/test_prompt.py b/tests/test_prompt.py new file mode 100644 index 0000000..15d56c8 --- /dev/null +++ b/tests/test_prompt.py @@ -0,0 +1,131 @@ +from pathlib import Path + +import pytest + +from rdb_discovery.tasks import generate_agent_prompt, get_next_task + + +def test_generate_agent_prompt_includes_task_info(tmp_path: Path) -> None: + (tmp_path / "TASKS.md").write_text( + "# TASKS\n\n" + "## TASK-001 — First\nStatus: Done\n\n" + "Goal: First goal.\n\n" + "Acceptance Criteria:\n" + "- a\n\n" + "## TASK-002 — Second\nStatus: Todo\n\n" + "Goal: Second goal.\n\n" + "Acceptance Criteria:\n" + "- b\n" + "- c\n", + encoding="utf-8", + ) + + prompt = generate_agent_prompt(tmp_path) + + assert "TASK-002" in prompt + assert "Second" in prompt + assert "Todo" in prompt + assert "Second goal." in prompt + + +def test_generate_agent_prompt_includes_acceptance_criteria(tmp_path: Path) -> None: + (tmp_path / "TASKS.md").write_text( + "# TASKS\n\n" + "## TASK-001 — First\nStatus: Done\n\n" + "Goal: x.\n\n" + "Acceptance Criteria:\n" + "- alpha\n\n" + "## TASK-002 — Second\nStatus: Todo\n\n" + "Goal: y.\n\n" + "Acceptance Criteria:\n" + "- bravo\n", + encoding="utf-8", + ) + + prompt = generate_agent_prompt(tmp_path) + + assert "- alpha" not in prompt + assert "- bravo" in prompt + + +def test_generate_agent_prompt_includes_read_instructions(tmp_path: Path) -> None: + (tmp_path / "TASKS.md").write_text( + "# TASKS\n\n" + "## TASK-001 — First\nStatus: Done\n\n" + "Goal: x.\n\n" + "## TASK-002 — Second\nStatus: Todo\n\n" + "Goal: y.\n", + encoding="utf-8", + ) + + prompt = generate_agent_prompt(tmp_path) + + assert "README.md" in prompt + assert "TASKS.md" in prompt + assert "PROJECT_STATE.md" in prompt + assert "AGENT_HANDOFF.md" in prompt + assert "context/agent-guidelines.md" in prompt + + +def test_generate_agent_prompt_includes_one_task_constraint(tmp_path: Path) -> None: + (tmp_path / "TASKS.md").write_text( + "# TASKS\n\n" + "## TASK-001 — First\nStatus: Done\n\n" + "Goal: x.\n\n" + "## TASK-002 — Second\nStatus: Todo\n\n" + "Goal: y.\n", + encoding="utf-8", + ) + + prompt = generate_agent_prompt(tmp_path) + + assert "ONE task only" in prompt or "one task only" in prompt + + +def test_generate_agent_prompt_includes_validation(tmp_path: Path) -> None: + (tmp_path / "TASKS.md").write_text( + "# TASKS\n\n" + "## TASK-001 — First\nStatus: Done\n\n" + "Goal: x.\n\n" + "## TASK-002 — Second\nStatus: Todo\n\n" + "Goal: y.\n", + encoding="utf-8", + ) + + prompt = generate_agent_prompt(tmp_path) + + assert "Validation" in prompt + assert "pytest" in prompt + + +def test_generate_agent_prompt_includes_reporting(tmp_path: Path) -> None: + (tmp_path / "TASKS.md").write_text( + "# TASKS\n\n" + "## TASK-001 — First\nStatus: Done\n\n" + "Goal: x.\n\n" + "## TASK-002 — Second\nStatus: Todo\n\n" + "Goal: y.\n", + encoding="utf-8", + ) + + prompt = generate_agent_prompt(tmp_path) + + assert "Reporting" in prompt + + +def test_generate_agent_prompt_no_todo_task(tmp_path: Path) -> None: + (tmp_path / "TASKS.md").write_text( + "# TASKS\n\n" + "## TASK-001 — First\nStatus: Done\n\n", + encoding="utf-8", + ) + + prompt = generate_agent_prompt(tmp_path) + + assert prompt == "No Todo task found." + + +def test_generate_agent_prompt_no_tasks_file(tmp_path: Path) -> None: + prompt = generate_agent_prompt(tmp_path) + + assert prompt == "No Todo task found."