feat: add read_discovery_answers, append_followup_answer, and ask-more command
- Add read_discovery_answers() to parse discovery-log.md rows into dicts with low_confidence and needs_followup boolean flags. - Add append_followup_answer() to append follow-up rows linked to original Q-A IDs. - Add 'ask-more' CLI command that detects Low-confidence and Follow-up-needed answers, prompts for additional details, and records them in the log. - Add 6 tests: read_discovery_answers round-trip, empty-log handling, low-confidence detection, follow-up flag detection, and append_followup.
This commit is contained in:
@@ -7,7 +7,7 @@ import typer
|
||||
from rich.console import Console
|
||||
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 .status import project_stage, task_counts
|
||||
from .tasks import generate_agent_prompt, get_next_task, update_task_status
|
||||
@@ -71,6 +71,49 @@ def discover() -> None:
|
||||
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()
|
||||
def status() -> None:
|
||||
"""Show current project stage and task counts."""
|
||||
|
||||
@@ -36,3 +36,71 @@ def append_discovery_answer(
|
||||
|
||||
def core_questions() -> list[str]:
|
||||
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)
|
||||
|
||||
@@ -105,6 +105,96 @@ def test_append_preserves_existing_history(tmp_path: Path) -> None:
|
||||
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:
|
||||
discovery_mod.append_discovery_answer(
|
||||
tmp_path,
|
||||
|
||||
Reference in New Issue
Block a user