feat(task-018): add role-based context selection
This commit is contained in:
@@ -36,3 +36,8 @@
|
||||
{"timestamp": "2026-06-02T16:29:43.405985+00:00", "event_type": "command", "target": "rdb next", "details": {}}
|
||||
{"timestamp": "2026-06-02T16:29:59.879010+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
|
||||
{"timestamp": "2026-06-02T16:47:40.958013+00:00", "event_type": "command", "target": "rdb status", "details": {}}
|
||||
{"timestamp": "2026-06-02T16:57:02.286495+00:00", "event_type": "command", "target": "rdb status", "details": {}}
|
||||
{"timestamp": "2026-06-02T16:57:05.774698+00:00", "event_type": "command", "target": "rdb next", "details": {}}
|
||||
{"timestamp": "2026-06-02T16:57:29.853052+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
|
||||
{"timestamp": "2026-06-02T17:05:41.754949+00:00", "event_type": "command", "target": "rdb status", "details": {}}
|
||||
{"timestamp": "2026-06-02T17:07:48.597850+00:00", "event_type": "command", "target": "rdb status", "details": {}}
|
||||
|
||||
@@ -309,7 +309,7 @@ Definition of Done:
|
||||
|
||||
## TASK-018 — Role-based context selection
|
||||
|
||||
Status: Todo
|
||||
Status: Done
|
||||
|
||||
Role: Implementation Agent
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ Requires-Dist: pytest>=8.0.0; extra == "dev"
|
||||
|
||||
# rdb-discovery
|
||||
|
||||
A small CLI-first tool for repeatable software project discovery and delivery.
|
||||
A small CLI-first tool that helps you generate and maintain useful project context files for AI-assisted development.
|
||||
|
||||
It creates markdown files that help humans and AI coding agents understand:
|
||||
## Purpose
|
||||
|
||||
rdb-discovery exists to make it easier for humans and AI coding agents to understand:
|
||||
|
||||
- what is being built
|
||||
- why it exists
|
||||
@@ -21,6 +23,20 @@ It creates markdown files that help humans and AI coding agents understand:
|
||||
- what task should be done next
|
||||
- what stage the project is currently in
|
||||
|
||||
The tool creates and manages a set of markdown files — your project's source-of-truth context.
|
||||
|
||||
## Core workflow
|
||||
|
||||
1. **Ask discovery questions** — `rdb discover` captures answers about the product, architecture, risks, and open questions.
|
||||
2. **Capture answers** — Structured responses are stored in `context/discovery-log.md` alongside confidence scores and follow-ups.
|
||||
3. **Generate context files** — `rdb init` creates standardised project files (`product-brief.md`, `architecture.md`, `decisions.md`, etc.).
|
||||
4. **Support agent implementation** — `rdb prompt` produces ready-to-paste implementation prompts for Claude Code or Cline, and `rdb start` / `rdb complete` manage task lifecycles.
|
||||
|
||||
## Support features (not the product)
|
||||
|
||||
- **Telemetry** records structured agent activity events in `.rdb/session-log.jsonl`. It enables future analysis but is not the main offering.
|
||||
- **Guardrails** detect agent stalls, repeated reads, and inconsistent state to keep projects on track. They support the workflow but are secondary.
|
||||
|
||||
## Install for local development
|
||||
|
||||
```bash
|
||||
@@ -32,23 +48,43 @@ pip install -e '.[dev]'
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
rdb init
|
||||
rdb discover
|
||||
rdb status
|
||||
rdb next
|
||||
rdb start TASK-001
|
||||
rdb complete TASK-001
|
||||
rdb handoff
|
||||
rdb init # Create standard project structure and context files
|
||||
rdb discover # Ask 10 core discovery questions, record answers
|
||||
rdb guardrails # Check for agent stalls and state inconsistencies
|
||||
rdb prompt # Generate implementation prompt for the next task
|
||||
rdb start TASK-001 # Mark a task as in progress
|
||||
rdb complete TASK-001# Mark a task as done
|
||||
rdb status # Show current stage and active task
|
||||
rdb next # Print the next task to work on
|
||||
rdb handoff # Generate agent handoff notes
|
||||
```
|
||||
|
||||
## Bootstrap workflow
|
||||
|
||||
1. Run `rdb init`
|
||||
2. Run `rdb discover`
|
||||
3. Run `rdb status`
|
||||
4. Run `rdb next`
|
||||
5. Give the next task to Claude Code or Cline
|
||||
6. Commit after each completed task
|
||||
3. Review `rdb status` and `rdb next`
|
||||
4. Give the next prompt to Claude Code or Cline via `rdb prompt`
|
||||
5. Commit after each completed task
|
||||
|
||||
## Project structure
|
||||
|
||||
`rdb init` creates the following files (skips existing ones):
|
||||
|
||||
- context/discovery-log.md
|
||||
- context/product-brief.md
|
||||
- context/architecture.md
|
||||
- context/decisions.md
|
||||
- context/risks.md
|
||||
- context/assumptions.md
|
||||
- context/open-questions.md
|
||||
- context/repository-context.md
|
||||
- TASKS.md
|
||||
- TEST_PLAN.md
|
||||
- RUN_LOG.md
|
||||
- PROJECT_STATE.md
|
||||
- AGENT_HANDOFF.md
|
||||
- .rdb/project.json
|
||||
|
||||
## Principle
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ pyproject.toml
|
||||
src/rdb_discovery/__init__.py
|
||||
src/rdb_discovery/cli.py
|
||||
src/rdb_discovery/discovery.py
|
||||
src/rdb_discovery/guardrails.py
|
||||
src/rdb_discovery/handoff.py
|
||||
src/rdb_discovery/status.py
|
||||
src/rdb_discovery/tasks.py
|
||||
src/rdb_discovery/telemetry.py
|
||||
src/rdb_discovery/templates.py
|
||||
src/rdb_discovery.egg-info/PKG-INFO
|
||||
src/rdb_discovery.egg-info/SOURCES.txt
|
||||
@@ -13,5 +15,10 @@ src/rdb_discovery.egg-info/dependency_links.txt
|
||||
src/rdb_discovery.egg-info/entry_points.txt
|
||||
src/rdb_discovery.egg-info/requires.txt
|
||||
src/rdb_discovery.egg-info/top_level.txt
|
||||
tests/test_discovery.py
|
||||
tests/test_guardrails.py
|
||||
tests/test_prompt.py
|
||||
tests/test_status.py
|
||||
tests/test_tasks.py
|
||||
tests/test_telemetry.py
|
||||
tests/test_templates.py
|
||||
@@ -109,7 +109,7 @@ def update_task_status(root: Path, task_id: str, new_status: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
CONTEXT_FILES_TO_READ = [
|
||||
DEFAULT_CONTEXT_FILES = [
|
||||
"README.md",
|
||||
"TASKS.md",
|
||||
"PROJECT_STATE.md",
|
||||
@@ -117,6 +117,38 @@ CONTEXT_FILES_TO_READ = [
|
||||
"context/agent-guidelines.md",
|
||||
]
|
||||
|
||||
ROLE_CONTEXT_FILES: dict[str, list[str]] = {
|
||||
"Architecture Agent": [
|
||||
"README.md",
|
||||
"TASKS.md",
|
||||
"PROJECT_STATE.md",
|
||||
"context/architecture.md",
|
||||
"context/product-brief.md",
|
||||
"context/decisions.md",
|
||||
],
|
||||
"Implementation Agent": [
|
||||
"README.md",
|
||||
"TASKS.md",
|
||||
"PROJECT_STATE.md",
|
||||
"AGENT_HANDOFF.md",
|
||||
"context/agent-guidelines.md",
|
||||
"CLAUDE.md",
|
||||
],
|
||||
"Documentation Agent": [
|
||||
"README.md",
|
||||
"TASKS.md",
|
||||
"PROJECT_STATE.md",
|
||||
"AGENT_HANDOFF.md",
|
||||
"context/agent-guidelines.md",
|
||||
"TEST_PLAN.md",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _context_files_for_role(role: str) -> list[str]:
|
||||
"""Return the context file list for a given role, falling back to default."""
|
||||
return ROLE_CONTEXT_FILES.get(role, DEFAULT_CONTEXT_FILES)
|
||||
|
||||
TEST_CMD_SECTION_RE = re.compile(
|
||||
r"^##\s*Test Commands\s*\n([\s\S]*?)(?=^##|\Z)",
|
||||
re.MULTILINE,
|
||||
@@ -186,6 +218,7 @@ def generate_agent_prompt(root: Path) -> str:
|
||||
return "No Todo task found."
|
||||
|
||||
role_text, article = task.role(root)
|
||||
context_files = _context_files_for_role(role_text)
|
||||
|
||||
sections = [
|
||||
f"You are {article} {role_text.lower()}.",
|
||||
@@ -194,13 +227,15 @@ def generate_agent_prompt(root: Path) -> str:
|
||||
"",
|
||||
"# Read these files first",
|
||||
"",
|
||||
f"""- README.md
|
||||
- TASKS.md
|
||||
- PROJECT_STATE.md
|
||||
- AGENT_HANDOFF.md""",
|
||||
"",
|
||||
"- context/agent-guidelines.md",
|
||||
"",
|
||||
]
|
||||
for i, f in enumerate(context_files):
|
||||
line = f"- {f}"
|
||||
if i == len(context_files) - 1:
|
||||
sections.append(line)
|
||||
else:
|
||||
sections.append(line)
|
||||
|
||||
sections.extend([
|
||||
"---",
|
||||
"",
|
||||
"# Task",
|
||||
@@ -210,7 +245,7 @@ def generate_agent_prompt(root: Path) -> str:
|
||||
f"Status: {task.status}",
|
||||
"",
|
||||
f"Goal:\n{task.goal(root)}",
|
||||
]
|
||||
])
|
||||
|
||||
gap = task.implementation_gap(root)
|
||||
if gap:
|
||||
|
||||
@@ -322,3 +322,94 @@ def test_generate_agent_prompt_with_custom_role(tmp_path: Path) -> None:
|
||||
prompt = generate_agent_prompt(tmp_path)
|
||||
|
||||
assert "You are a devops engineer." in prompt
|
||||
|
||||
|
||||
def test_architecture_agent_receives_architecture_context(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"
|
||||
"Role: Architecture Agent\n"
|
||||
"Goal: y.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
prompt = generate_agent_prompt(tmp_path)
|
||||
|
||||
assert "context/architecture.md" in prompt
|
||||
assert "context/product-brief.md" in prompt
|
||||
assert "context/decisions.md" in prompt
|
||||
|
||||
|
||||
def test_implementation_agent_receives_implementation_context(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"
|
||||
"Role: Implementation Agent\n"
|
||||
"Goal: y.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
prompt = generate_agent_prompt(tmp_path)
|
||||
|
||||
assert "AGENT_HANDOFF.md" in prompt
|
||||
assert "context/agent-guidelines.md" in prompt
|
||||
assert "CLAUDE.md" in prompt
|
||||
|
||||
|
||||
def test_documentation_agent_receives_documentation_context(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"
|
||||
"Role: Documentation Agent\n"
|
||||
"Goal: y.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
prompt = generate_agent_prompt(tmp_path)
|
||||
|
||||
assert "TEST_PLAN.md" in prompt
|
||||
assert "AGENT_HANDOFF.md" in prompt
|
||||
|
||||
|
||||
def test_context_files_differ_by_role(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 — Architect\nStatus: Todo\n\n"
|
||||
"Role: Architecture Agent\n"
|
||||
"Goal: y.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
prompt = generate_agent_prompt(tmp_path)
|
||||
|
||||
# Architecture role should have architecture-specific files
|
||||
assert "context/architecture.md" in prompt
|
||||
|
||||
|
||||
def test_no_role_uses_default_context(tmp_path: Path) -> None:
|
||||
"""When no Role is set, the default file list is used (backward compatible)."""
|
||||
(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)
|
||||
|
||||
# Default files must still be present
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user