
Grill Me
- 1k installs
- 23.5k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
grill-me is an agent skill with Python companion scripts that runs structured interrogation sessions to pressure-test plans, decisions, and assumptions for developers before committing code or resources.
About
grill-me is a companion-enhanced agent skill layered on Matt's grill-me interrogation workflow, bundled with stdlib Python validation tools in alirezarezvani/claude-skills. Scripts include decision_tree_extractor.py to scan plan docs for decision branches tagged as intent, choice, open, tradeoff, dependency, or question; question_generator.py to produce forcing questions with recommended answers and dependency-aware ordering; and grill_session_tracker.py for JSON-backed session storage in ~/.grill_sessions/ to track and resume answers across turns. Developers reach for grill-me when a technical plan, architecture doc, or product decision needs adversarial questioning before implementation starts. The skill is planning-focused, not a code generator or test runner.
- Extracts decision branches (intent, choice, tradeoff, dependency, question) from any plan document
- Generates dependency-aware forcing questions with recommended answers
- JSON-backed session tracker that persists across days and supports concurrent grills
- cs-grill-master persona enforces one-question-at-a-time rule with codebase-first exploration
- Three stdlib-only Python tools plus slash command and agent persona
Grill Me by the numbers
- 1,019 all-time installs (skills.sh)
- +19 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #461 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/alirezarezvani/claude-skills --skill grill-meAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1k |
|---|---|
| repo stars | ★ 23.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
How do you pressure-test a technical plan before coding?
Run structured interrogation sessions that pressure-test plans, decisions, and assumptions before committing code or resources.
Who is it for?
Developers or tech leads with a written plan, RFC, or architecture doc who want structured adversarial questioning before writing code.
Skip if: Teams that already have a finalized spec and only need implementation, test generation, or automated linting without plan interrogation.
When should I use this skill?
The user wants to grill, challenge, or pressure-test a plan, RFC, architecture decision, or assumptions before committing resources.
What you get
Extracted decision trees, ordered forcing questions with recommended answers, and resumable JSON session records in ~/.grill_sessions/.
- Decision branch map
- Ordered forcing questions
- Resumable grill session JSON
By the numbers
- Bundles 3 stdlib Python validation scripts
- Extracts 6 decision branch types from plan documents
- Stores sessions as JSON in ~/.grill_sessions/
Files
Grill Me
Derived from Matt Pocock's grill-me (MIT). Matt's interview discipline preserved verbatim. Additions: extraction + question + session tools + references + cs-* wrapper (see references/companion_tooling.md).
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
Ask the questions one at a time.
If a question can be answered by exploring the codebase, explore the codebase instead.
Rules (preserved + amplified)
1. One question per turn. Never bundle. 2. Provide a recommended answer with each question. Defaulting to "what do you think?" is lazy. 3. Explore the codebase before asking. If grep / Read resolves it, do that first. Saves a turn. 4. Walk the tree depth-first. Finish a branch before opening another. 5. Track dependencies. If decision B depends on decision A, ask A first.
Workflow
1. User provides a plan or design (or path to one). 2. Run scripts/decision_tree_extractor.py to extract branches. 3. Run scripts/question_generator.py to produce the question list with recommendations. 4. Start a session: scripts/grill_session_tracker.py --action start. 5. Walk the tree, one question at a time, recording answers in the session. 6. When all branches resolved: report "shared understanding reached" + the locked-in decisions.
Output Pattern
Per question turn:
Q[i]/[total]: [question]
Recommended answer: [your call + 1-sentence rationale]
(Or: I explored the codebase and found [evidence]. Confirm?)Tooling
See references/companion_tooling.md. Tools: extractor + generator + tracker. Agent: cs-grill-master. Command: /cs:grill-me.
---
Version: 1.0.0 Derived: Matt Pocock (MIT) + this repo's wrapper
Companion Tooling
Interrogation tools + cs-* wrapper layered on top of Matt's grill-me skill.
Validation Tools (stdlib Python)
| Tool | Purpose | Run when |
|---|---|---|
scripts/decision_tree_extractor.py | Scan a plan doc for decision branches (intent / choice / open / tradeoff / dependency / question) | Starting a grill session — see what's there to interrogate |
scripts/question_generator.py | Generate forcing questions from extracted branches with recommended answers + dependency-aware ordering | Producing the question list for a grill session |
scripts/grill_session_tracker.py | JSON-backed session storage in ~/.grill_sessions/ — track answers across turns, resume sessions | Running a multi-turn grill (most real grills) |
All three:
- Stdlib-only
- Run with embedded sample if no input provided
- Output text or JSON (
--output json)
Session Storage
grill_session_tracker.py persists state to ~/.grill_sessions/<name>.json. This enables:
- Resume a grill across days
- Switch between concurrent grills (e.g., per project)
- Audit which decisions were resolved when
- Generate a "decisions locked" summary at end
cs-grill-master Persona Agent
Lives at ../agents/cs-grill-master.md. Voice: relentless, one-question-at-a-time, codebase-exploration-first.
The persona's hard rule: never bundle questions. Even when there are 10 obvious follow-ups, ask one, wait for answer, then ask the next.
/cs:grill-me Slash Command
Lives at ../commands/cs-grill-me.md. Activation pattern:
1. /cs:grill-me <path-to-plan> — start grill session on plan doc 2. Persona asks Q1 with recommended answer 3. User answers 4. Persona asks Q2 5. ...continues until all branches resolved
Why Wrap Matt's Original
Matt's grill-me skill is intentionally minimal (3 sentences). The wrapper adds:
1. Automatic branch extraction — manually identifying decision branches is the slow part; the extractor does it deterministically 2. Question templating — consistent question patterns per branch kind (intent / choice / tradeoff) 3. Session persistence — grills span days; persistence prevents re-asking + losing context 4. Recommendation defaults — every question carries a recommended answer (per Matt's "provide your recommended answer" rule)
Attribution
Original: matt-pocock/skills/skills/productivity/grill-me (MIT).
---
Source authorities (non-exhaustive):
- Matt Pocock — grill-me (https://github.com/mattpocock/skills/, MIT) — the upstream source
- Socratic Method (5th-century BC) — interrogation as truth-finding; one-question-at-a-time discipline
- YC office hours format (Y Combinator) — forcing questions for founders; "what's blocking this?" + "why this and not Y?"
- Cockburn, A. — "Writing Effective Use Cases" (2000) — exploring decision branches in requirements
- Fournier, C. — "The Manager's Path" (2017) — interview discipline for hard decisions
- Larson, W. — "An Elegant Puzzle" (2019) — engineering manager decision-making patterns
- 5 Whys (Toyota Production System) — Sakichi Toyoda — sequential interrogation for root cause
Forcing-Question Patterns for Plan Interrogation
This reference answers exactly one decision: what makes a question "forcing" vs "soft", and how do we ask forcing questions that resolve decisions?
Pair with scripts/question_generator.py for templated forcing questions.
What Makes a Question "Forcing"
A forcing question:
1. Cannot be answered with "yes"/"no" without follow-up 2. Names the alternative — "X or Y" not "is X right?" 3. Demands evidence — "what's the kill criterion?" not "what do you think?" 4. Removes the escape hatch — asks the trade-off explicitly
Soft questions let the answerer evade. Forcing questions don't.
Six Forcing-Question Patterns
Pattern 1: "Why X and not Y?"
When user says "We'll use Postgres" — forcing question: "Why Postgres and not MySQL?"
The forcing element: requires the answerer to articulate the alternative + the rejection reason. Reveals whether the choice was deliberate or default.
Soft variant (bad): "Are you sure about Postgres?"
Pattern 2: "What's the kill criterion?"
When user says "We'll try approach X" — forcing question: "What would convince you X is wrong?"
The forcing element: requires the answerer to commit to falsifiability ahead of time. Prevents motivated reasoning later.
Soft variant (bad): "What if it doesn't work?"
Pattern 3: "What's blocking the decision?"
When user says "TBD" or "open question" — forcing question: "What input is missing, and when does it arrive?"
The forcing element: separates "haven't decided" from "can't decide yet". Most TBDs are decideable now under uncertainty.
Soft variant (bad): "Have you thought about that?"
Pattern 4: "Which side of the trade-off?"
When user says "trade-off between A and B" — forcing question: "Which side are you optimizing for, and what's the deciding constraint?"
The forcing element: requires picking. "Both" is not an option for actual trade-offs.
Soft variant (bad): "Have you considered the trade-offs?"
Pattern 5: "What's the dependency?"
When user says "depends on X" — forcing question: "Is X locked in? If not, that decision comes first."
The forcing element: surfaces dependency chains. Forces depth-first walk of the decision tree.
Soft variant (bad): "Have you thought about dependencies?"
Pattern 6: "Even at 60% confidence — what's your best guess?"
When user hedges — forcing question: "Even uncertain, what would you decide today?"
The forcing element: prevents indefinite deferral. Most decisions can be made under uncertainty + revised later.
Soft variant (bad): "When will you decide?"
The "Recommended Answer" Rule (per Matt)
Every question should carry a recommended answer with rationale. Why:
1. Models the depth of analysis expected — answerer sees what "good" looks like 2. Accelerates the interview — answerer can agree/disagree faster than constructing from scratch 3. Surfaces interrogator bias — if the recommendation is wrong, answerer can correct it explicitly 4. Prevents "what do you think?" loops — both sides commit to a position
Format:
Q: [forcing question]
Recommended: [position] because [1-sentence reason].
One-at-a-Time Discipline (per Matt)
"Ask the questions one at a time."
Why this matters:
1. Bundled questions get partial answers — answerer addresses the easiest one; hard ones get skipped 2. Each answer constrains the next — the second question often changes after hearing the first answer 3. Cognitive load — answerer can focus + give a complete response 4. Visible progress — each Q→A pair locks one decision; bundle masks progress
Anti-pattern: "Here are 8 questions: [list]". This is a survey, not an interrogation.
Codebase Exploration > Speculation (per Matt)
"If a question can be answered by exploring the codebase, explore the codebase instead."
When to explore instead of asking:
| Question | Action |
|---|---|
| "What auth library are we using?" | grep -r "auth" package.json — don't ask |
| "Does X already exist?" | find . -name "X*" — don't ask |
| "What's the current schema?" | Read path/to/migrations/latest.sql — don't ask |
| "Are tests passing?" | Run the test suite — don't ask |
When to ask anyway:
- Intent: "Why this approach?" can't be grepped
- Trade-offs: only the human knows which they value
- Future state: codebase shows current, not desired
Anti-Patterns
1. "Are you sure?" — invites defensive answer; no information value 2. "Have you thought about ...?" — implies "no" is acceptable; doesn't force a decision 3. "What if it fails?" — speculative; better: "what's the kill criterion?" 4. "Could you elaborate?" — passive; better: name the specific gap 5. Yes/no questions without follow-up — wastes the turn 6. Stacking questions — bundles violate one-at-a-time rule
How question_generator.py Implements This
The tool's question templates map each detected branch kind to a forcing-question pattern:
intent→ "Why this approach and not the obvious alternative?" (Pattern 1)choice→ "Which side of the choice, and what's the deciding criterion?" (Pattern 4)open→ "What's blocking this decision?" (Pattern 3)tradeoff→ "Which side of the trade-off are you optimizing for?" (Pattern 4)dependency→ "Is the dependency locked in?" (Pattern 5)question→ "What's your current best answer, even if uncertain?" (Pattern 6)
Each generated question carries a recommended-answer template per Matt's rule.
When This Reference Doesn't Help
- Open-ended exploration — early-stage ideation needs soft questions; grill-me is for plans not yet committed
- Therapeutic/coaching contexts — forcing questions can feel adversarial; tone matters
- Hiring interviews — different mode; behavioral questions follow different patterns
---
Source authorities (non-exhaustive):
- Matt Pocock — grill-me (https://github.com/mattpocock/skills/, MIT) — the one-at-a-time + recommended-answer rules
- Socratic Method (5th-century BC) — Plato's dialogues — sequential questioning toward truth
- Y Combinator office-hour format (Garry Tan + Michael Seibel) — founder interrogation pattern
- Toyota Production System — 5 Whys (Sakichi Toyoda) — sequential causal questioning
- Cockburn, A. — "Writing Effective Use Cases" (2000) — decision-branch enumeration
- Popper, K. — "Conjectures and Refutations" (1963) — falsifiability + kill criteria
- Galef, J. — "The Scout Mindset" (2021) — calibrating beliefs under uncertainty
- Larson, W. — "An Elegant Puzzle" (2019) — eng decision-making in practice
When to Stop Grilling
This reference answers exactly one decision: when is "shared understanding" actually reached, and how do we know to stop the interrogation?
Pair with scripts/grill_session_tracker.py — the session tracker shows progress and surfaces unanswered branches.
Matt Pocock's Stopping Condition (Implicit)
"Interview me relentlessly about every aspect of this plan until we reach a shared understanding."
>
— Matt Pocock, grill-me SKILL.md
"Shared understanding" is the stopping condition. But what does that mean operationally?
Three Conditions That Mean "Stop"
Condition 1: Every decision branch has an answer
Track via grill_session_tracker.py status. When percent_complete = 100%, every detected branch has a recorded answer. Stop grilling.
Risk: The extractor missed branches. Run decision_tree_extractor.py once more after answers are in — sometimes answers reveal new branches.
Condition 2: No new questions arise from the last 3 answers
If the last 3 answers all triggered follow-up questions, grilling continues. If 3 answers in a row resolve cleanly with no new questions, the tree is exhausted.
Pattern: count the rate of new-question generation per turn. When it drops to zero for 3+ turns, stop.
Condition 3: The interrogator can predict the answerer's response
If the interrogator can predict, with high confidence, what the answerer will say to the next question — that question doesn't add information. Skip it or stop entirely.
Test: before asking the next question, write down your guess at the answer. If the guess matches, you don't need to ask. Move on.
Three Conditions That Mean "Keep Going"
Condition A: The answerer is dodging
Signs:
- "We'll figure that out later" (without a date)
- "It depends" (without naming the dependency)
- Answers a different question than was asked
- Hedges every answer with "probably" / "likely" / "maybe"
Action: re-ask the same question with the same words. If dodged twice, name the dodge: "You said 'we'll figure it out later' — what's the latest moment you can decide and still ship?"
Condition B: Answers contradict each other
If Q3 answer contradicts Q1 answer, stop the forward progress and reconcile:
"You said X in Q1 but now Y in Q3. Which is it?"
Reconciliation is a separate grill phase — don't continue forward until resolved.
Condition C: A new branch surfaces
If the answerer says "but if we do X, then we also need to decide Y" — Y is a new branch. Add to the question queue. Don't stop until Y is resolved.
The "Recommended Answer Match" Heuristic
When generating questions with question_generator.py, each question has a recommended answer. Track:
| Answer matches recommendation? | What it means |
|---|---|
| Yes, with same rationale | Strong signal — both interrogator + answerer converged on the same logic |
| Yes, different rationale | Worth probing — same conclusion via different reasoning could mean one is wrong |
| No, with strong rationale | Healthy disagreement — record the rationale; this is the value of the grill |
| No, weak rationale | Push back — "the recommendation was X because Y; your answer rejects Y — why?" |
When 80%+ of answers match the recommendations cleanly, the grill is over-engineered for this plan — stop.
The "Diminishing Returns" Test
Each grill question costs ~1 turn. After 10-15 questions on a single plan, returns diminish:
- First 3-5: high value (catches major missing decisions)
- Questions 6-10: medium value (refines edge cases)
- Questions 11-15: lower value (catches rare edge cases)
- Questions 16+: noise (usually the interrogator over-conditioning)
If a plan has 20+ branches, consider splitting into multiple plans rather than one mega-grill.
When to Stop Even Before Conditions Met
When the user signals fatigue
"Can we move on?" / "Let's just decide and revisit if needed" / "Skip ahead"
Stop. Note unresolved branches in the session for later. Don't push through fatigue — answers under fatigue are often wrong.
When the cost of deciding exceeds the cost of being wrong
For reversible decisions, grilling is overhead. Ship and revisit. For irreversible decisions, grill thoroughly.
Test: "If we're wrong about this, what does it cost to fix?" If the answer is "trivial" or "we just change a flag", stop grilling early.
When the plan is exploratory
If the plan is "let's try X for a week and see" — don't grill the details. Grill the decision criteria for after the week.
The Locking-In Pattern
When the grill ends, the session should produce a "decisions locked" summary:
Session: my-plan
Started: 2026-05-13
Closed: 2026-05-13
Status: Complete (8/8 branches resolved)
Decisions locked:
1. [L4] Schema-per-tenant chosen for cost reasons; isolation risk accepted.
2. [L8] Okta for SSO. Auth0 rejected (less Workday integration).
3. ...The summary becomes the reference document. The grill session is throwaway; the summary is the artifact.
Anti-Patterns
1. Grilling forever — every plan has 100 decideable details; grill stops at "shared understanding", not "complete certainty" 2. Grilling reversible decisions — wasteful; ship + revise 3. Grilling without producing a summary — wastes the answers; lock them in 4. Grilling without exploring codebase first — wastes turns asking questions the code answers 5. Re-grilling the same plan — if the plan was already grilled, don't re-grill the same branches; only grill new branches
When This Reference Doesn't Help
- Live-decision grilling in a meeting — different mode; meetings have time pressure
- Code review — different scope; review is post-decision
- Brainstorming — wrong tool; grilling is for committed plans, not exploration
---
Source authorities (non-exhaustive):
- Matt Pocock — grill-me (https://github.com/mattpocock/skills/, MIT) — the "shared understanding" stopping condition
- Galef, J. — "The Scout Mindset" (2021) — when to stop seeking more evidence
- Kahneman, D. — "Thinking, Fast and Slow" (2011) — decision fatigue + diminishing returns
- Bezos, J. — Type 1 vs Type 2 decisions (Amazon shareholder letter, 2015) — reversible vs irreversible decisions
- YC Founder School — "Decide and move on" — when grilling becomes procrastination
- Larson, W. — "An Elegant Puzzle" (2019) — engineering decision-making sequencing
- Cynefin framework (Snowden) — different decision domains require different evidence thresholds
#!/usr/bin/env python3
"""decision_tree_extractor.py — Extract decision branches from a plan/design doc.
Stdlib-only. Scans a markdown plan and identifies decision branches by detecting:
1. Modal verbs of intent: "we'll", "we will", "we plan to", "we should", "we could"
2. Open questions: sentences ending in "?"
3. Choices: "X or Y" / "either X or Y" / "vs"
4. TBDs: "TBD", "to be decided", "open question"
5. Trade-off markers: "trade-off", "tradeoff", "pros/cons"
Output: numbered list of decision branches with line refs.
NO LLM CALLS. Pure regex + line walking.
Usage:
python decision_tree_extractor.py # uses embedded sample
python decision_tree_extractor.py path/to/plan.md
python decision_tree_extractor.py plan.md --output json
"""
import argparse
import json
import re
import sys
from typing import Any, Dict, List
# Regex patterns that indicate a decision branch
DECISION_PATTERNS = [
(re.compile(r"\bwe\s*(?:'ll|will|plan\s+to|should|could|might|may)\b", re.IGNORECASE),
"intent"),
(re.compile(r"\b(?:either|or)\b.{0,80}\b(?:or|alternatively)\b", re.IGNORECASE),
"choice"),
(re.compile(r"\bversus\b|\bvs\.?\b", re.IGNORECASE),
"choice"),
(re.compile(r"\bTBD\b|\bto\s+be\s+(?:decided|determined)\b", re.IGNORECASE),
"open"),
(re.compile(r"\bopen\s+question\b", re.IGNORECASE),
"open"),
(re.compile(r"\btrade-?offs?\b", re.IGNORECASE),
"tradeoff"),
(re.compile(r"\bdepends?\s+on\b", re.IGNORECASE),
"dependency"),
(re.compile(r"\?\s*$"),
"question"),
]
SAMPLE_PLAN = """# Plan: Multi-tenant SaaS Migration
## Architecture
We'll move to a single-tenant database per customer. Or maybe we should
do schema-per-tenant for cost. This is a trade-off between isolation and ops cost.
## Auth
TBD: SSO provider — Okta or Auth0?
## Migration sequence
We plan to migrate the largest tenant first. Depends on whether their data fits in 24h.
Open question: rollback strategy?
## Data layer
We could use Postgres logical replication, but we might prefer dual-writes.
Trade-off: complexity vs zero-downtime guarantee.
## Cut-over
Final decision TBD on whether to flip DNS at midnight or use feature flags.
"""
def extract_branches(text: str) -> List[Dict[str, Any]]:
branches: List[Dict[str, Any]] = []
seen_lines = set()
for line_no, line in enumerate(text.splitlines(), start=1):
for pattern, kind in DECISION_PATTERNS:
match = pattern.search(line)
if not match:
continue
if line_no in seen_lines:
continue
seen_lines.add(line_no)
branches.append({
"line": line_no,
"kind": kind,
"trigger": match.group(0),
"context": line.strip()[:160],
})
break
return branches
def analyze(text: str) -> Dict[str, Any]:
branches = extract_branches(text)
by_kind: Dict[str, int] = {}
for b in branches:
by_kind[b["kind"]] = by_kind.get(b["kind"], 0) + 1
return {
"total_branches": len(branches),
"by_kind": by_kind,
"branches": branches,
}
def render_text(r: Dict[str, Any]) -> str:
lines = []
lines.append("=" * 72)
lines.append("DECISION TREE EXTRACTOR")
lines.append("=" * 72)
lines.append("")
lines.append(f"Total decision branches found: {r['total_branches']}")
lines.append(f"By kind: {r['by_kind']}")
lines.append("")
lines.append("-" * 72)
for i, b in enumerate(r["branches"], start=1):
lines.append(f" [{i:2d}] L{b['line']:>4d} ({b['kind']:11s}) {b['context']}")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(
description="Extract decision branches from a plan/design document.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("path", nargs="?", help="Path to markdown plan (uses embedded sample if omitted)")
parser.add_argument("--output", choices=("text", "json"), default="text", help="Output format")
args = parser.parse_args()
if args.path:
try:
with open(args.path, "r", encoding="utf-8") as f:
text = f.read()
except (IOError, OSError) as e:
print(f"error: {e}", file=sys.stderr)
return 1
else:
text = SAMPLE_PLAN
result = analyze(text)
if args.output == "json":
print(json.dumps(result, indent=2))
else:
print(render_text(result))
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""grill_session_tracker.py — Track grill-me session state across turns.
Stdlib-only. JSON-backed session storage for the relentless interrogation pattern.
Tracks: questions asked, answers received, recommendations, decisions locked,
remaining branches. Persistence enables resume across sessions.
Storage: ~/.grill_sessions/<session_name>.json
Actions:
- start <session_name>: initialize new session from plan doc
- record <session_name> --question-id N --answer "text": record an answer
- status <session_name>: show progress
- list: list all sessions
- close <session_name>: mark complete + summary
NO LLM CALLS. Stdlib only.
Usage:
python grill_session_tracker.py --action list
python grill_session_tracker.py --action start --session my-plan --plan path/to/plan.md
python grill_session_tracker.py --action record --session my-plan --question-id 1 --answer "we chose X"
python grill_session_tracker.py --action status --session my-plan
python grill_session_tracker.py --action close --session my-plan
"""
import argparse
import json
import os
import sys
from datetime import datetime
from typing import Any, Dict, List
# Import question generator
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
from question_generator import analyze as analyze_plan, SAMPLE_PLAN # noqa: E402
SESSIONS_DIR = os.path.expanduser("~/.grill_sessions")
def _ensure_dir() -> None:
os.makedirs(SESSIONS_DIR, exist_ok=True)
def _session_path(name: str) -> str:
return os.path.join(SESSIONS_DIR, f"{name}.json")
def _load(name: str) -> Dict[str, Any]:
path = _session_path(name)
if not os.path.isfile(path):
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def _save(name: str, data: Dict[str, Any]) -> None:
_ensure_dir()
with open(_session_path(name), "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
def start_session(name: str, plan_path: str) -> Dict[str, Any]:
if plan_path:
with open(plan_path, "r", encoding="utf-8") as f:
plan_text = f.read()
else:
plan_text = SAMPLE_PLAN
plan_path = "<embedded sample>"
plan_analysis = analyze_plan(plan_text)
session = {
"name": name,
"started_at": datetime.now().isoformat(timespec="seconds"),
"plan_source": plan_path,
"total_questions": plan_analysis["total_questions"],
"questions": plan_analysis["questions"],
"answers": {}, # question_n -> {"answer": str, "recorded_at": iso}
"status": "active",
}
_save(name, session)
return session
def record_answer(name: str, qid: int, answer: str) -> Dict[str, Any]:
session = _load(name)
if not session:
raise ValueError(f"Session not found: {name}")
session["answers"][str(qid)] = {
"answer": answer,
"recorded_at": datetime.now().isoformat(timespec="seconds"),
}
_save(name, session)
return session
def session_status(name: str) -> Dict[str, Any]:
session = _load(name)
if not session:
return {"error": f"Session not found: {name}"}
answered = len(session.get("answers", {}))
total = session.get("total_questions", 0)
pct = round(100.0 * answered / max(total, 1), 1)
next_q = None
for q in session.get("questions", []):
if str(q["n"]) not in session.get("answers", {}):
next_q = q
break
return {
"name": session["name"],
"status": session.get("status", "active"),
"answered": answered,
"total": total,
"percent_complete": pct,
"next_question": next_q,
"all_answers": session.get("answers", {}),
}
def list_sessions() -> List[str]:
_ensure_dir()
return sorted(
os.path.splitext(f)[0]
for f in os.listdir(SESSIONS_DIR)
if f.endswith(".json")
)
def close_session(name: str) -> Dict[str, Any]:
session = _load(name)
if not session:
raise ValueError(f"Session not found: {name}")
session["status"] = "closed"
session["closed_at"] = datetime.now().isoformat(timespec="seconds")
_save(name, session)
return session
def render_status(r: Dict[str, Any]) -> str:
if "error" in r:
return f"ERROR: {r['error']}"
lines = []
lines.append("=" * 72)
lines.append(f"GRILL SESSION: {r['name']}")
lines.append("=" * 72)
lines.append(f"Status: {r['status']} ({r['answered']} / {r['total']} answered, {r['percent_complete']}%)")
lines.append("")
if r["next_question"]:
q = r["next_question"]
lines.append(f"Next question (Q{q['n']}):")
lines.append(f" {q['question']}")
lines.append(f" Recommended: {q['recommended']}")
else:
lines.append("All questions answered. Run --action close to mark session complete.")
lines.append("")
if r["all_answers"]:
lines.append("Answered:")
for qid, ans in sorted(r["all_answers"].items(), key=lambda x: int(x[0])):
lines.append(f" Q{qid}: {ans['answer'][:100]}")
return "\n".join(lines)
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Track grill-me session state across turns.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
action_choices = ("start", "record", "status", "list", "close")
parser.add_argument("--action", default="status", choices=action_choices, help="Session action")
parser.add_argument("--session", help="Session name")
parser.add_argument("--plan", default="", help="Path to plan markdown (start action)")
parser.add_argument("--question-id", type=int, help="Question number to record")
parser.add_argument("--answer", help="Answer text (record action)")
parser.add_argument("--output", choices=("text", "json"), default="text", help="Output format")
return parser
def _print_session_list(sessions: List[str], json_output: bool) -> None:
if json_output:
print(json.dumps({"sessions": sessions}, indent=2))
return
print("Sessions:")
items = sessions or ["(none)"]
for s in items:
print(f" - {s}")
def _print_start_summary(session: Dict[str, Any]) -> None:
print(f"Started session: {session['name']}")
print(f" Plan: {session['plan_source']}")
print(f" Total questions: {session['total_questions']}")
questions = session.get("questions") or []
first = questions[0]["question"] if questions else "(none)"
print(f" First question: {first}")
def _action_list(args: argparse.Namespace) -> int:
_print_session_list(list_sessions(), args.output == "json")
return 0
def _action_start(args: argparse.Namespace) -> int:
name = args.session or "sample-session"
session = start_session(name, args.plan)
if args.output == "json":
print(json.dumps(session, indent=2))
else:
_print_start_summary(session)
return 0
def _action_record(args: argparse.Namespace) -> int:
if not args.session or args.question_id is None or not args.answer:
print("error: record requires --session, --question-id, --answer", file=sys.stderr)
return 1
record_answer(args.session, args.question_id, args.answer)
result = session_status(args.session)
output = json.dumps(result, indent=2) if args.output == "json" else render_status(result)
print(output)
return 0
def _action_status(args: argparse.Namespace) -> int:
name = args.session or "sample-session"
result = session_status(name)
output = json.dumps(result, indent=2) if args.output == "json" else render_status(result)
print(output)
return 0
def _action_close(args: argparse.Namespace) -> int:
if not args.session:
print("error: close requires --session", file=sys.stderr)
return 1
session = close_session(args.session)
if args.output == "json":
print(json.dumps(session, indent=2))
else:
print(f"Closed session: {args.session}")
return 0
ACTION_DISPATCH = {
"list": _action_list,
"start": _action_start,
"record": _action_record,
"status": _action_status,
"close": _action_close,
}
def main() -> int:
args = _build_parser().parse_args()
handler = ACTION_DISPATCH.get(args.action)
if handler is None:
return 0
return handler(args)
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""question_generator.py — Generate forcing questions from extracted decision branches.
Stdlib-only. Takes a plan doc, runs decision_tree_extractor, then generates
forcing questions per Matt Pocock's grill-me discipline:
- Each question maps to one decision branch
- Each question proposes a recommended answer
- Questions ordered by dependency (independent first, dependent last)
- One question per turn (output is a list, not a paragraph)
Template per question:
Q: [forcing question]
Recommended: [recommendation with 1-sentence rationale]
Question templates by branch kind:
- intent -> "You said you'll X. Why X and not Y?"
- choice -> "Between X and Y, which one and why?"
- open -> "X is marked TBD. What's blocking the decision?"
- tradeoff -> "Trade-off between A and B. Which side are you optimizing for?"
- dependency -> "X depends on Y. Is Y locked in? If not, ask about Y first."
- question -> "[original question] — what's your current answer?"
Usage:
python question_generator.py # uses embedded sample
python question_generator.py path/to/plan.md
python question_generator.py plan.md --output json
"""
import argparse
import json
import sys
import os
from typing import Any, Dict, List
# Import extractor as a module
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
from decision_tree_extractor import extract_branches, SAMPLE_PLAN # noqa: E402
QUESTION_TEMPLATES = {
"intent": "Why this approach and not the obvious alternative?",
"choice": "Which side of the choice, and what's the deciding criterion?",
"open": "What's blocking this decision? What would unblock it today?",
"tradeoff": "Which side of the trade-off are you optimizing for, and what's the kill criterion?",
"dependency": "Is the dependency locked in? If not, that decision comes first.",
"question": "What's your current best answer, even if uncertain?",
}
RECOMMENDED_TEMPLATES = {
"intent": "State the alternative explicitly + 1 sentence why you rejected it.",
"choice": "Pick the option that aligns with the constraint you can't change (budget, deadline, team).",
"open": "Name the missing input. Estimate when it arrives. Decide now under uncertainty if it won't arrive in time.",
"tradeoff": "Choose the side that's reversible later. Trade-offs are usually one-way; pick the one with the escape hatch.",
"dependency": "Resolve the upstream decision first. Then re-evaluate this one.",
"question": "Even a 60%-confidence answer is better than 'we'll figure it out later'.",
}
def _detect_dependencies(branches: List[Dict[str, Any]]) -> List[int]:
"""Reorder: dependency branches go AFTER what they depend on (best-effort)."""
dep_indices = [i for i, b in enumerate(branches) if b["kind"] == "dependency"]
non_dep_indices = [i for i, b in enumerate(branches) if b["kind"] != "dependency"]
return non_dep_indices + dep_indices
def generate_questions(branches: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
ordered = _detect_dependencies(branches)
questions: List[Dict[str, Any]] = []
for n, idx in enumerate(ordered, start=1):
b = branches[idx]
q_template = QUESTION_TEMPLATES.get(b["kind"], "What's the current state?")
r_template = RECOMMENDED_TEMPLATES.get(b["kind"], "State your best answer.")
questions.append({
"n": n,
"line": b["line"],
"branch_kind": b["kind"],
"context": b["context"],
"question": f"L{b['line']}: {b['context']} -> {q_template}",
"recommended": r_template,
})
return questions
def analyze(text: str) -> Dict[str, Any]:
branches = extract_branches(text)
questions = generate_questions(branches)
return {
"total_questions": len(questions),
"branch_kinds": sorted(set(b["kind"] for b in branches)),
"questions": questions,
}
def render_text(r: Dict[str, Any]) -> str:
lines = []
lines.append("=" * 72)
lines.append("FORCING QUESTION GENERATOR (one at a time, per Matt's grill-me)")
lines.append("=" * 72)
lines.append("")
lines.append(f"Total questions: {r['total_questions']}")
lines.append(f"Branch kinds: {r['branch_kinds']}")
lines.append("")
lines.append("-" * 72)
for q in r["questions"]:
lines.append(f" Q{q['n']:>2d}: {q['question']}")
lines.append(f" Recommended: {q['recommended']}")
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(
description="Generate forcing questions from a plan/design document.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("path", nargs="?", help="Path to markdown plan (uses embedded sample if omitted)")
parser.add_argument("--output", choices=("text", "json"), default="text", help="Output format")
args = parser.parse_args()
if args.path:
try:
with open(args.path, "r", encoding="utf-8") as f:
text = f.read()
except (IOError, OSError) as e:
print(f"error: {e}", file=sys.stderr)
return 1
else:
text = SAMPLE_PLAN
result = analyze(text)
if args.output == "json":
print(json.dumps(result, indent=2))
else:
print(render_text(result))
return 0
if __name__ == "__main__":
sys.exit(main())
Related skills
How it compares
Pick grill-me over generic planning skills when you need scripted decision-tree extraction and resumable interrogation sessions rather than open-ended brainstorming.
FAQ
What Python scripts ship with grill-me?
grill-me bundles three stdlib Python tools: decision_tree_extractor.py for scanning plan branches, question_generator.py for forcing questions, and grill_session_tracker.py for JSON session storage in ~/.grill_sessions/.
What decision branch types does grill-me extract?
grill-me's decision_tree_extractor.py tags branches as intent, choice, open, tradeoff, dependency, or question, giving reviewers a structured map of what to interrogate.
Is Grill Me safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.