Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 13 additions & 7 deletions prd_taskmaster/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions prd_taskmaster/tournament/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down
31 changes: 22 additions & 9 deletions prd_taskmaster/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
20 changes: 20 additions & 0 deletions tests/core/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
15 changes: 15 additions & 0 deletions tests/core/test_tournament_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
23 changes: 17 additions & 6 deletions tests/core/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Loading