Skip to content

Commit 7da1664

Browse files
authored
Merge pull request #544 from PlanExeOrg/feature/prompt-adherence
Add prompt adherence check to pipeline
2 parents efc22c2 + 0cbb55d commit 7da1664

15 files changed

Lines changed: 1415 additions & 22 deletions

File tree

docs/superpowers/plans/2026-04-09-prompt-adherence.md

Lines changed: 683 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Prompt Adherence Check for PlanExe
2+
3+
## Problem
4+
5+
PlanExe's pipeline has a "normalization bias." Each of the ~70 nodes nudges the plan toward what a reasonable project *should* look like, and the cumulative drift over the full pipeline is significant. The user's stated reality gets overridden by the LLM's priors about what's plausible.
6+
7+
This manifests as:
8+
- **Stated facts ignored.** The user says "the East Wing has already been demolished" but the plan includes demolition permitting steps.
9+
- **Requirements softened.** The user says "100% renewable energy" and the plan targets 60-80%.
10+
- **Intent diluted.** The user's tone is "this is happening, execute it" but the plan spends 40% on feasibility studies.
11+
- **Unsolicited caveats.** The plan adds qualifications, risk disclaimers, and scope reductions the user didn't ask for.
12+
- **Generic PM filler.** The plan relies on boilerplate project management language instead of addressing the specific problem.
13+
14+
Existing pipeline steps (Premise Attack, Premortem, Expert Criticism, Self Audit) assess plan *quality* — whether the plan is internally consistent, well-structured, and risk-aware. None of them check whether the plan actually does what the user asked.
15+
16+
## Goal
17+
18+
A pipeline step that checks the final plan against the original user prompt and produces a scored report showing which user directives were honored, softened, or ignored. The user can scan the report and immediately see the degree of prompt drift.
19+
20+
## Architecture
21+
22+
Two-phase LLM approach: extract directives from the prompt, then score each one against the final plan.
23+
24+
### Phase 1 — Extract Directives
25+
26+
Read `plan.txt` (the original user prompt) and extract a structured list of directives. Each directive is one thing the user stated or implied that the plan must respect.
27+
28+
```python
29+
class DirectiveType(str, Enum):
30+
CONSTRAINT = "constraint" # "Budget: DKK 500M", "Timeline: 12 months"
31+
STATED_FACT = "stated_fact" # "The East Wing has already been demolished"
32+
REQUIREMENT = "requirement" # "Build a casino", "Reeducate teachers"
33+
BANNED = "banned" # "Banned words: blockchain/NFT"
34+
INTENT = "intent" # "I'm not targeting revenue", tone/posture signals
35+
```
36+
37+
Each directive has:
38+
- `directive_id`: "D1", "D2", etc.
39+
- `directive_type`: one of the types above
40+
- `text`: the user's words (short quote or paraphrase)
41+
- `importance_5`: 1 (minor detail) to 5 (core requirement)
42+
43+
The LLM is instructed to extract 5-15 directives, prioritizing things that are easy to dilute: stated facts about the world, hard numbers, explicit scope boundaries, banned words, and the user's posture (execute vs. study).
44+
45+
### Phase 2 — Score Against Final Plan
46+
47+
Read the extracted directives plus the final plan artifacts (executive summary, project plan, consolidated assumptions). For each directive, score adherence.
48+
49+
```python
50+
class AdherenceCategory(str, Enum):
51+
FULLY_HONORED = "fully_honored"
52+
PARTIALLY_HONORED = "partially_honored"
53+
SOFTENED = "softened" # requirement weakened
54+
IGNORED = "ignored" # not addressed at all
55+
CONTRADICTED = "contradicted" # plan says the opposite
56+
UNSOLICITED_CAVEAT = "unsolicited_caveat" # plan adds qualifications user didn't ask for
57+
```
58+
59+
Each scoring result has:
60+
- `directive_id`: references a Phase 1 directive
61+
- `adherence_5`: 1 (ignored/contradicted) to 5 (fully honored)
62+
- `category`: one of the categories above
63+
- `evidence`: direct quote from the plan (under 200 chars)
64+
- `explanation`: how the plan handled this directive and why the score was given
65+
66+
### Output Files
67+
68+
- `prompt_adherence_raw.json` — full structured data (directives + scores + metadata)
69+
- `prompt_adherence.md` — human-readable report
70+
71+
### Markdown Report Structure
72+
73+
1. **Summary table** — all directives sorted by severity (importance_5 x (6 - adherence_5), worst offenders first):
74+
75+
```
76+
| ID | Directive | Type | Importance | Adherence | Category |
77+
|----|-----------|------|------------|-----------|----------|
78+
| D3 | "East Wing already demolished" | stated_fact | 5/5 | 1/5 | contradicted |
79+
| D1 | "Budget: DKK 500M" | constraint | 5/5 | 3/5 | softened |
80+
| D7 | "No feasibility studies" | intent | 4/5 | 2/5 | ignored |
81+
```
82+
83+
2. **Overall adherence score** — weighted average: `sum(adherence_5 * importance_5) / sum(5 * importance_5)` as a percentage. A plan that fully honors everything scores 100%.
84+
85+
3. **Detail section** — for each directive scoring adherence_5 ≤ 3, the full explanation and evidence quotes from both the prompt and the plan.
86+
87+
### Pipeline Placement
88+
89+
After `self_audit`, before `report`. The task reads:
90+
- `setup` — plan.txt (the original user prompt)
91+
- `executive_summary` — the final plan summary
92+
- `project_plan` — the detailed plan
93+
- `consolidate_assumptions_markdown` — accumulated assumptions that may have drifted
94+
95+
The report task includes `prompt_adherence.md` in the final HTML output.
96+
97+
### FilenameEnum Entries
98+
99+
```python
100+
PROMPT_ADHERENCE_RAW = "prompt_adherence_raw.json"
101+
PROMPT_ADHERENCE_MARKDOWN = "prompt_adherence.md"
102+
```
103+
104+
### Code Structure
105+
106+
```
107+
worker_plan/worker_plan_internal/
108+
diagnostics/
109+
prompt_adherence.py — Phase 1 + Phase 2 logic, Pydantic models, markdown generation
110+
plan/nodes/
111+
prompt_adherence.py — Luigi task (PromptAdherenceTask)
112+
```
113+
114+
Follows the same pattern as `premortem.py` / `nodes/premortem.py`:
115+
- Business logic in `diagnostics/prompt_adherence.py`
116+
- Luigi wiring in `plan/nodes/prompt_adherence.py`
117+
- Pydantic structured output via `llm.as_structured_llm()`
118+
- `LLMExecutor` for model fallback and retry
119+
120+
### Scope Boundaries
121+
122+
**In scope:**
123+
- Extract directives from plan.txt
124+
- Score each directive against the final plan
125+
- Produce JSON + markdown report
126+
- Integrate as a Luigi pipeline step
127+
- Include in the final HTML report
128+
129+
**Out of scope:**
130+
- Fixing the drift (this step surfaces it, doesn't correct it)
131+
- Tracing where in the pipeline drift was introduced (that's RCA's job)
132+
- Judging plan quality (that's self_audit's job)
133+
- Comparing multiple plans against each other

worker_plan/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def create_run_directory(request: StartRunRequest) -> tuple[str, Path]:
223223
start_time_file.save(run_dir / FilenameEnum.START_TIME.value)
224224

225225
plan_file = PlanFile.create(vague_plan_description=request.plan_prompt, start_time=start_time)
226-
plan_file.save(run_dir / FilenameEnum.INITIAL_PLAN.value)
226+
plan_file.save(run_dir / FilenameEnum.INITIAL_PLAN_RAW.value)
227227

228228
return run_id, run_dir.resolve()
229229

worker_plan/worker_plan_api/filenames.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
class FilenameEnum(str, Enum):
44
START_TIME = "start_time.json"
5+
INITIAL_PLAN_RAW = "plan_raw.json"
56
INITIAL_PLAN = "plan.txt"
67
PLANEXE_METADATA = "planexe_metadata.json"
78
SCREEN_PLANNING_PROMPT_RAW = "screen_planning_prompt.json"
@@ -128,6 +129,8 @@ class FilenameEnum(str, Enum):
128129
PREMORTEM_MARKDOWN = "premortem.md"
129130
SELF_AUDIT_RAW = "self_audit_raw.json"
130131
SELF_AUDIT_MARKDOWN = "self_audit.md"
132+
PROMPT_ADHERENCE_RAW = "prompt_adherence_raw.json"
133+
PROMPT_ADHERENCE_MARKDOWN = "prompt_adherence.md"
131134
REPORT = "report.html"
132135
PIPELINE_COMPLETE = "pipeline_complete.txt"
133136

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,50 @@
11
"""
22
PROMPT> python -m worker_plan_api.plan_file
33
"""
4+
import json
45
from datetime import datetime
56
from dataclasses import dataclass
67

8+
9+
PLAN_TEMPLATE = "Plan:\n{plan_prompt}\n\nToday's date:\n{pretty_date}\n\nProject start ASAP"
10+
11+
712
@dataclass
813
class PlanFile:
9-
content: str
14+
plan_prompt: str
15+
pretty_date: str
1016

1117
@classmethod
1218
def create(cls, vague_plan_description: str, start_time: datetime) -> "PlanFile":
1319
pretty_date = start_time.strftime("%Y-%b-%d")
14-
plan_prompt = (
15-
f"Plan:\n{vague_plan_description}\n\n"
16-
f"Today's date:\n{pretty_date}\n\n"
17-
"Project start ASAP"
18-
)
19-
return cls(plan_prompt)
20+
return cls(plan_prompt=vague_plan_description, pretty_date=pretty_date)
21+
22+
def to_dict(self) -> dict:
23+
return {
24+
"plan_prompt": self.plan_prompt,
25+
"pretty_date": self.pretty_date,
26+
}
27+
28+
@classmethod
29+
def from_dict(cls, data: dict) -> "PlanFile":
30+
return cls(plan_prompt=data["plan_prompt"], pretty_date=data["pretty_date"])
31+
32+
@classmethod
33+
def load(cls, file_path: str) -> "PlanFile":
34+
with open(file_path, "r", encoding="utf-8") as f:
35+
return cls.from_dict(json.load(f))
2036

2137
def save(self, file_path: str) -> None:
2238
with open(file_path, "w", encoding="utf-8") as f:
23-
f.write(self.content)
39+
json.dump(self.to_dict(), f, indent=2)
40+
41+
def to_plan_text(self) -> str:
42+
return PLAN_TEMPLATE.format(plan_prompt=self.plan_prompt, pretty_date=self.pretty_date)
43+
2444

2545
if __name__ == "__main__":
2646
start_time: datetime = datetime.now().astimezone()
2747
plan = PlanFile.create(vague_plan_description="My plan is here!", start_time=start_time)
28-
print(plan.content)
48+
print(json.dumps(plan.to_dict(), indent=2))
49+
print("---")
50+
print(plan.to_plan_text())

0 commit comments

Comments
 (0)