diff --git a/.gitignore b/.gitignore index cde10a2..9dccfa1 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ SHARING_TEMPLATES.md .taskmaster/ .atlas-ai/ +# walkthrough RAG runlog store — local-only by owner decision (2026-06-17) +.i-walkthroughs/ + # Logs logs *.log diff --git a/prd_taskmaster/cli.py b/prd_taskmaster/cli.py index 41a832a..0011a7a 100644 --- a/prd_taskmaster/cli.py +++ b/prd_taskmaster/cli.py @@ -176,7 +176,10 @@ def build_parser() -> argparse.ArgumentParser: # engine-preflight (batched Phase 1: preflight + taskmaster + providers + capabilities) p = sub.add_parser("engine-preflight", help="One-call Phase 1: all probes + summary") - p.add_argument("--no-configure", action="store_true") + p.add_argument( + "--no-configure", action="store_true", + help="Skip auto-configuring providers; read-only probe (default configures providers on an existing .taskmaster project)", + ) # detect-taskmaster sub.add_parser("detect-taskmaster", help="Find MCP or CLI taskmaster") @@ -329,7 +332,7 @@ def build_parser() -> argparse.ArgumentParser: # economy-report p = sub.add_parser("economy-report", help="Summarize .atlas-ai/telemetry.jsonl per (op_class, model)") - p.add_argument("--input", default=None) + p.add_argument("--input", default=None, help="Telemetry JSONL path (default: .atlas-ai/telemetry.jsonl)") # context-pack p = sub.add_parser("context-pack", help="Extract AST-based Python signature context") @@ -357,17 +360,20 @@ def build_parser() -> argparse.ArgumentParser: # next-task p = sub.add_parser("next-task", help="Select the next TaskMaster-compatible task") - p.add_argument("--tag") + p.add_argument("--tag", help="Tasks.json tag context to read (default: the active/master tag)") # claim-task p = sub.add_parser("claim-task", help="Atomically select and claim the next task") - p.add_argument("--tag") + p.add_argument("--tag", help="Tasks.json tag context to read (default: the active/master tag)") # set-status p = sub.add_parser("set-status", help="Set a task or subtask status") - p.add_argument("--id", required=True) - p.add_argument("--status", required=True) - p.add_argument("--tag") + p.add_argument("--id", required=True, help="Task or subtask id (e.g. 7 or 1.2)") + p.add_argument( + "--status", required=True, + help="Task status; one of: pending, in-progress, done, review, deferred, cancelled, blocked", + ) + p.add_argument("--tag", help="Tasks.json tag context to write (default: the active/master tag)") p.add_argument( "--evidence-ref", default=None, diff --git a/prd_taskmaster/tournament/watcher.py b/prd_taskmaster/tournament/watcher.py index f0fc4cd..c060d60 100644 --- a/prd_taskmaster/tournament/watcher.py +++ b/prd_taskmaster/tournament/watcher.py @@ -429,12 +429,15 @@ def permit_enforce_slash( f"permitted: {len(confirmed)} confirmed slash(es), " f"concordance={concordance:.3f} over {observations} prior decisions" ) - elif not to_slash: - reason = "blocked: no to-be-slashed submissions to confirm" + # Surface the most-serious finding FIRST: a discrepancy (incl. a cheating + # winner) or an abstain must never be masked by the benign "nothing to + # slash" message when to_slash happens to be empty. elif discrepancies: reason = f"blocked: {len(discrepancies)} discrepancy(ies) in the job" elif abstained: reason = f"blocked: {len(abstained)} submission(s) in the job could not be independently verified (abstain)" + elif not to_slash: + reason = "blocked: no to-be-slashed submissions to confirm" elif not track_record_ok: reason = ( f"blocked: thin track record — {observations}/{MIN_OBSERVATIONS} prior " diff --git a/prd_taskmaster/validation.py b/prd_taskmaster/validation.py index 3368ca4..b97dbe3 100644 --- a/prd_taskmaster/validation.py +++ b/prd_taskmaster/validation.py @@ -123,15 +123,28 @@ def run_validate_prd(input_path: str) -> dict: reqs_section = get_section_content(text, "Functional Requirements") if not reqs_section: reqs_section = get_section_content(text, "Requirements") - vague_in_reqs = VAGUE_PATTERN.findall(reqs_section) - checks.append({ - "id": 6, - "category": "required", - "name": "Functional requirements are testable", - "passed": len(vague_in_reqs) == 0, - "detail": f"Vague terms found: {vague_in_reqs}" if vague_in_reqs else "All requirements are specific", - "points": 5, - }) + if not reqs_section.strip(): + # Fail-closed: an ABSENT requirements section must not vacuously pass and + # claim "all requirements are specific" — functional requirements aren't + # optional, so their absence is itself a testability failure. + checks.append({ + "id": 6, + "category": "required", + "name": "Functional requirements are testable", + "passed": False, + "detail": "No requirements section found", + "points": 5, + }) + else: + vague_in_reqs = VAGUE_PATTERN.findall(reqs_section) + checks.append({ + "id": 6, + "category": "required", + "name": "Functional requirements are testable", + "passed": len(vague_in_reqs) == 0, + "detail": f"Vague terms found: {vague_in_reqs}" if vague_in_reqs else "All requirements are specific", + "points": 5, + }) # Check 7: Each requirement has priority (Must/Should/Could or P0/P1/P2) has_priority = bool(re.search( diff --git a/tests/core/test_cli.py b/tests/core/test_cli.py index 8bdbf74..f681a6f 100644 --- a/tests/core/test_cli.py +++ b/tests/core/test_cli.py @@ -390,3 +390,23 @@ def test_native_expand_and_rate_no_key_return_agent_action_json(tmp_path, monkey ) assert rate["ok"] is False assert rate["agent_action_required"]["op"] == "rate" + + +def test_cli_flags_have_discoverable_help(): + """Walkthrough fix: previously-undocumented flags must carry help text; + set-status --status must reveal its valid vocabulary.""" + from prd_taskmaster.cli import build_parser + p = build_parser() + subs = p._subparsers._group_actions[0].choices + + def flag_help(cmd, flag): + for a in subs[cmd]._actions: + if flag in a.option_strings: + return a.help + return None + + assert flag_help("set-status", "--status"), "set-status --status needs help" + assert "in-progress" in (flag_help("set-status", "--status") or ""), "status vocab must be discoverable" + assert flag_help("engine-preflight", "--no-configure"), "--no-configure needs help" + assert flag_help("next-task", "--tag"), "next-task --tag needs help" + assert flag_help("economy-report", "--input"), "economy-report --input needs help" diff --git a/tests/core/test_tournament_watcher.py b/tests/core/test_tournament_watcher.py index 74d79c7..87ecd16 100644 --- a/tests/core/test_tournament_watcher.py +++ b/tests/core/test_tournament_watcher.py @@ -323,6 +323,21 @@ def test_permit_blocks_on_abstained_winner(tmp_path): assert "ex-win" in out["abstained"] +def test_permit_reason_surfaces_winner_discrepancy_over_empty_slash(tmp_path): + # A cheating winner (recorded PASS, watcher DISCREPANCY) with NO to-be-slashed + # subs must surface the discrepancy in the reason, not a benign 'no to-be-slashed'. + ledger = tmp_path / "watcher.jsonl" + _seed_ledger(ledger, decisions=watcher.MIN_OBSERVATIONS, confirmed=watcher.MIN_OBSERVATIONS) + record = {"job_id": "job-1", "submissions": [ + _v("ex-win", agreement="DISCREPANCY", slash_grounds=True, recorded_passes_both=True), + ]} + out = watcher.permit_enforce_slash(record, ledger_path=ledger) + assert out["permitted"] is False + assert "ex-win" in out["discrepancies"] + assert "discrepanc" in out["reason"].lower() + assert "no to-be-slashed" not in out["reason"].lower() + + def test_permit_blocks_on_empty_to_slash(tmp_path): # No to-be-slashed submissions → never a blanket green-light. ledger = tmp_path / "watcher.jsonl" diff --git a/tests/core/test_validation.py b/tests/core/test_validation.py index a3a12ee..372860e 100644 --- a/tests/core/test_validation.py +++ b/tests/core/test_validation.py @@ -405,12 +405,23 @@ def test_validate_empty_prd(self, tmp_project): empty_prd.write_text("") out = run_validate_prd(str(empty_prd)) assert out["grade"] == "NEEDS_WORK" - # 3 checks pass vacuously on empty PRD: - # ch 5: no stories found → pass - # ch 6: no vague reqs → pass - # ch 10: no NFR section → pass - assert out["checks_passed"] == 3 - assert out["score"] == 13 # 5 + 5 + 3 + # 2 checks pass on absent-but-OPTIONAL sections; ch 6 (functional + # requirements) now FAILS-closed on an absent requirements section rather + # than vacuously claiming "all requirements are specific". + # ch 5: no stories found → pass (user stories optional) + # ch 10: no NFR section → pass (NFRs optional) + assert out["checks_passed"] == 2 + assert out["score"] == 8 # 5 (ch5) + 3 (ch10) + + def test_check6_fails_closed_on_absent_requirements(self, tmp_project): + """Check 6 must not vacuously pass (and assert specificity) when there is + no requirements section at all — functional requirements aren't optional.""" + prd = tmp_project / ".taskmaster" / "docs" / "prd.md" + prd.write_text("# PRD: Thing\n\n## Overview\nA thing.\n") # no requirements section + out = run_validate_prd(str(prd)) + check6 = next(c for c in out["checks"] if c["id"] == 6) + assert check6["passed"] is False + assert "no requirements" in check6["detail"].lower() def test_validate_grade_boundaries(self, tmp_project): """Verify grade boundary calculations match documented thresholds."""