Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21611b7c4e | ||
|
|
9536f5d0ff | ||
|
|
5456947ac9 | ||
|
|
39728667a7 |
@@ -18,3 +18,29 @@ Rules:
|
|||||||
- Stop.
|
- Stop.
|
||||||
|
|
||||||
Do not start the next task automatically.
|
Do not start the next task automatically.
|
||||||
|
|
||||||
|
## Test Commands
|
||||||
|
|
||||||
|
Use the existing virtual environment.
|
||||||
|
|
||||||
|
From the repository root, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pytest
|
||||||
|
|
||||||
|
Do not search the filesystem for pytest.
|
||||||
|
Do not create a new virtual environment unless explicitly asked.
|
||||||
|
|
||||||
|
If pytest is unavailable, run:
|
||||||
|
|
||||||
|
pip install -e '.[dev]'
|
||||||
|
python -m pytest
|
||||||
|
Validation Commands
|
||||||
|
|
||||||
|
For normal task validation, run:
|
||||||
|
|
||||||
|
python -m pytest
|
||||||
|
rdb status
|
||||||
|
rdb next
|
||||||
|
```
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ TASK-001 marked Done — rdb init creates all project control files.
|
|||||||
2026-06-02
|
2026-06-02
|
||||||
Added TASK-002 for agent prompt generation.
|
Added TASK-002 for agent prompt generation.
|
||||||
TASK-002 marked Done — agent prompt generation complete.
|
TASK-002 marked Done — agent prompt generation complete.
|
||||||
|
| 2026-06-02T10:01:31 | Task completed | TASK-004 | 28 tests passed |
|
||||||
|
| 2026-06-02T10:02:18 | Task completed | TASK-004 | rdb ask-more implemented and tested |
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Acceptance Criteria:
|
|||||||
|
|
||||||
## TASK-004 — Implement ask-more command
|
## TASK-004 — Implement ask-more command
|
||||||
|
|
||||||
Status: Todo
|
Status: Done
|
||||||
|
|
||||||
Goal: Add a command that finds weak answers and asks deeper follow-up questions.
|
Goal: Add a command that finds weak answers and asks deeper follow-up questions.
|
||||||
|
|
||||||
@@ -72,3 +72,41 @@ Acceptance Criteria:
|
|||||||
- command reads discovery-log.md
|
- command reads discovery-log.md
|
||||||
- low-confidence answers are detected
|
- low-confidence answers are detected
|
||||||
- follow-up answers are appended to discovery-log.md
|
- follow-up answers are appended to discovery-log.md
|
||||||
|
|
||||||
|
## TASK-005 — Task lifecycle commands
|
||||||
|
|
||||||
|
Status: Done
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
Allow tasks to be managed from the CLI rather than manually editing TASKS.md.
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
|
||||||
|
- `rdb start TASK-ID`
|
||||||
|
- Marks task In Progress
|
||||||
|
|
||||||
|
- `rdb complete TASK-ID`
|
||||||
|
- Marks task Done
|
||||||
|
|
||||||
|
- Updates PROJECT_STATE.md
|
||||||
|
- Updates AGENT_HANDOFF.md
|
||||||
|
- Updates RUN_LOG.md
|
||||||
|
- Preserves task formatting
|
||||||
|
- Add/update tests
|
||||||
|
- Run python -m pytest
|
||||||
|
|
||||||
|
## TASK-007 — Improve generated agent prompts
|
||||||
|
|
||||||
|
Status: Todo
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
Make `rdb prompt` produce smaller, more direct prompts for Claude Code/local LLM agents.
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
|
||||||
|
- Prompt includes exact known implementation gap when available
|
||||||
|
- Prompt includes existing test command from CLAUDE.md
|
||||||
|
- Prompt tells agent not to repeatedly reread unchanged files
|
||||||
|
- Prompt tells agent to inspect first, then edit
|
||||||
|
- Prompt limits scope to one small implementation step
|
||||||
|
- Add/update tests
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import typer
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from .discovery import append_discovery_answer, core_questions
|
from .discovery import append_discovery_answer, append_followup_answer, core_questions, read_discovery_answers
|
||||||
from .handoff import build_handoff
|
from .handoff import build_handoff
|
||||||
from .status import project_stage, task_counts
|
from .status import project_stage, task_counts, update_project_state, update_agent_handoff
|
||||||
from .tasks import generate_agent_prompt, 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
|
from .templates import CONTEXT_FILES, write_file_if_missing
|
||||||
|
|
||||||
@@ -71,6 +71,49 @@ def discover() -> None:
|
|||||||
console.print("[bold green]Discovery complete.[/bold green]")
|
console.print("[bold green]Discovery complete.[/bold green]")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def ask_more() -> None:
|
||||||
|
"""Ask for additional details on Low-confidence or follow-up-needed answers."""
|
||||||
|
root = root_path()
|
||||||
|
answers = read_discovery_answers(root)
|
||||||
|
|
||||||
|
if not answers:
|
||||||
|
console.print("[yellow]No discovery answers found. Run `discover` first.[/yellow]")
|
||||||
|
raise typer.Exit(code=0)
|
||||||
|
|
||||||
|
low_conf = [a for a in answers if a["low_confidence"]]
|
||||||
|
follow_ups = [a for a in answers if a["needs_followup"]]
|
||||||
|
|
||||||
|
flagged: dict[str, list[str]] = {} # question_id -> list of reasons
|
||||||
|
for a in low_conf:
|
||||||
|
flagged.setdefault(a["id"], []).append("Low confidence")
|
||||||
|
for a in follow_ups:
|
||||||
|
flagged.setdefault(a["id"], []).append("Follow-up needed")
|
||||||
|
|
||||||
|
if not flagged:
|
||||||
|
console.print("[green]No Low-confidence or Follow-up-needed answers found.[/green]")
|
||||||
|
raise typer.Exit(code=0)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for qid, reasons in sorted(flagged.items()):
|
||||||
|
original = next(a for a in answers if a["id"] == qid)
|
||||||
|
console.print(f"\n[yellow]Question:[/yellow] {original['question']}")
|
||||||
|
console.print(f"[dim]Reasons: {', '.join(reasons)}[/dim]")
|
||||||
|
|
||||||
|
fu_question = typer.prompt(
|
||||||
|
f"Follow-up for {qid}",
|
||||||
|
default=f"Additional detail on: {original['question'][:40]}",
|
||||||
|
)
|
||||||
|
fu_answer = typer.prompt("New answer")
|
||||||
|
fu_confidence = typer.prompt("Confidence?", default="Medium")
|
||||||
|
|
||||||
|
append_followup_answer(root, qid, fu_question, fu_answer, fu_confidence)
|
||||||
|
count += 1
|
||||||
|
console.print(f"[green]Recorded follow-up for {qid}.[/green]")
|
||||||
|
|
||||||
|
console.print(f"\n[bold green]Ask-more complete: recorded {count} follow-up(s).[/bold green]")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def status() -> None:
|
def status() -> None:
|
||||||
"""Show current project stage and task counts."""
|
"""Show current project stage and task counts."""
|
||||||
@@ -112,6 +155,8 @@ def start(task_id: str) -> None:
|
|||||||
console.print(f"[red]Task not found:[/red] {task_id}")
|
console.print(f"[red]Task not found:[/red] {task_id}")
|
||||||
raise typer.Exit(code=1)
|
raise typer.Exit(code=1)
|
||||||
append_run_log(root, "Task started", task_id=task_id)
|
append_run_log(root, "Task started", task_id=task_id)
|
||||||
|
update_project_state(root, task_id)
|
||||||
|
update_agent_handoff(root, task_id)
|
||||||
console.print(f"[green]Started {task_id}.[/green]")
|
console.print(f"[green]Started {task_id}.[/green]")
|
||||||
|
|
||||||
|
|
||||||
@@ -124,6 +169,8 @@ def complete(task_id: str) -> None:
|
|||||||
console.print(f"[red]Task not found:[/red] {task_id}")
|
console.print(f"[red]Task not found:[/red] {task_id}")
|
||||||
raise typer.Exit(code=1)
|
raise typer.Exit(code=1)
|
||||||
append_run_log(root, "Task completed", task_id=task_id, notes=notes)
|
append_run_log(root, "Task completed", task_id=task_id, notes=notes)
|
||||||
|
update_project_state(root, task_id)
|
||||||
|
update_agent_handoff(root, task_id)
|
||||||
console.print(f"[green]Completed {task_id}.[/green]")
|
console.print(f"[green]Completed {task_id}.[/green]")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,3 +36,71 @@ def append_discovery_answer(
|
|||||||
|
|
||||||
def core_questions() -> list[str]:
|
def core_questions() -> list[str]:
|
||||||
return CORE_QUESTIONS
|
return CORE_QUESTIONS
|
||||||
|
|
||||||
|
|
||||||
|
def read_discovery_answers(root: Path) -> list[dict]:
|
||||||
|
"""Read discovery-log.md and return parsed rows as dicts.
|
||||||
|
|
||||||
|
Each dict has keys: id, question, answer, confidence, follow_up_needed.
|
||||||
|
Rows with 'Low' confidence or follow_up_needed == 'Yes' are flagged
|
||||||
|
via ``needs_followup`` and ``low_confidence`` boolean fields.
|
||||||
|
"""
|
||||||
|
log_path = root / "context" / "discovery-log.md"
|
||||||
|
if not log_path.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
content = log_path.read_text(encoding="utf-8")
|
||||||
|
lines = content.splitlines()
|
||||||
|
|
||||||
|
answers: list[dict] = []
|
||||||
|
for line in lines:
|
||||||
|
# Skip header and non-data rows
|
||||||
|
if not line.startswith("| Q-"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
cells = [c.strip() for c in line.split("|")[1:-1]]
|
||||||
|
if len(cells) < 5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
answers.append({
|
||||||
|
"id": cells[0],
|
||||||
|
"question": cells[1],
|
||||||
|
"answer": cells[2].replace("\\|", "|"),
|
||||||
|
"confidence": cells[3],
|
||||||
|
"follow_up_needed": cells[4],
|
||||||
|
"needs_followup": cells[4] == "Yes",
|
||||||
|
"low_confidence": cells[3] in ("Low", "low"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return answers
|
||||||
|
|
||||||
|
|
||||||
|
def append_followup_answer(
|
||||||
|
root: Path,
|
||||||
|
question_id: str,
|
||||||
|
follow_up_question: str,
|
||||||
|
answer: str,
|
||||||
|
confidence: str = "Medium",
|
||||||
|
) -> None:
|
||||||
|
"""Append a follow-up answer row to discovery-log.md.
|
||||||
|
|
||||||
|
The original ``question_id`` is stored in the 'Linked decision' column.
|
||||||
|
"""
|
||||||
|
log_path = root / "context" / "discovery-log.md"
|
||||||
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not log_path.exists():
|
||||||
|
log_path.write_text(
|
||||||
|
"# Discovery Log\n\n"
|
||||||
|
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
|
||||||
|
"|---|---|---|---|---|---|---|---|\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
safe_answer = answer.replace("|", "\\|").replace("\n", " ").strip()
|
||||||
|
line = (
|
||||||
|
f"| {question_id} | {follow_up_question} | {safe_answer} | {confidence} | "
|
||||||
|
f"Yes | | | {date.today().isoformat()} |\n"
|
||||||
|
)
|
||||||
|
with log_path.open("a", encoding="utf-8") as handle:
|
||||||
|
handle.write(line)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
from .tasks import read_tasks
|
from .tasks import read_tasks
|
||||||
|
|
||||||
@@ -23,3 +25,38 @@ def task_counts(root: Path) -> dict[str, int]:
|
|||||||
for task in read_tasks(root):
|
for task in read_tasks(root):
|
||||||
counts[task.status] = counts.get(task.status, 0) + 1
|
counts[task.status] = counts.get(task.status, 0) + 1
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
_CURRENT_TASK_RE = re.compile(r"^(Current Task): .+$", re.MULTILINE)
|
||||||
|
_UPDATED_RE = re.compile(r"^(Last Updated): .+$", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def update_project_state(root: Path, task_id: str = "None") -> bool:
|
||||||
|
"""Update PROJECT_STATE.md current task and timestamp."""
|
||||||
|
path = root / "PROJECT_STATE.md"
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
text = _CURRENT_TASK_RE.sub(f"Current Task: {task_id}", text, count=1)
|
||||||
|
text = _UPDATED_RE.sub(f"Last Updated: {date.today().isoformat()}", text, count=1)
|
||||||
|
path.write_text(text, encoding="utf-8")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
_STAGE_RE = re.compile(r"(## Current Stage\n\n)[\s\S]+?(?=\n)", re.MULTILINE | re.DOTALL)
|
||||||
|
_TASK_RE = re.compile(r"(## Current Task\n\n)[\s\S]+?(?=\n)", re.MULTILINE | re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def update_agent_handoff(root: Path, task_id: str = "None") -> bool:
|
||||||
|
"""Update AGENT_HANDOFF.md current stage and task."""
|
||||||
|
path = root / "AGENT_HANDOFF.md"
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
stage = project_stage(root)
|
||||||
|
text = _STAGE_RE.sub(f"## Current Stage\n\n{stage}", text, count=1)
|
||||||
|
text = _TASK_RE.sub(f"## Current Task\n\n{task_id}", text, count=1)
|
||||||
|
path.write_text(text, encoding="utf-8")
|
||||||
|
return True
|
||||||
|
|||||||
@@ -105,6 +105,96 @@ def test_append_preserves_existing_history(tmp_path: Path) -> None:
|
|||||||
assert len(data_rows) == 2
|
assert len(data_rows) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_discovery_answers_returns_dicts(tmp_path: Path) -> None:
|
||||||
|
discovery_mod.append_discovery_answer(
|
||||||
|
tmp_path,
|
||||||
|
question_id="Q-001",
|
||||||
|
question="What is this?",
|
||||||
|
answer="A thing",
|
||||||
|
confidence="High",
|
||||||
|
follow_up_needed="No",
|
||||||
|
)
|
||||||
|
|
||||||
|
answers = discovery_mod.read_discovery_answers(tmp_path)
|
||||||
|
assert len(answers) == 1
|
||||||
|
assert answers[0]["id"] == "Q-001"
|
||||||
|
assert answers[0]["question"] == "What is this?"
|
||||||
|
assert answers[0]["answer"] == "A thing"
|
||||||
|
assert answers[0]["confidence"] == "High"
|
||||||
|
assert answers[0]["follow_up_needed"] == "No"
|
||||||
|
assert answers[0]["needs_followup"] is False
|
||||||
|
assert answers[0]["low_confidence"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_discovery_answers_returns_empty_when_no_log(tmp_path: Path) -> None:
|
||||||
|
log_path = tmp_path / "context" / "discovery-log.md"
|
||||||
|
assert not log_path.exists()
|
||||||
|
|
||||||
|
answers = discovery_mod.read_discovery_answers(tmp_path)
|
||||||
|
assert answers == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_discovery_answers_detects_low_confidence(tmp_path: Path) -> None:
|
||||||
|
discovery_mod.append_discovery_answer(
|
||||||
|
tmp_path,
|
||||||
|
question_id="Q-002",
|
||||||
|
question="How much?",
|
||||||
|
answer="About half",
|
||||||
|
confidence="Low",
|
||||||
|
follow_up_needed="No",
|
||||||
|
)
|
||||||
|
|
||||||
|
answers = discovery_mod.read_discovery_answers(tmp_path)
|
||||||
|
low_conf_rows = [a for a in answers if a["low_confidence"]]
|
||||||
|
assert len(low_conf_rows) == 1
|
||||||
|
assert low_conf_rows[0]["id"] == "Q-002"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_discovery_answers_detects_followup_flag(tmp_path: Path) -> None:
|
||||||
|
discovery_mod.append_discovery_answer(
|
||||||
|
tmp_path,
|
||||||
|
question_id="Q-003",
|
||||||
|
question="What next?",
|
||||||
|
answer="TBD",
|
||||||
|
confidence="High",
|
||||||
|
follow_up_needed="Yes",
|
||||||
|
)
|
||||||
|
|
||||||
|
answers = discovery_mod.read_discovery_answers(tmp_path)
|
||||||
|
fu_rows = [a for a in answers if a["needs_followup"]]
|
||||||
|
assert len(fu_rows) == 1
|
||||||
|
assert fu_rows[0]["id"] == "Q-003"
|
||||||
|
|
||||||
|
|
||||||
|
def test_append_followup_appends_row(tmp_path: Path) -> None:
|
||||||
|
# First create the log with an existing entry
|
||||||
|
discovery_mod.append_discovery_answer(
|
||||||
|
tmp_path,
|
||||||
|
question_id="Q-010",
|
||||||
|
question="Original?",
|
||||||
|
answer="Original answer",
|
||||||
|
confidence="High",
|
||||||
|
follow_up_needed="No",
|
||||||
|
)
|
||||||
|
|
||||||
|
original_count = len(discovery_mod.read_discovery_answers(tmp_path))
|
||||||
|
|
||||||
|
discovery_mod.append_followup_answer(
|
||||||
|
tmp_path,
|
||||||
|
question_id="Q-010",
|
||||||
|
follow_up_question="Can you elaborate?",
|
||||||
|
answer="Yes, it's bigger than expected.",
|
||||||
|
confidence="Medium",
|
||||||
|
)
|
||||||
|
|
||||||
|
answers = discovery_mod.read_discovery_answers(tmp_path)
|
||||||
|
assert len(answers) == original_count + 1
|
||||||
|
# The new row should have the original Q-010 id and follow-up data
|
||||||
|
new_row = [a for a in answers if a["question"] == "Can you elaborate?"][0]
|
||||||
|
assert new_row["follow_up_needed"] == "Yes"
|
||||||
|
assert new_row["confidence"] == "Medium"
|
||||||
|
|
||||||
|
|
||||||
def test_header_is_written_on_create(tmp_path: Path) -> None:
|
def test_header_is_written_on_create(tmp_path: Path) -> None:
|
||||||
discovery_mod.append_discovery_answer(
|
discovery_mod.append_discovery_answer(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from rdb_discovery.status import (
|
||||||
|
update_project_state,
|
||||||
|
update_agent_handoff,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_project_state_updates_task_id(tmp_path: Path) -> None:
|
||||||
|
ps = tmp_path / "PROJECT_STATE.md"
|
||||||
|
ps.write_text(
|
||||||
|
"# Project State\n\nCurrent Stage: DISCOVERY\nPrevious Stage: NONE\nNext Stage: BOOTSTRAP_READY\n\nCurrent Task: None\nActive Branch: main\n\nLast Updated: 2026-01-01\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert update_project_state(tmp_path, "TASK-005") is True
|
||||||
|
|
||||||
|
content = ps.read_text(encoding="utf-8")
|
||||||
|
assert "Current Task: TASK-005" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_project_state_updates_timestamp(tmp_path: Path) -> None:
|
||||||
|
ps = tmp_path / "PROJECT_STATE.md"
|
||||||
|
ps.write_text(
|
||||||
|
"# Project State\n\nCurrent Stage: DISCOVERY\nPrevious Stage: NONE\nNext Stage: BOOTSTRAP_READY\n\nCurrent Task: None\nActive Branch: main\n\nLast Updated: 2026-01-01\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert update_project_state(tmp_path, "TASK-999") is True
|
||||||
|
|
||||||
|
content = ps.read_text(encoding="utf-8")
|
||||||
|
from datetime import date
|
||||||
|
expected_date = date.today().isoformat()
|
||||||
|
assert f"Last Updated: {expected_date}" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_project_state_no_file_returns_false(tmp_path: Path) -> None:
|
||||||
|
assert update_project_state(tmp_path, "TASK-001") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_agent_handoff_updates_task_and_stage(tmp_path: Path) -> None:
|
||||||
|
ah = tmp_path / "AGENT_HANDOFF.md"
|
||||||
|
ah.write_text(
|
||||||
|
"# Agent Handoff\n\n## Current Stage\n\nDISCOVERY\n\n## Current Task\n\nNone\n\n## Instructions For Agent\n\n- Complete one task only\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert update_agent_handoff(tmp_path, "TASK-005") is True
|
||||||
|
|
||||||
|
content = ah.read_text(encoding="utf-8")
|
||||||
|
assert "Current Task" in content
|
||||||
|
assert "TASK-005" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_agent_handoff_no_file_returns_false(tmp_path: Path) -> None:
|
||||||
|
assert update_agent_handoff(tmp_path, "TASK-001") is False
|
||||||
Reference in New Issue
Block a user