Pre-mortem prompt for a risk register, checked against the plan
Have the AI assume the project failed and cite a plan line for every risk, then check quotes and owners with a script and spot cells that shift between runs.
A plan can say "the security review usually takes six weeks" and "migrate in Jan-Feb" a few lines apart, and nobody in the kickoff meeting puts the two side by side. Hand the pre-mortem to an AI, then check that every risk it lists really comes from the plan.
[1] Number every line of the plan (P1, P2, …). The numbers are what the check matches on. The example is a fictional plan, 11 lines, for replacing a company learning platform.
P2 Phase 1 (Nov-Dec 2026): vendor selection and contract. Led by the HR Learning team.P3 Phase 2 (Jan-Feb 2027): migrate five years of course records and 420 course videos. IT Operations owns the migration.P4 Phase 3 (Mar 2027): first run with 120 new hires, then expand to all 1,800 employees from April.…P7 Constraint 1: Information Security's cloud security review usually takes six weeks from the request.P9 Constraint 3: in January, IT Operations loses half its available staff to year-end close support for the finance system.…[2] Put a one-sentence role in the system prompt and everything else in the message. The <context> block says why the register is wanted; Anthropic's prompting best practices note that explaining the reason behind an instruction gets a more targeted answer. The line "a generic list already exists" is there to stop the textbook list.
(system) You are the risk manager for internal IT projects. Your job is to find the gaps in a plan, not to add optimism. <context>The project lead will use this as the first draft of a risk register before the kickoff meeting. In team meetings everyone only says it will go fine, so the goal is to see the risks that come from the schedule, staffing and constraints actually written in the plan. A generic list of project risks already exists; don't repeat it.</context> <instructions>It is {date the failure is confirmed} and this project has failed. Using only <plan>, work backwards from the failure and write the causes as a risk register.- One Markdown table. Columns: ID | Cause of failure | Evidence | Early warning sign | Owner | Likelihood- Evidence: the plan line number and that line's wording copied exactly. Example: P7 "usually takes six weeks from the request"- Early warning sign: when, and what you would see, that shows this cause is under way.- Owner: only a team named in the plan. If none fits, write "TBD".- Likelihood: high, medium or low, plus one phrase from the plan that supports it. If the plan gives no basis, write "cannot judge".- Risks whose evidence is not in the plan do not go in the table. List them under "Outside the plan", one line each, at most three.- Do not split one cause across two rows. Write nothing except the table and "Outside the plan".</instructions> <plan>{numbered plan}</plan>[3] Run it twice. In a chat window, paste the system sentence at the top of the message instead.
$ claude -p --model claude-sonnet-5-5 --max-turns 1 --tools "" --setting-sources "" --strict-mcp-config --system-prompt "$(cat role.en.txt)" < prompt.en.txt > out.en.1.txtBoth runs returned eight risks. One row from the first:
| R2 | IT Operations is under-staffed exactly when the migration runs. The 420 videos and five years of records are moved in Jan-Feb while half its available staff is on finance year-end support. | P3 "IT Operations owns the migration."; P9 "in January, IT Operations loses half its available staff to year-end close support for the finance system." | In the first week of Jan 2027 the roster shows under half the migration team free. By mid-Jan no trial batch of records or videos has been loaded. | IT Operations | High: "loses half its available staff" |Both runs caught the clash between the six-week review (P7) and the Phase 1 dates (P2), and between the January staff cut (P9) and the migration (P3).
[4] Check the table with a script: is each quote really on the cited line, is the owner a team from the plan, and do several risks lean on the same line (SHARED).
#!/usr/bin/env python3"""usage: check_risks.py plan.txt out.txt "owner1,owner2,..." """import re, sys, collectionsplan = {l.split(" ", 1)[0]: l.split(" ", 1)[1] for l in open(sys.argv[1], encoding="utf-8") if l.strip()}owners = {o.casefold() for o in sys.argv[3].split(",")} | {"미정", "tbd"}rows = [[c.strip() for c in l.strip().strip("|").split("|")] for l in open(sys.argv[2], encoding="utf-8") if l.startswith("|")][2:]bad, used = 0, collections.defaultdict(list)for r in rows: rid, cite, signal, owner = r[0], r[2], r[3], r[4] for pid, quote in re.findall(r"(P\d+)\s*[「\"“]([^」\"”]+)[」\"”]", cite): used[pid].append(rid) if quote not in plan.get(pid, ""): bad += 1; print(f"QUOTE {rid}: {pid} 「{quote}」 not in plan line") if owner.casefold() not in owners: bad += 1; print(f"OWNER {rid}: {owner}")for pid, ids in sorted(used.items(), key=lambda x: int(x[0][1:])): if len(set(ids)) > 1: print(f"SHARED {pid}: {' '.join(sorted(set(ids)))}")print(f"{len(rows)} risks, {bad} problems")sys.exit(1 if bad else 0)$ O="HR Learning,IT Operations,Information Security,division training coordinators"$ python3 -I check_risks.py plan.en.txt out.en.1.txt "$O"; echo "exit $?"SHARED P3: R2 R4SHARED P5: R7 R88 risks, 0 problemsexit 0The second run also passed. casefold() is there for a reason: both runs wrote the owner as "Division training coordinators" with a capital D, and an exact match flagged it as an unknown team. In the Korean runs, one table added "3월:" in front of a quote that the plan line doesn't contain, and the script caught it. SHARED is not an error; it marks rows to check for one cause split in two.
[5] Compare the two runs by eye; the script only checks wording. The likelihood column moved the most: the first run wrote "cannot judge" for five of eight risks, the second for two. SSO readiness went from "cannot judge" to "Medium", and so did the April expansion. The second run's "Outside the plan" also listed "low employee adoption or poor usability", the generic kind of risk the context asked it to skip. Leave "cannot judge" cells empty in the register rather than inventing a score.