# Prompt Auditor — the seven, scored, and the cut proved

The reader has been told to "read your prompt back and see what you can delete." Nobody can. You
cannot see the padding in your own writing — that is what padding *is*. And the sentence you are
proudest of is exactly the one the model skimmed past.

**You are going to prove which sentences did nothing, mechanically, by deleting them and re-running.**

Most bad answers are caused by something the reader **added**, not something they left out. So this
skill never suggests a longer prompt. It only ever subtracts, and every cut is defended with evidence.

## The seven — these are fixed. Do not invent an eighth.

| # | Name | The tell | The subtraction |
|---|---|---|---|
| 1 | **The Pleaser** | Long, extremely polite. The verb is buried in throat-clearing; the answer comes back soft and non-committal. | Rewrite as one sentence starting with a verb. |
| 2 | **The Novelist** | Paragraphs of project history. The real question is 200 words deep, competing with facts that don't change the answer. | Failure · what you ruled out · the question. |
| 3 | **The Micromanager** | Every step spelled out. You get exactly what you described — including the flaw in your plan. | State the goal and the rules, not the route. |
| 4 | **The Optimist** | "Fix the login bug." It cannot see what you see, so it guesses, confidently. | What you did · what you expected · what happened. |
| 5 | **The Copy-Paster** | A borrowed template with your task bolted on the end. Only the last line was doing work. | Delete the template, keep the sentence, add what to look **for**. |
| 6 | **The Threatener** | "CRITICAL: do NOT make mistakes." You asked for anxiety and got anxiety: disclaimers and belt-and-braces. | Turn the pressure into a requirement it can follow. |
| 7 | **The Role-Player** | An elaborate persona. Changes how the answer *sounds*, and what it will admit to. | Just ask. Keep a persona only if it knows or cares about something. |

Two of these are load-bearing in opposite directions, and confusing them is the classic misdiagnosis:
**The Optimist left something out. Every other one put something in.** Only the Optimist is ever fixed
by adding — and what it adds is three facts, not three paragraphs.

## Step 0 — Get the prompt. Prefer the ones that actually failed.

Three sources, in descending order of value:

1. **The reader's own session transcripts.** The best material, because these prompts have *already
   underperformed in real life* and the transcript records how. This is the default. Go to Step 1.
2. A prompt in a file (`prompts/`, a `.md`, a template they reuse). Read it and go to Step 2.
3. A block they paste into the chat. Go to Step 2.

## Step 1 — Mine the prompts that underperformed

Claude Code writes one JSONL file per session under
`~/.claude/projects/<escaped-project-path>/<session-uuid>.jsonl`, where the directory name is the
project's absolute cwd with **every non-alphanumeric character replaced by `-`** (`C:\new\University`
→ `C--new-University`).

**The schema, and how to tell a human turn from a tool result, is already documented and verified in
`vc-claude-md-harvester` — reuse it, do not re-derive it.** The one thing you must carry across, because
getting it wrong makes this skill worthless: in a real 1,752-line session, 360 lines were
`type: "user"` and **317 of them were tool results, not the human**. A naive `grep '"type":"user"'`
returns ~88% noise. The filter that isolates an actual person typing:

```
o.type === 'user'
  && o.toolUseResult === undefined   // not a tool result
  && o.origin?.kind === 'human'      // not a task-notification
  && !o.isMeta && !o.isSidechain
  && !text.startsWith('<command-name>')   // not a slash command
```

`node` is the only tool you can count on — it ships with Claude Code. `rg` and `jq` are frequently
absent. Check first:

```bash
node --version
command -v rg jq || echo "no rg/jq — node does the mining (this is the normal case)"
```

Write `.claude/scratch/prompts.mjs` with the Write tool — do **not** inline this as a shell one-liner;
the regexes get eaten by heredoc and PowerShell quoting:

```js
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';

const dir = join(homedir(), '.claude', 'projects', process.cwd().replace(/[^a-zA-Z0-9]/g, '-'));
if (!existsSync(dir)) { console.error('no transcripts at', dir); process.exit(1); }

const turns = [];
for (const f of readdirSync(dir).filter((f) => f.endsWith('.jsonl'))) {
  let pending = null;                       // the human turn awaiting its reply
  for (const line of readFileSync(join(dir, f), 'utf8').split('\n')) {
    if (!line) continue;
    let o; try { o = JSON.parse(line); } catch { continue; }

    if (o.type === 'assistant' && pending) {
      const blocks = o.message?.content || [];
      const said = blocks.filter((b) => b.type === 'text').map((b) => b.text).join('\n');
      pending.reply_chars += said.length;
      // The Optimist's fingerprint: the model has to ASK before it can answer.
      if (/\?\s*$/m.test(said.trim()) && said.length < 900) pending.asked_back = true;
      continue;
    }
    if (o.type !== 'user') continue;
    if (o.toolUseResult !== undefined) continue;   // TOOL RESULT — not a human
    if (o.isMeta || o.isSidechain) continue;
    if (o.origin?.kind !== 'human') continue;

    const c = o.message?.content;
    const text = typeof c === 'string' ? c
      : Array.isArray(c) ? c.filter((b) => b.type === 'text').map((b) => b.text).join('\n') : '';
    if (!text || text.startsWith('<command-name>')) continue;

    if (pending) turns.push(pending);
    pending = {
      session: f.slice(0, 8), ts: o.timestamp,
      chars: text.length, words: text.split(/\s+/).filter(Boolean).length,
      reply_chars: 0, asked_back: false, text,
    };
  }
  if (pending) turns.push(pending);
}

// UNDERPERFORMANCE SIGNALS — mechanical, from the lesson's own yellow flags.
const NOISE = /^(ok|yes|no|thanks|go|continue|do it|y|n)\b/i;
for (let i = 0; i < turns.length; i++) {
  const t = turns[i], next = turns[i + 1];
  t.signals = [];
  // "Your request is longer than the answer you expect back."
  if (t.reply_chars && t.chars > t.reply_chars) t.signals.push('longer-than-its-answer');
  // "It asks you a clarifying question — you left something out."
  if (t.asked_back) t.signals.push('model-asked-back');
  // "You've rephrased the same ask twice and the answer barely moved."
  if (next && !NOISE.test(next.text) && next.words > 8 && similar(t.text, next.text) > 0.4)
    t.signals.push('re-asked-immediately');
}

function similar(a, b) {                    // crude bag-of-words overlap; good enough to rank
  const A = new Set(a.toLowerCase().match(/[a-z]{4,}/g) || []);
  const B = new Set(b.toLowerCase().match(/[a-z]{4,}/g) || []);
  if (!A.size || !B.size) return 0;
  let hit = 0; for (const w of A) if (B.has(w)) hit++;
  return hit / Math.min(A.size, B.size);
}

const ranked = turns.filter((t) => t.signals.length && t.words >= 12)
  .sort((a, b) => b.signals.length - a.signals.length || b.words - a.words);
console.log(JSON.stringify(ranked.slice(0, 20), null, 2));
```

Run it and read the result:

```bash
mkdir -p .claude/scratch
node .claude/scratch/prompts.mjs > .claude/scratch/prompts.json
node -e "const r=require('./.claude/scratch/prompts.json');
console.log(r.length,'prompts with an underperformance signal');
const t={}; for(const x of r) for(const s of x.signals) t[s]=(t[s]||0)+1; console.log(t);"
```

Take the **top three**. A prompt carrying two or three signals at once is the one worth the reader's
attention — and `re-asked-immediately` is the strongest of them, because it is the reader's own past
self telling you the answer was wrong.

If the mine comes back empty, the reader's prompts are short and are working. Say so, and audit the
file or the pasted block instead. **Do not manufacture findings.**

## Step 2 — Score against the seven

Run the detectors first. They are cheap, they are evidence, and they stop you from pattern-matching
your own opinions onto the prompt.

```bash
rg -n --no-heading -i 'please|kindly|if you could|would you mind|i.d (really )?appreciate|sorry to|when you get a chance' .claude/scratch/target.txt
rg -n --no-heading -i 'CRITICAL|URGENT|do NOT|DON.T MESS|this will break|very important|whatever you do|be careful|extremely important' .claude/scratch/target.txt
rg -n --no-heading -i 'you are (a|an|the) |act as|pretend you|world.class|expert|senior|10x|never makes? (a )?mistake|helpful assistant|be thorough' .claude/scratch/target.txt
rg -n --no-heading -i '^\s*(step )?[0-9]+[.)]|first,|then,|after that|next,|finally,' .claude/scratch/target.txt
```

Then fill the card. **A pattern is only "committed" if you can quote the line that commits it.** No
quote, no finding — a scored prompt with no evidence is astrology.

| Pattern | Mechanical evidence | Judgment you still have to make |
|---|---|---|
| Pleaser | politeness hits; first sentence does not start with a verb | Does the ask survive without them? It always does. |
| Novelist | prompt words ≫ facts the answer depends on; `longer-than-its-answer` | **Which** sentences change the answer. Step 3 settles this. |
| Micromanager | numbered/sequenced imperative steps | Is the *how* load-bearing (matching existing code, a rule they must follow) or is it just their guess at a route? |
| Optimist | `model-asked-back`; no did/expected/got triple | This is the only pattern fixed by ADDING. Three facts, not three paragraphs. |
| Copy-Paster | persona/boilerplate hits that name no part of *this* task | Would this line make sense in someone else's prompt? Then it is not doing work here. |
| Threatener | pressure hits | Is there a real constraint hiding inside the shouting? Extract it as a rule. |
| Role-Player | `you are a…` opener | Does the character *know* something, or *care* about something, that improves the answer? |

## Step 3 — The ablation. This is the step nobody does, and it is the whole skill.

The lesson's rule is *"delete each sentence in turn and ask whether the answer would change."* That is
a question with an answer, and you can go and get it.

Split the prompt into sentences. For each one, build the prompt **without it**, run it, and compare.
A sentence whose removal does not change the answer is a sentence the model never used.

Write `.claude/scratch/ablate.mjs`:

```js
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { execFileSync } from 'node:child_process';

const original = readFileSync(process.argv[2], 'utf8').trim();
const sentences = original.split(/(?<=[.!?])\s+(?=[A-Z"'`])|\n{2,}/).map(s => s.trim()).filter(Boolean);
if (sentences.length > 12) { console.error(`${sentences.length} sentences — trim to the 12 you doubt most; each one costs a model call`); process.exit(1); }

mkdirSync('.claude/scratch/ablation', { recursive: true });

const ask = (prompt) => execFileSync('claude', ['-p', '--model', 'sonnet', '--output-format', 'text'],
  { input: prompt, encoding: 'utf8', maxBuffer: 8e6, timeout: 180000 });

const base = ask(original);
writeFileSync('.claude/scratch/ablation/_base.txt', base);

const rows = [];
for (let i = 0; i < sentences.length; i++) {
  const variant = sentences.filter((_, j) => j !== i).join(' ');
  let out = ''; try { out = ask(variant); } catch (e) { out = '(call failed) ' + e.message; }
  writeFileSync(`.claude/scratch/ablation/${i}.txt`, out);
  rows.push({ i, removed: sentences[i].slice(0, 90), overlap: +overlap(base, out).toFixed(2), delta_chars: out.length - base.length });
}

function overlap(a, b) {                    // content-word overlap: 1.0 = the answer did not change
  const bag = (s) => new Set((s.toLowerCase().match(/[a-z]{4,}/g) || []));
  const A = bag(a), B = bag(b);
  if (!A.size || !B.size) return 0;
  let hit = 0; for (const w of A) if (B.has(w)) hit++;
  return (2 * hit) / (A.size + B.size);
}

rows.sort((x, y) => y.overlap - x.overlap);
console.log(JSON.stringify({ sentences: sentences.length, rows }, null, 2));
```

Run it against the prompt under audit:

```bash
node .claude/scratch/ablate.mjs .claude/scratch/target.txt > .claude/scratch/ablation.json
node -e "const a=require('./.claude/scratch/ablation.json');
for(const r of a.rows) console.log(r.overlap.toFixed(2), r.overlap>=0.85?'DEAD WEIGHT':'load-bearing', '|', r.removed);"
```

**How to read it.** `overlap` is how much of the answer survived the sentence's removal.

- **≥ 0.85 — dead weight.** Deleting it changed nothing. Cut it, and name which of the seven it was.
- **0.60–0.85 — cosmetic.** It moved the tone, not the substance. Usually a Pleaser, Threatener or
  Role-Player line: it changed *how the answer sounds*. Cut it unless the reader wanted that tone.
- **< 0.60 — load-bearing.** This sentence is doing the work. **Keep it.** It is usually the verb, the
  constraint, or one of the Optimist's three facts.

The ablation is non-deterministic, so a borderline row is a borderline row. Two rules keep you honest:
re-run any sentence that lands between 0.80 and 0.90 before you call it dead, and **never cut a
sentence you did not test.** The whole point of this step is that the reader's instinct about which
line matters is the thing that has been wrong all along.

Cost note: this is `sentences + 1` model calls, all short. Cap it at 12 sentences. If the prompt is
longer than that, the reader has a Novelist and the ablation is a formality — but run it anyway on the
twelve they would defend, because that is the list they will argue with you about.

## Step 4 — Rewrite by subtraction

Assemble the new prompt **only from sentences the ablation kept.** You are not allowed to write a new
sentence except in one case: an **Optimist** finding, where the missing did/expected/got facts must be
added — and you add exactly those three, sourced from the transcript or from the reader, never invented.

Then apply the universal fix, in this order:

1. **Name the verb.** Review, write, explain, fix. First word, first sentence.
2. **Keep only the facts the answer depends on** — the ones the ablation scored load-bearing.
3. **Say what the answer should look like.**
4. **Everything else is already deleted.** It went in Step 3.

Finally, verify the subtraction did not cost anything. Run the original and the rewrite and diff:

```bash
node -e "const{execFileSync}=require('child_process'),fs=require('fs');
const run=p=>execFileSync('claude',['-p','--model','sonnet','--output-format','text'],{input:p,encoding:'utf8',maxBuffer:8e6});
const before=fs.readFileSync('.claude/scratch/target.txt','utf8');
const after=fs.readFileSync('.claude/scratch/rewrite.txt','utf8');
fs.writeFileSync('.claude/scratch/answer-before.txt',run(before));
fs.writeFileSync('.claude/scratch/answer-after.txt',run(after));
console.log('prompt words:',before.split(/\s+/).length,'->',after.split(/\s+/).length);"
```

Read both answers yourself. If the short prompt's answer is worse, **say so and keep the long one** —
one of your "dead weight" calls was wrong, and reporting that is worth more than a tidy result.

## Step 5 — Emit the report

Write `prompt-audit.md` in the repo root. Never overwrite an existing one — suffix `-2`.

```markdown
# Prompt audit — 2026-07-14
Source: session 31478d1f, 2026-07-11 (signals: re-asked-immediately, longer-than-its-answer).
Original: 214 words · 9 sentences. Rewrite: 38 words · 3 sentences. Cut: 82%.

## Verdict — this prompt commits 3 of the 7

| # | Pattern | Committed | The line that proves it |
|---|---|---|---|
| 1 | Pleaser | **YES** | "If you could kindly take a look when you get a chance, I'd really appreciate it" |
| 2 | Novelist | **YES** | 5 sentences of project history; ablation scored 4 of them ≥ 0.91 |
| 3 | Micromanager | no | — |
| 4 | Optimist | **YES** | The model asked back: "which upload path do you mean?" No expected/actual given. |
| 5 | Copy-Paster | no | — |
| 6 | Threatener | no | — |
| 7 | Role-Player | no | — |

## Ablation — measured, not guessed

| Overlap | Verdict | Sentence removed | Pattern |
|---|---|---|---|
| 0.97 | DEAD WEIGHT | "We started this project back in March using…" | Novelist |
| 0.94 | DEAD WEIGHT | "If you could kindly take a look…" | Pleaser |
| 0.91 | DEAD WEIGHT | "The whole thing is built on the stack we picked…" | Novelist |
| 0.72 | cosmetic | "This is really important to get right." | Threatener — moved tone, not substance |
| 0.31 | **load-bearing** | "The app crashes when someone uploads a large photo." | keep |
| 0.24 | **load-bearing** | "Small photos are fine." | keep — this is the ruled-out half |

## Before

> (verbatim, 214 words)

## After

> Look at the upload code and tell me why the app crashes on large photos.
> Small photos work. I already tried raising the upload timeout and it made no difference.

## What was removed, and why

| Removed | Pattern | Why it was safe to cut |
|---|---|---|
| 3 sentences of project history | Novelist | Ablation overlap 0.91–0.97. The answer did not change. |
| The politeness wrapper | Pleaser | Overlap 0.94. Buried the verb; softened the reply. |
| "This is really important" | Threatener | Overlap 0.72 — it changed the tone (added two hedging paragraphs) and nothing else. |

## What was ADDED — and this is the only addition allowed

| Added | Why |
|---|---|
| "Small photos work" / "I already tried raising the timeout" | **Optimist.** The model asked back because it could not see what the reader could see. These are the two facts it needed: what was ruled out, and what was already tried. |

## Side-by-side answers
`.claude/scratch/answer-before.txt` (1,840 chars, opens with three clarifying questions)
`.claude/scratch/answer-after.txt` (900 chars, names the file and the line on the first try)
```

## What this skill will not do

- **It will not make your prompt longer.** If the only fix it can find is "add more context," it is
  either an Optimist (three facts, and no more) or you have a good prompt and a hard problem.
- **It will not cut a sentence it did not test.** Every removal in the report carries an ablation score.
  A cut defended by taste is the same mistake as the padding it removes.
- **It will not write your CLAUDE.md.** A correction you type over and over is not a prompt problem —
  it is a missing rule. That is `vc-claude-md-harvester`.
- **It will not tune a test-generation prompt.** Prompts whose output is a test suite have their own
  failure modes. That is `vc-test-prompt-builder`.
- **It will not touch your transcripts.** It reads `~/.claude/projects/**` and writes only into
  `.claude/scratch/` and `prompt-audit.md`.
