Claude Skill

agami-reconcile

Reconciles known (label, expected_value) numbers from an existing dashboard against agami's answers. Input can be a SCREENSHOT of a Metabase / Power BI / Tableau / Looker dashboard (Claude's vision extracts the pairs), a CSV, or numbers pasted inline — the user doesn't need to kn

LLM Mart · 0 points · 8 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download AgamiAI-agami-core-plugins_agami_skills_agami-reconcile-6fbffd4.zip · 29 KB
Part of agamiai/agami-core — 6 skills

Install

skills CLI npx skills add https://github.com/AgamiAI/agami-core/tree/main/plugins/agami/skills/agami-reconcile
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agamiai-agami-core@llmmart
Git git clone https://github.com/AgamiAI/agami-core.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agamiai/agami-core collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

agami reconcile

You are running the reconciliation harness. Goal: take the evidence a person brings and prove agami can reproduce each answer. That evidence is labeled numbers from an existing dashboard (Tableau / Looker / Mode / Metabase / Power BI / spreadsheet), most often a screenshot, sometimes a CSV or pasted list; or the SQL they trust; or a list of questions. When numbers match and every part of the person's statement checks out, that's evidence the semantic model is right. When they don't, the receipt drill-down and the part ledger explain why: a definitional disagreement (gross vs net, refunds in vs out, FX rate at booking vs reporting date), a join the semantic model is missing, or a mistake in the person's own query. That is exactly the trust signal that makes a DE relax.

The person's query is evidence, never the answer. Every part of it is graded against the semantic model and the warehouse before anything is compared or kept.

This skill orchestrates:

  1. Extract the evidence rows from the input — a dashboard screenshot (via vision, confirmed with the user), a CSV, a pasted list, SQL the person trusts, or a list of questions. Number parsing is always deterministic (reconcile.py).
  2. Grade a supplied statement part by part: run it the way agami runs its own SQL, then grade its joins, typed values, required filters and aggregates (Phase 1.5).
  3. Generate a matching NL question for each label that has none.
  4. Run each question through the same NL→SQL→execute pipeline as agami-query.
  5. Compare actual vs expected with a tolerance, as one number or as a table, and name the part that differs.
  6. Present a markdown table with per-row status; for mismatches, render the full receipt as a drill-down; for the person's statements, the parts that fell short. Write the findings to disk.

Spec for the deterministic helpers: scripts/reconcile.py (input reader, number normalization, diff with tolerance, the part ledger and the findings). The shared procedure lives in shared/evidence-row.md, shared/part-ledger.md and shared/statement-check.md.

Conversation style

  • Tight loops. This skill is a tool, not a tutorial. One question per turn, max two sentences of prose between phases.
  • Surface mismatches loud. A reconcile run with 9/12 matches and 3 mismatches is a SUCCESSFUL run — the mismatches are the value. Lead with what didn't match.
  • Don't paste raw SQL in chat. The receipt has it. Same hard rule as agami-query — with one exception, Phase 3e's promotion offer, where the statement is shown because it is the thing being accepted into an answer key and cannot be hidden behind a receipt link at the moment somebody agrees to replay it.

Phase 0: Preflight

Same checks as agami-query / agami-connect:

  1. Plan-mode check per shared/plan-mode-check.md. This skill needs Bash + Read + Write — refuse if locked in plan mode. DO NOT write a plan file. DO NOT call ExitPlanMode. Refusal text: "I can't reconcile in plan mode — each row runs a live query and writes a receipt. Switch to Auto or Edit Automatically mode (Shift+Tab to cycle) and re-invoke me with the same input."

  2. Credentials present — read <artifacts_dir>/local/credentials for the active profile. If missing, invoke /agami-connect to set up first; this skill needs a working DB connection.

  3. Model present — <artifacts_dir>/<profile>/datasource.yaml must exist. If not, invoke /agami-connect. This skill needs an introspected model to generate questions against.

  4. Input — accept any of four shapes, or a mix; the user needn't know which. Detect what they gave:

    • A screenshot / image of a dashboard (Metabase, Power BI, Tableau, Looker, a spreadsheet) — the common case. Go to Phase 1's vision branch.
    • A CSV — a path in $ARGUMENTS, or pasted inline (write inline CSV to /tmp/agami-reconcile-<ts>.csv). Go to Phase 1's CSV branch. A third column holding SQL is the statement behind each tile: Phase 1n reads it as a statement, never as part of the label.
    • Numbers pasted inline as a list/table — treat as inline CSV.
    • SQL the person trusts — one or more statements, pasted or in a .sql file, alone or beside the question each answers. Go to Phase 1's statement branch. The statement is evidence, never the answer: Phase 1.5 grades every part of it before it is compared with anything.
    • A list of questions with no answers — one per line. Go to Phase 1's questions branch. A screenshot and the SQL behind its tiles may arrive together; Phase 1n joins them by label. A filled .csv in <artifacts_dir>/local/reconcile/inbox/ is the CSV branch: check the inbox before asking, so a person who filled the template and came back to say reconcile is not asked again. If they gave nothing (or just asked "can you check my dashboard?"), ask once the way agami-connect asks which database: AskUserQuestion, the four shapes as options, the lowest-friction one first, and the rarer inputs named in the prompt so they are visibly welcome ("Something else, a pasted label: value list or a JSON export? Choose Other and paste it.").
    label description
    A screenshot of the dashboard Metabase, Power BI, Tableau, Looker, a spreadsheet: whatever you have. I read the tiles and confirm what I read with you before anything runs.
    A CSV or an export Two columns, label and value, or the template I can write for you (label, value, sql, question).
    The SQL you trust One or more statements, pasted or in a .sql file. Each is graded part by part before anything is compared.
    A list of questions One per line. Agami answers each, the run checks the query behind each answer, and you decide on the report page.

    When they pick the CSV and have nothing to hand, write the template and hand off, the way connect writes credentials.example: with the Write tool, <artifacts_dir>/local/reconcile/inbox/reconcile.example.csv:

    # One line per number to check. Keep the header; these # lines are skipped.
    # label: the tile's name.  value: the number as shown ($4.2M, 12,450, 3.1%).
    # sql: the SQL behind it, if you have it.  question: the question in your words (optional).
    label,value,sql,question
    

    Then: "Fill in local/reconcile/inbox/reconcile.example.csv, one line per number, and come back and say reconcile. Nothing runs until you do." End the turn. On re-entry, Phase 1n reads every .csv in the inbox through reconcile.py intake; a file still holding only the header and comments is reported as empty (exit 4) and the person is asked again, never guessed at.

  5. If they gave a file path, validate it exists. If not, surface the error and stop.


Phase 1: Extract the (label, value) pairs

Whatever the input shape, the goal is the same normalized rows JSON. Number parsing is always deterministic — it goes through reconcile.py, never the LLM eyeballing a value (a misread expected number would manufacture a false mismatch on a verification surface).

Vision branch — a dashboard screenshot

  1. Read the image and extract every labeled number you can see — KPI tiles, table cells, chart value labels — as (label, raw_value) pairs. Keep the label the user would recognize ("Total Revenue", "Active Users — Apr"), and the value exactly as shown, verbatim ($4.2M, ₹2.16Cr, 42%, 1,234) — don't convert it; the normalizer does that.
  2. Write the pairs as a 2-column CSV (Write tool — never a heredoc/python3 -c) to /tmp/agami-reconcile-<ts>.csv, then run the SAME normalizer as the CSV branch (below) so value parsing stays deterministic.
  3. Confirm before reconciling — vision can misread. Show the extracted pairs as a small table and ask the user to fix any misread label/number: "I read these N numbers off your screenshot — correct anything I got wrong, then I'll reconcile." This confirm step is mandatory: a wrong expected-value isn't a model bug but it reads like one. If a tile is ambiguous or partly cut off, say so and skip it rather than guess.

CSV branch — a CSV path or inline-pasted CSV

python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" intake --file "<csv_path>" \
  --source "<the person's own words for where this came from>" > /tmp/agami-reconcile-rows-<ts>.json

The helper (used by both branches) handles:

  • Header detection (with-or-without first-row column names)
  • 2-column or 3+ column inputs (3rd onward are appended to the label as context)
  • Currency symbols / magnitude suffixes / accounting parens / percent ($4.2M, ₹2.16Cr, (123.45), 42%)
  • Null sentinels (n/a, —, blank)

Read the JSON. Each row is {label, expected_value, raw_value}. Discard rows where expected_value is null (unparseable) — surface a one-liner: "Skipped 2 rows where the value couldn't be parsed: 'X', 'Y'."

Statement branch — SQL the person trusts

If the SQL was pasted, write it to /tmp/agami-reconcile-<ts>.sql with the Write tool (several statements separated by ;); if it came as question,sql pairs, write those as a CSV. Take a file path as given. A statement that arrives without a question needs one before agami can be asked the same thing: derive the question the statement answers, show it, and let the person correct the wording. Do not run the statement here. It is evidence, and Phase 1.5 is where its parts are graded.

Questions branch — a list of questions and no answers

Write the lines to /tmp/agami-reconcile-<ts>.txt with the Write tool, one question per line. These rows carry no expected value: Phase 2 asks agami each question, and until the person grades the answers there is nothing to compare against. Say so up front: "No answers to compare with, so I'll ask agami each one, check the query behind each answer, and you decide from there."

1n — Normalize every input into evidence rows

A filled template in <artifacts_dir>/local/reconcile/inbox/ is one of the files here, passed with its own --file.

python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" intake --file <path> [--file <path> ...] \
  --source "<the person's own words for where this came from>" > /tmp/agami-reconcile-rows-<ts>.json

parse stays the number-only reader; intake reads everything parse reads and the other shapes too, so a run with any SQL or questions in it goes through intake for all of its files, the vision CSV included. One row per thing to check, {label, question, statement, expected, raw_value, provenance}, with any of the three evidence fields missing. Several files merge by label under a case-and-whitespace fold, so the SQL behind a tile joins the tile's row. Exit 2 is a file that could not be opened; exit 4 is an input with no question, statement or number anywhere. The shapes, the detection rules and the row are in shared/evidence-row.md.

Create the run directory <artifacts_dir>/local/reconcile/<ts>/, and rows/<n>/ under it for every row that carries a statement.

Show what was read before anything runs, and hand off, the way agami-connect shows the prune page before it introspects. The intake page lists every row: the question we will ask agami (read from a label, and editable), the number expected, whether SQL came with it, and which file and line it came from. The person fixes a question we read wrong or unticks a row, generates the block, and pastes it back. It is the same design language and the same paste-back grammar as the report page.

python3 "$AGAMI_PLUGIN_ROOT/scripts/render_reconcile_intake.py" --title "What we read · <profile>" \
  --profile <profile> --run <ts> --intake-file /tmp/agami-reconcile-rows-<ts>.json \
  --out "<artifacts_dir>/local/reconcile/<ts>/intake.html"

Then say it in two lines and end the turn:

I read <N> rows from <the screenshot / csv_path / the statements / the questions>: <n> numbers, <m> with your SQL, <k> questions. Open the page, fix any question I read wrong or untick a row, and paste the block back; then I run. Typically <N> × 5–15s per row.

On re-entry (the block arrives, profile: / reconcile-run: / intake: / one JSON array / done), never hand-edit the rows: pipe the block to the parser, which applies it to the rows file and says what it did:

python3 "$AGAMI_PLUGIN_ROOT/scripts/parse_reconcile_intake.py" --block-file /tmp/agami-reconcile-intake-<ts>.txt \
  --rows-file /tmp/agami-reconcile-rows-<ts>.json --run <ts> --out "<artifacts_dir>/local/reconcile/<ts>/intake.json"

intake.json in the run directory is the run's own copy of what runs: Phase 2 reads the next rows from it, a resume on a later day reads it, and nothing under /tmp is needed again. ok: true with kept, dropped and edited counts means it holds exactly what runs; an edited question carries provenance.question_from, so it is never mistaken for one we read. A needs_judgment (another run's block, a row the file does not have, a question that is not text, a missing section) applies nothing: ask for the block again. If the person answers in chat instead ("looks right, go ahead"), run with the rows as read: Phase 2's first next-chunk --rows-file call copies them into the run directory unchanged. A per-row "is this the question?" in chat is never asked.


Phase 1.5: Grade a supplied statement, part by part

For every row that carries a statement. Skip this phase for a row that does not.

The person's statement runs through the guard agami's own SQL runs through, and never on a command-line tier. shared/statement-check.md is the procedure, step by step, and shared/part-ledger.md is what each file means. Write statement.sql in rows/<n>/ first, verbatim, then check the row with one call:

"$PY" "$AGAMI_PLUGIN_ROOT/scripts/check_statement.py" --profile <profile> --area <area> \
  --row-dir "<artifacts_dir>/local/reconcile/<ts>/rows/<n>"

Every step that reaches the database runs inside that script, and every statement it sends goes through execute_sql's guarded chokepoint with the built-in executor, never with --no-safety. psql, mysql, snowsql, sqlite3 and DuckDB are never used here, whatever tier the profile queries on: they have no read-only gate, no scope gate and no bound, and a statement that writes must not meet a database through them. Never run the statement or a probe yourself. Exit 0 means the row was checked, whatever the statement did; exit 3 stops the run (below); exit 2 means it could not start, and statement-check.md says why; any other exit is a crash, which stops the run too. Checking a row again is the same call, and it first clears every file the last check wrote. What the script does:

  • 1.5a — Is it one read-only SELECT? The guard's own read-only gate reads it first, per shared/sql-generation-rules.md, and then its recon gate, which refuses a call that reads the server's own metadata (its version, the session's identity, a privilege check). A statement either gate refuses gets status: "refused" in run.json and the gate's rule (read_only or recon); nothing runs after it, and nothing is probed. Mark the row error.
  • 1.5b — Run it the way agami runs its own. The zero-row check, then sm prepare, then the statement through the guard: its result to statement.csv, and into run.json the status, exit, classifier kind, guard rule, detail and remediation, never the raw error text. A refusal is a finding, not a crash: a table_scope or column_scope refusal grades scope: model_gap, because the person wanted a table or column the semantic model does not expose; select_star grades runs: query_defect. Never rewrite the statement and never retry. Other failures carry the kind shared/db_error_classifier.md names; auth, dsn, network, permission and driver_missing exit 3, and so does an engine_mismatch refusal, which means the semantic model declares an engine its credentials do not connect to. Exit 3 stops the run as agami-query Phase 3b stops it: tell the person the remediation from run.json. Nothing in this phase writes query_log.jsonl: agami-save-correction reads that log's last successful line as the question to correct, and a probe there would be corrected instead of the answer. The phase keeps its own record instead: run.json for the statement, zero-row.run.json for its wrap, and a .run.json beside every probe's CSV, so every execution and every refusal here is written down. A wrap the guard refuses is that check not run, never a fault in the statement, and the statement is still checked on its own.
  • 1.5c — Its receipt, and the semantic model's words. sm receipt to statement-receipt.json, and beside it sm mentions to mentions.json: every description, caveat, glossary line, narrative paragraph and prompt example that mentions a table or column the statement reads. The ledger puts those words beside any part that falls short, so the caveat that shaped the SQL is read next to the number that went wrong. Quoted, never graded.
  • 1.5d — Probes. sm join-probes and sm filter-values plan emit SQL; each probe is written to its own .sql file, and the row's probes run as one plan through execute_sql's batch door (statement-check.md step 8): the semantic model is resolved once and the connection kept open, every probe still through the guard on its own, each to the CSV part-ledger.md names with a .run.json beside it. The near-miss probe for a typed value runs only when its exists probe counted 0. Then sm filter-values judge. A probe the guard refuses or the database fails leaves an empty CSV; leave it there, the ledger reads it as a probe that failed. A file the ledger expects and does not find, or finds empty, is a part it grades unresolved, never clean.

After the script, three steps are yours. 1.5g is a judgment made by reading, and no script makes it.

  • 1.5e — The ledger, run once. python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" ledger --row-dir rows/<n> writes ledger.json: one grade per part, confirmed, model_gap, query_defect or unresolved, and the weakest grade as the row's ledger_verdict; a part may also be noted, a fact the run states and never judges, which never decides the verdict. Run it once per row, in Phase 2e, after the comparison, with --with-claims when agami's statement exists too and without it when agami's run failed. The files this phase wrote are what it reads, so nothing is lost by waiting, and a ledger written here and again later is the same ledger twice. A part reaches model_gap only by measurement; the statement asserting something is never the evidence for it.
  • 1.5f — Its result is the expected value. For a row that came with no number, expected is the single cell statement.csv returned (its text folded to a number by reconcile.py diff, which reads $4.2M and 47,238,221.00 alike), or the table's shape when it returned several rows. For a row that came with a tile number too, run reconcile.py diff between the tile and the statement's own result: a disagreement means the statement is not the tile's statement, or the data moved; flag the row in Phase 3b.5 and keep the tile's number as expected.
  • 1.5g — Does the statement answer the question? For every statement row, write question_fit.json; when the row carries a question, read the two side by side first, before anything is compared. Doubtful when the grain differs (a count of items for a question about orders), the measure differs (revenue for a question about a count), a filter is present the question never asked for or absent when it did, or the time window differs. Write question_fit.json in the row directory: {"fit": "plausible" | "doubtful" | "no_question", "reason": "<one sentence, or null>"}, with no_question only for a statement that came alone: against a row that carries a question it is a contradiction, and the findings verb refuses to keep such a row. This is a judgment made by reading, the one part of the ledger that is; it can withhold a row from the keep-offer and never proves anything about the semantic model. A doubtful row grades match_unverified at best, shows in Phase 3b.5 with the reason, and the person settles it by rewording the question or the statement and re-running that row. Phase 3e keeps a match row as a worked example, which teaches the AI a question-to-SQL pairing, and a sound statement paired with the wrong question is the most harmful thing that step could keep.

Phase 2: Generate questions + execute

Work five rows at a time. rows.jsonl is the run's checkpoint: Phase 2d appends one record per finished row, and the next rows to run are read from it, never chosen by hand:

python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" next-chunk --run-dir "<artifacts_dir>/local/reconcile/<ts>" \
  --rows-file /tmp/agami-reconcile-rows-<ts>.json    # seeds the run's intake.json once; later calls need only --run-dir

Exit 0 hands back chunk, the next five rows not yet in rows.jsonl, with finished, remaining, chunk_index of chunks_total, and progress, the counts by status over the rows done so far, read from the checkpoint and never tallied by hand. Take the five through Phase 1.5 (statement rows) and 2a to 2f below; each lands in rows.jsonl as it finishes. When the chunk is done: build the report items for every row finished so far and render the report page (3a.5), so the person can already read the first rows, say one progress line, and end the turn:

Rows 6 to 10 of 50 done. So far: <progress as colored words, in 3a's wording>. Page: <artifacts_dir>/local/reconcile/<ts>/report.html. Say continue for the next five, or continue all to run the rest without stopping.

continue calls next-chunk again. continue all runs chunk after chunk, a progress line per chunk and no stop, until exit 4. Exit 4 means every row is in the checkpoint: go to Phase 3. A run interrupted anywhere resumes with the same call: a row in rows.jsonl is never run twice, and "resume the reconcile" on a later day is python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" resume --reconcile-dir "<artifacts_dir>/local/reconcile", which names the newest run directory that still has rows remaining (exit 4 when none does), then next-chunk on it. Exit 2 is a refusal and stderr says which: a run directory or rows file that is not there, a rows file that is not a list of row objects, or a checkpoint line that cannot be read. When it names a rows.jsonl line, fix or remove that line before continuing, never skip it, since skipping would run its row again. The first five are a smoke test: a wrong profile, a misread file or a question in the wrong words is caught at five rows, not fifty. The keep-offer stays one per run (3e), never one per chunk.

For each row in the chunk:

2a — Generate the NL question

A row that already carries a question (a question,sql pair, or a statement whose question the person confirmed in Phase 1) uses it as written; do not generate a second one. Otherwise use the LLM to translate label (+ context if present) into the most natural English question whose answer should be expected. Examples:

label question
Q3 2025 Revenue "What was total revenue in Q3 2025?"
Active customers (Apr 2026) "How many active customers did we have in April 2026?"
Pipeline value (open opps) "What's the total pipeline value across open opportunities?"
Mean order size last 30 days "What's the average order size over the last 30 days?"

The semantic model + examples library are loaded; let the LLM pick the right subject areas / entities / metrics that resolve the labeled term to a concrete query.

2b — Run via the agami-query pipeline

Agami's answer comes from a cold client, never from this session. This session holds your statement, the ledger's grades and the fit reasoning; anything it wrote would not be what a person gets from agami. Ask the way the golden run asks, once per chunk, with the five rows next-chunk handed back:

python3 "$AGAMI_PLUGIN_ROOT/scripts/run_golden_eval.py" --profile <profile> --via mcp \
  --ask-file /tmp/agami-reconcile-chunk-<ts>.json --out-dir "<artifacts_dir>/local/reconcile/<ts>/rows" --parallel 4

--ask-file takes next-chunk's output as it is (its chunk), and spawns the operator's own client per question, several at a time. --via mcp is what makes the answer a person's answer: each client is served agami's own MCP tools and nothing else, so it calls get_datasource_schema and get_prompt_examples itself, scoping them as it sees fit, and runs its own statement. That is the point of asking this way. A run that pre-fetched one schema into the prompt would never exercise the things that decide whether a real question succeeds, and could pass on a semantic model nobody could query.

It probes as much as it likes, and a query that did not answer ends the row. Successful queries are free, and how many a row needed is the measurement: probe_count reads how much the semantic model failed to say up front. But the first query the database rejects or the safety check blocks stops the row, and the row's error is what the safety check or the database said about it. That is not a run that went wrong, it is the finding: a statement that did not run says the semantic model names something that is not there, and a client left free to retry would bury exactly that. The rule is enforced by the server, so it cannot be half-obeyed.

--via context is the older way, kept for comparing the two surfaces against one intake: the run pre-fetches one schema into the prompt and the client gets no tools. Each row's answer file records which ran, as mode, because the two measure different things.

Each row's file carries rows/<n>/agami-answer.json as {row, question, sql, statements, error, mode, probe_count, probes, client_value} (the last three only under --via mcp, where there is a trace to carry): statements is every statement the client wrote, in order, and sql is the last of them, the one whose result answers the question. What is reused across the chunk is what does not depend on the question; the session itself is never reused, because a fresh one is the thing being measured. One question at a time is --ask "<question>" --out rows/<n>/agami-answer.json. Per row, exit 0 for the batch, or a sql in the row's file, carries a statement: write it verbatim to rows/<n>/agami.sql (only sql; when statements has more than one, keep them all in the row record's agami_statements for the page and never run the earlier ones: the read-only rule refuses anything but a SELECT, so an earlier statement can only be a look at the data). Under --via mcp, do not run it yourself: the run has already written rows/<n>/actual.csv and rows/<n>/agami-run.json, in code. A statement the server did not run is never run again (its outcome is copied from the trace), and one that ran is run once more, only for its result, through execute_sql's guarded envelope. No command-line tier is involved, and what runs is the server's own record of the statement, never the client's copy of it. Under --via context, run it through the profile's tier exactly as agami-query Phase 1e tabulates it, by file and never with --no-safety (stdout to rows/<n>/actual.csv, its exit to rows/<n>/agami-run.json in run.json's shape). Then sm receipt --sql-file rows/<n>/agami.sql and the chart report, as agami-query Phase 3 does. A row whose file has no sql carries one of the generator's fixed sentences as its error (the client could not be started, timed out, exited without answering, answered without a statement, or, under --via mcp, reported a number it did not query or a statement it did not run); the batch exits 3 when any row is like that. A row whose query DID NOT RUN (the safety check blocked it, or the database rejected it) is different and keeps its statement, with what the safety check or the database said as its error. That row is error with the sentence as its error. Never write agami's SQL yourself, and never retry with your own wording; a row with no cold answer is an error row, and that is the finding. Exit 2 means the run could not start: the profile's context could not be built, or, under --via mcp, agami's tools did not come up on this machine. Stop the run and say which. A tool surface that will not start is broken tool fetching, which is a finding about the deployment — never answer the rows another way and print the numbers under agami's name.

Capture, per row:

  • The generated SQL, verbatim, from agami-answer.json (its sql; and its statements when the client wrote several)
  • The result (one cell, or the columns and a row count)
  • The full chart-template HTML report (so the user can drill in for mismatches)
  • The trust receipt (with confidence, signed-off-by, etc.)

The SQL you capture here is the one that lands in the row record (Phase 2d) — keep it verbatim. It is the only place the statement survives the run: the chart report at report_path is HTML, and nothing re-derives the statement from it afterwards.

If the SQL fails OR the result isn't a single scalar (e.g., the LLM-generated question returned a multi-row table), capture an error: Could not extract a single scalar from the result. These rows show up as error status in the report. One case is different: a row that carries the person's statement may legitimately return a table. Keep recorded as {"columns": [...], "rows": []} for it (the columns, never the rows), write the result CSV to rows/<n>/actual.csv, and let Phase 2e compare it as a table.

2.5 — Check agami's query when there is nothing to compare against

Only for rows that carry a question and neither a statement nor an expected value (the questions branch). Agami has answered each one in 2b and there is no number to diff against. Two things are true of such a row, and the phase does one each. Whether the answer is right is the person's call, because nothing here can measure it. Whether the query behind it holds up against the semantic model is not a matter of opinion, and this phase measures it.

  1. Grade agami's query, part by part. Take agami's statement from 2b through Phase 1.5 exactly as a supplied statement goes through it, writing it as statement.sql in rows/<n>/: the files name the statement under test, whoever wrote it. Three differences, and only three:
    • 1.5f does not apply. Agami's own result never becomes expected. A row whose expected value came from the query being graded compares agami against agami, proves nothing, and reads on the page as a match. This is the never-ground-truth rule at its sharpest: the side being checked cannot supply the answer key.
    • 1.5e runs without --with-claims. Claims compare two statements and there is only one.
    • 1.5g still runs, and it is the most useful check on the page: whether agami's query answers the question asked is exactly what the person is about to judge. A row where agami's run failed has no query to check; it skips this phase and says so.
  2. A grade that falls short names agami, not the person. query_defect on this row means agami wrote a query with a mistake in it, which the person fixes by teaching agami, not by editing anything of their own; model_gap is a gap in the semantic model whoever tripped on it. The card's words follow from the ledger and the row having no statement, so nothing here is written by hand.
  3. Report it on the reconciliation report, the one page every path ends on (3a.5). The row's status is ungraded: its verdict says agami answered and the call is the person's, and carries what the checks found so the call is an informed one. The card's three sections are the same three: Data shows up to five of agami's rows, because an answer nobody can see cannot be judged; SQL shows agami's query, and says it is the only one written for this row; Checks is the ledger just run. The decisions are the same six, and the person chooses example when agami's answer is right (it sends agami's query for this question to /agami-save-correction, so agami writes it that way again), change when a definition is wrong, reword when the question was read differently, and nothing to leave the row as it is. fix is not offered here: it means "fix your query", and on this row the person wrote none.

2c — Diff

python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" diff \
  --expected "<expected_value>" \
  --actual "<actual_value_from_query>" \
  --tolerance 0.01 > rows/<n>/diff.json

The diff is written to the row directory, where the record verb (2d) reads it.

Default tolerance: ±1%. The user can override with tolerance=N% in their original ask (e.g., "reconcile with 5% tolerance"). Tolerance applies to numeric comparisons; for text values (rare), use exact match.

Capture: match (bool), delta, delta_pct. When expected came from running the person's statement (Phase 1.5f), diff against it exactly the same way; the statement's result is the expected value, and its grades say how far to trust it.

2d — Build the row record

Per row:

{
  "label":        "<from CSV>",
  "question":     "<LLM-generated NL question>",
  "expected":     <number>,
  "actual":       <number or null if errored>,
  "delta_pct":    <signed fraction or null>,
  "match":        true | false,
  "status":       "match" | "match_unverified" | "mismatch" | "expected_doubtful" | "error" | "ungraded",
  "report_path":  "<artifacts_dir>/local/charts/<profile>/<ts>.html",  // the full chart report for this query
  "sql":          "<the statement agami wrote, kept even when it failed; null only when none was written>",
  "recorded":     {"columns": ["<column name>"], "rows": [[<value>]]},  // what the query actually returned
  "error":        "<message if status=error, else null>",
  "provenance":   {"shape": "a|b|c|d", "source": "<the person's words>", "file": "...", "line": 2},
  "statement":    "<the person's SQL, verbatim, or null>",
  "statement_recorded":     {"columns": ["..."], "rows": [[<one cell>]]} | {"columns": ["..."], "row_count": 12} | null,
  "statement_receipt_path": "<rows/<n>/statement-receipt.json, or null>",
  "receipt_path": "<the receipt of agami's own statement>",
  "ledger":       <the ledger.json object from reconcile.py ledger: its rows, verdict and counts> | null,
  "ledger_verdict": "confirmed" | "model_gap" | "query_defect" | "unresolved" | null,
  "comparison":   {"scalar": <the diff>} | {"result_set": <the compare-results score>} | null,
  "claims":       <the sm claims diff between the two statements, or null>,
  "finding_keys": ["<keys of the findings this row contributed to>"],
  "agami_statements": ["<every statement the client wrote, in order; sql is the last>"]  // only when there were several, else []
}

The keys from provenance down are appended after error and every earlier key keeps its meaning; a reader that only knows the older shape keeps working. status gains three values beside the three it had. Their rules, applied by code and never by feel:

python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" status --match <true|false|none> --ledger-verdict <verdict|none>
Status When
match the numbers match, and every graded part is confirmed (or there was no statement to grade)
match_unverified the numbers match, but a part of the person's statement is not confirmed. Phase 3e never sees it: a match nobody could verify may be luck. A query written differently from agami's is not this: the claims comparison notes it (noted, "same answer, different query" on the page) and the row stays match. A table that matched only because a column of yours paired with a differently named column of agami's, where one value fills most of your column's rows, is this: the two could be different columns holding the same values, so the ledger's value_pair part stays open and the card names both columns
mismatch the numbers differ and the person's statement has no query_defect, so agami is the likelier culprit
expected_doubtful the numbers differ and the person's statement has a query_defect, so the expected value itself is in doubt. Kept out of the mismatch tally
error the row could not run
ungraded agami answered and there was nothing to compare against: no statement of the person's and no number. Nothing failed and nothing has been decided, so the row waits for a person on the report page (Phase 2.5). Its expected value never comes from the run

sql is the statement captured in Phase 2b, written down verbatim. recorded is the result it returned, shaped as columns + rows — the same two keys the golden-dataset receipt uses — so whoever picks this row up later forwards it as-is instead of rebuilding it from a number and guessing at a column name.

On a status: "error" row recorded is null and the statement is kept. An error row carries nothing a later reader could mistake for a verified ANSWER, which is what actual, recorded and delta_pct are; a statement that failed cannot be mistaken for one that answered, because the row's own error sentence says it did not, and reading it is how a person tells a semantic model declaring a column the warehouse lacks from a query agami wrote wrong. sql is null only when agami wrote no statement at all.

Build the record with the verb, never by hand:

python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" record --run-dir "<artifacts_dir>/local/reconcile/<ts>" --row <n> [--tolerance 0.01] [--report-path <path>]

It reads the row directory (intake.json for the label, the question, your statement and the number you gave; agami-answer.json, agami-run.json and actual.csv for agami's side; statement.csv, diff.json or comparison.json, claims.json and ledger.json) and appends the record above to rows.jsonl, replacing an earlier record for the same row. Exit 2 names the file it needs and cannot find: fix the run, never type the record. A question-only row is recorded ungraded rather than refused or written as error: nothing failed, and it waits for a person on the report page. Its expected stays empty on purpose, and the verb takes no argument that could fill it: a value the run supplied would be agami's own answer coming back as its own answer key. The keys are additive — a reader that only knows the older shape keeps working.

2e — Compare, as a number or as a table, and name the part that differs

For a row whose expected is one number, Phase 2c's diff is the comparison. For a row whose statement returned a table, compare the two result CSVs through the golden comparator, so a table-shaped answer is judged the way an answer key is:

bash "$AGAMI_PLUGIN_ROOT/scripts/sm" compare-results "$ROOT" \
  --golden-csv rows/<n>/statement.csv --generated-csv rows/<n>/actual.csv \
  --match values --unordered > rows/<n>/comparison.json

accuracy of 1.0 is a match. Row order is never part of this comparison (--unordered): the ordering claim below says whether the two statements sort the same way, and a different sort is a different query, not a different answer. The score also says how far the two tables agree when they do not match: paired_row_share and column_agreement beside column_pairs, which the report page reads as "9 of 10 rows match".

When the row carries both statements, name where they differ before anyone reads two receipts side by side:

bash "$AGAMI_PLUGIN_ROOT/scripts/sm" claims "$ROOT" --sql-file rows/<n>/agami.sql --against-sql-file rows/<n>/statement.sql > rows/<n>/claims.json
python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" ledger --row-dir rows/<n> --with-claims

This is the row's one ledger run. Every file Phase 1.5 wrote is still there, so the grades are the same ones 1.5 would have produced, plus the two claim parts and, for a table that matched, a value_pair part for each column of yours that matched a differently named column only through one repeated value. When agami's own run failed and there is no statement to compare against, run it here without --with-claims. Never run it twice.

The eight claims (tables, what is selected, filter predicates, date window, group keys, join keys, ordering, limit) say which part differs; they never say who is right. Two statements are the same query only when every claim that could be read agrees, what they select included; when the data could not be compared, the page still says "same query, answer not compared" or "different query, answer not compared". Then set the row's status with reconcile.py status, from the diff's match and the ledger's verdict. For a table there is no diff: pass --match true when compare-results reports accuracy of 1.0, false otherwise, and none when it could not score.

2f — Write the findings

reconcile.py record has written every row record to <artifacts_dir>/local/reconcile/<ts>/rows.jsonl; then:

python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" findings --run-dir "<artifacts_dir>/local/reconcile/<ts>"

It writes findings.json (one entry per place the semantic model was shown to be missing or wrong, with every row that showed it), query_defects.json (the parts of the person's statements the data proved wrong, listed apart so nothing about the semantic model is proposed from them), and ledger.json. Question text and SQL only, never result rows beyond the one recorded cell. This is reconcile's own record for the person; nothing reads it but them.


Phase 3: Present

Tell every row in four beats, in the reader's order. What reconcile is for, given any input (SQL, a question, a chart, or a mix): (1) how we read your input and how we checked it, from Phase 1's shape and the question we read from a label, the parts of your statement that held and the ones that did not (the ledger), and whether the statement answers its question (1.5g); (2) what agami did with the question and what it answered, from Phase 2b, beside the value you expected; (3) how agami got there, from the receipt (the tables it read, the joins, the declared filters it applied, the metric it matched), the claim that differs between the two statements (2e), and the semantic model's own words about what it read (1.5c); (4) what to change so the output matches, on whichever side the mistake is, or what to keep when it already matches and every check passed: a definition through /agami-save-correction, a mistake in your query, a reworded question, or nothing; keep is Phase 3e's offer, made once for the batch and never per row. The summary comes first (3a), then every row in its four beats (3a.5), then the tables the beats drew from (3b to 3d), then the one offer (3e) and the close (3f). The tables and the offer keep their exact text; the beats decide what is read first.

Everything this phase says to the person follows shared/plain-language.md: name who did what (you and your query, agami and its answer, the semantic model and its caveats, filters, joins and metrics, the prompt examples, and the data), and never write the word "model" on its own, because it can mean the semantic model, the AI, the prompt examples or the database and a reader cannot tell which; name the thing and never the mechanism; one idea per sentence, cause then effect then the one action; quote a caveat when it decided something. The part ids and file names stay in the tables below and in the files. The sentences around them are plain, and the AI never speaks of itself steering, front-running or deciding the answer.

3a — Summary line first

Reconciled <N> numbers: <M> match (within ±1%), <K> mismatch, <E> error.

When any row carried a statement, add one more line, counting the two statuses that belong to neither <M> nor <K>. Say each status in the words reconcile.py's status_words gives it and never the status itself: a person reading expected_doubtful has to look it up, and that is the row where their own query is the thing in doubt:

<U> gave the same answer, but part of your query could not be checked; <D> gave a different answer, and your query has a problem.

3a.5 — Every row in four beats

The four beats are told on the report page, one card per row for a batch and a checklist with a rail for a single audited query, and the chat carries only three lines: the summary of 3a with the counts as colored words, the report's path, and one line of next steps naming what to fix, what to decide and how many rows are ready to keep. A person reading the transcript later keeps the counts and the actions; the page carries the checks. When the page cannot be written, the four beats below are said in chat instead, one block per row, in the words of shared/plain-language.md. A row with no statement skips beat 1's parts and says so; a row that matched with every check passed has a one-line beat 4: keep it. Beat 4 never asks anything per row; the keep question is 3e's, once.

What each card says, whether on the page or, without one, in chat:

**Q3 Revenue** (from your CSV, tile 3, with the SQL behind it)
1. What you gave us: a question we read from the tile label as "What was total revenue in Q3 2025?", which you confirmed, and a query that passed every check but one: it leaves out the filter the semantic model declares on orders, `status != 'cancelled'`.
2. What agami did: it asked the same question and answered $3,890,000; your number is $4,200,000, 7.4% apart.
3. How it got there: agami read orders, applied the declared filter on status, and matched the metric "revenue". The two queries differ in one place, that filter. The semantic model's caveat on orders says: "cancelled orders are excluded from revenue".
4. What to change: nothing on agami's side. Your query counts cancelled orders; add the filter and the numbers match. If cancelled orders belong in revenue for you, the caveat and the declared filter are the things to change, through /agami-save-correction.

**Order count** (from your CSV, tile 1)
1. What you gave us: a number, and a question we read from the label as "How many orders were placed in Q3 2025?", which you confirmed. No query to check.
2. What agami did: 12,450; your number is 12,450.
3. How it got there: agami read orders with the declared filter on status and matched the metric "order count".
4. Keep it: the numbers match and there is nothing unconfirmed, so this row is offered below.

Beat 4 names the side: "your query" for a defect, "the semantic model" for a gap, "the question" for a doubtful fit, and "agami's answer" for a mismatch where your statement passed every check (a worked example is the fix). It never says "the model".

Render the four beats as a page, the way /agami-connect hands over the model explorer, so the run is shown the way the semantic model is shown. Every row is the same card: the question, one verdict carrying the measurement behind it, the one thing to do, and three sections closed at rest, Data, SQL and Checks, each with a one-line summary that names what did not pass when something did not, and a count when everything did. Render it after every chunk with every row finished so far; the render after Phase 2's exit 4 is the one 3a points at. The page filters by status, by who acts and by a word in the question or the beats; a filter narrows what is shown, never what the block sends, so a row decided and then filtered away is still in the block. The chat keeps 3a, the link and one line of next steps; 3b to 3d below still render when the page could not be written.

  1. Build the report items file with the verb, never by hand. The renderer runs reconcile.py report-items --run-dir itself from the run directory: one item per row of rows.jsonl, from each row's ledger, its comparison, its claims and the two receipts, every item carrying diff (one row per check, {key, state, yours, agami, note, yours_hi?, agami_hi?}, the check named first and the two sides as values with the differing tokens marked), result (data: matches, partly, differs or could_not_compare; query: same, different or not_comparable; the plain-word label; unchecked, the checks on your query that could not run; differs_in), fix (your query, the semantic model, the examples, the question, agami again, or nothing), fix_words, prefill, sentence and change. You hand it words only: a words file {"<row>": {"sentence": "…", "change": ["…"]}} with one entry per row whose sentence or change you rewrote in the plain words of shared/plain-language.md. You rewrite only sentence and change, and never touch diff, result, fix or any other field; a words file carrying another key is refused. (report-items --run-dir on its own prints the items for reading.)
  2. Render from the run directory, check it, and point at it:
    python3 "$AGAMI_PLUGIN_ROOT/scripts/render_reconcile_report.py" --title "Reconcile · <profile>" \
      --profile <profile> --run-dir "<artifacts_dir>/local/reconcile/<ts>" \
      --words-file /tmp/agami-reconcile-words-<ts>.json \
      --out "<artifacts_dir>/local/reconcile/<ts>/report.html"
    python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" check-run --run-dir "<artifacts_dir>/local/reconcile/<ts>"
    
    The renderer writes <run_dir>/report-items.json beside the page (for the parser and for reading), stamps the page with the digest of the items it rendered, and prints three lines: the counts by result and by fix as words, the page's path, and the next step. check-run exits 0 only when every row in the checkpoint has its files and the page's stamp is the digest of the current items; on 4, fix what it names and render again. Say the three lines the renderer printed, the counts as colored words (match, differs, a mistake on your side, could not check): the 3a summary, the report's path, and one line of next steps. The page offers keep only on rows whose status is match with a single recorded cell, Phase 3e's own predicate; the page draws that from the items, and the parser checks it again from the run's own files, because the offer's predicate is the ledger's and never the page's. Never write report.html, report-items.json or rows.jsonl with the Write tool; the only files this session writes by hand in Phases 2 and 3 are the words file and the chunk file.
  3. Read the decisions back when the block arrives (profile:, reconcile-run:, decisions: and one JSON array, done), handing the parser the run directory the page was rendered from. It reads which rows may be kept from rows.jsonl and each row's ledger.json, never from anything typed, and refuses a block whose reconcile-run is not this run:
    python3 "$AGAMI_PLUGIN_ROOT/scripts/parse_reconcile_report.py" --block-file /tmp/agami-reconcile-decisions-<ts>.txt --run-dir "<artifacts_dir>/local/reconcile/<ts>"
    
    A needs_judgment means the block did not parse, came from another run, or carries a decision that could not be applied as written (a keep the run does not allow, words beside a keep, a misspelt decision): ask for it again, apply nothing. Then, per decision: keep is the person's yes to Phase 3e's offer for that row, applied through 3e's own doors and its own rules (the split, the band, one call per row); change takes the finding and the person's words to /agami-save-correction, one definition at a time; example takes a statement and its question to /agami-save-correction as a prompt example, with the words as its note, so agami writes the query that way next time (the same door 3e's keep uses for an example, without 3e's split). Which statement, on which row: the person's when they supplied one; on a question-only row there is none, and the person choosing example there is saying agami's own query is right, so agami's is what gets sent; fix and reword come back to the person as the next thing to do, with the row's beat 4 repeated; nothing changes nothing. No decision writes anything this skill does not already write.

3b — Mismatches table (lead with what didn't match)

Render the mismatches as a markdown table BEFORE the matches:

### Mismatches

| Label | Expected | Got | Δ | Drill-down |
|---|---:|---:|---:|---|
| Q3 2025 Revenue          | $4,200,000 | $3,890,000 | -7.4% | <artifacts_dir>/local/charts/&lt;profile&gt;/...html |
| Active customers (Apr)   | 12,450     | 11,920     | -4.3% | <artifacts_dir>/local/charts/&lt;profile&gt;/...html |

Cell formatting:

  • Numbers carry the same currency / magnitude suffix as the input where unambiguous (echo the user's raw_value for Expected, format Got with the same shape).
  • Δ is the signed percent (red wins emphasized in chat by ✗ prefix if Markdown rendering allows; otherwise plain text).
  • Drill-down links to the chart-template HTML for that query. Open these to see the full receipt — that's where the definitional disagreement lives.

For each mismatch row, surface a one-line interpretation under the table:

Q3 2025 Revenue — agami reports $3.89M, your dashboard says $4.2M (-7.4%). Open the receipt; the metric revenue here is gross of refunds in USD at invoice date. If your dashboard nets refunds, that's the gap.

This is where the trust win lands. The DE doesn't have to chase the disagreement — the receipt + your interpretation does it for them.

3b.5 — Your statements (where the evidence itself fell short)

Only when a row carried a statement, and only for the parts that did not grade confirmed. One rule throughout: no SQL in chat; the part's name and its note are enough, and findings.json holds the rest. Three blocks, because the three kinds of part answer three different questions, and a reader scanning one table counts every row as a problem with the query. The table holds the grades that judge; the two blocks under it hold what was not judged.

### Your statements

| Label | Checked | Grade | What the ledger found |
|---|---|---|---|
| Delivered orders | value orders.status = 'Delivered' | a mistake in your query        | not one of the values the semantic model lists, and no row holds it; did you mean 'delivered' |
| Paid revenue     | join orders to payments           | a mistake in your query        | the join is on a different key than the one the semantic model declares |
| Q3 Revenue       | default filter on orders          | the semantic model is missing this | the semantic model declares this filter and the statement does not apply it |
| Open items       | value list for items.state        | the semantic model is missing this | the column holds 6 distinct values and the semantic model lists none of them |

**What couldn't be checked**
- Q3 Revenue, double counting in SUM(total): the pre-flight could not bind this aggregate to one table: a column inside the aggregate could not be attributed to one table
- Orders placed, answers the question: the statement may not answer the question: the question asks how many orders were placed and the statement counts order items; reword the question or the statement and re-run this row

**What this run noticed**
- Open items, rows the join leaves out, items to users: 3 of 8345 items rows have no users partner and are dropped by this inner join; counted over the whole table, before the statement's own filters; rows of users with no items partner were not counted

**What the semantic model says in words**
- items.state, column caveat: "open is state NOT LIKE 'Closed%'"
- items, table caveat: "pending fulfillment is state IN ('Open','Pending','Work in Progress')"
- example "How many pending items?": WHERE state IN ('Open','Pending','Work in Progress')
- Two of these name different values for items.state. The semantic model disagrees with itself here; which is right is the person's call.

Say the kinds apart in one sentence each: a query_defect is the person's to fix, and nothing about agami changes because of it; a model_gap is a place the data proved their statement right where the semantic model is missing or wrong, and a single fix still goes through /agami-save-correction (an undeclared value list is a field's choice_field, the field_metadata route). An unresolved part is neither: it says what could not be checked and why, and it sits in its own block so nobody counts it as a defect. Every grade and every part name already has its words in reconcile.py (_STATUS_WORDS, _STATE_WORDS, _PART_WORDS, _PART_KEYS), which is what the report page renders; use those, so the table in chat and the card on the page call the same thing the same name. Say every one of these in the words of shared/plain-language.md

Files (agami-core)
  • SKILL.md 83.1 KB
    ---
    name: agami-reconcile
    description: "Reconciles known (label, expected_value) numbers from an existing dashboard against agami's answers. Input can be a SCREENSHOT of a Metabase / Power BI / Tableau / Looker dashboard (Claude's vision extracts the pairs), a CSV, or numbers pasted inline — the user doesn't need to know which; they can just ask. For each pair, the skill generates a matching NL question, runs it through the active profile's semantic model, diffs actual vs expected, and surfaces matches in green and mismatches in red with drill-down receipts. The strongest onboarding demo for a skeptical data engineer — either we agree with their numbers (trust earned via evidence) or we surface a real definitional disagreement (trust earned via transparency)."
    when_to_use: "Use when the user says 'reconcile against this dashboard', 'do these numbers match?', 'validate against my Tableau export', '/agami-reconcile <csv>', drops a screenshot of a BI dashboard (Metabase/Power BI/Tableau/Looker/spreadsheet) and asks agami to reproduce the numbers, or pastes a CSV / table of known numbers. Also use when they hand over SQL they trust ('here is the SQL behind each tile', 'validate this query part by part', 'audit this SQL', 'here is the query we trust, check it') or a list of questions with no answers ('check these questions'); the person's query is graded part by part against the semantic model and the warehouse, never taken as the answer. Also use after a run, when the user says 'keep these as golden questions' or 'promote these to a golden dataset' — the rows that agreed are split between the examples agami reads when answering and the answer key later runs are scored against. Requires agami-connect to have been run first (need a semantic model + examples library). A high-leverage validation surface for a skeptical data team — reproduce their dashboard numbers, or surface the definitional gap."
    argument-hint: "<screenshot | path-to-csv | pasted numbers>"
    ---
    
    # agami reconcile
    
    You are running the reconciliation harness. Goal: take the evidence a person brings and prove agami can reproduce each answer. That evidence is labeled numbers from an existing dashboard (Tableau / Looker / Mode / Metabase / Power BI / spreadsheet), most often a **screenshot**, sometimes a CSV or pasted list; or the SQL they trust; or a list of questions. When numbers match and every part of the person's statement checks out, that's evidence the semantic model is right. When they don't, the receipt drill-down and the part ledger explain why: a definitional disagreement (gross vs net, refunds in vs out, FX rate at booking vs reporting date), a join the semantic model is missing, or a mistake in the person's own query. That is exactly the trust signal that makes a DE relax.
    
    **The person's query is evidence, never the answer.** Every part of it is graded against the semantic model and the warehouse before anything is compared or kept.
    
    This skill orchestrates:
    
    1. **Extract** the evidence rows from the input — a dashboard screenshot (via vision, confirmed with the user), a CSV, a pasted list, SQL the person trusts, or a list of questions. Number parsing is always deterministic (`reconcile.py`).
    2. **Grade a supplied statement** part by part: run it the way agami runs its own SQL, then grade its joins, typed values, required filters and aggregates (Phase 1.5).
    3. **Generate a matching NL question** for each label that has none.
    4. **Run** each question through the same NL→SQL→execute pipeline as agami-query.
    5. **Compare** actual vs expected with a tolerance, as one number or as a table, and name the part that differs.
    6. **Present** a markdown table with per-row status; for mismatches, render the full receipt as a drill-down; for the person's statements, the parts that fell short. Write the findings to disk.
    
    Spec for the deterministic helpers: [`scripts/reconcile.py`](../../scripts/reconcile.py) (input reader, number normalization, diff with tolerance, the part ledger and the findings). The shared procedure lives in [`shared/evidence-row.md`](../../shared/evidence-row.md), [`shared/part-ledger.md`](../../shared/part-ledger.md) and [`shared/statement-check.md`](../../shared/statement-check.md).
    
    ## Conversation style
    
    - **Tight loops.** This skill is a tool, not a tutorial. One question per turn, max two sentences of prose between phases.
    - **Surface mismatches loud.** A reconcile run with 9/12 matches and 3 mismatches is a SUCCESSFUL run — the mismatches are the value. Lead with what didn't match.
    - **Don't paste raw SQL in chat.** The receipt has it. Same hard rule as agami-query — with one exception, Phase 3e's promotion offer, where the statement is shown because it is the thing being accepted into an answer key and cannot be hidden behind a receipt link at the moment somebody agrees to replay it.
    
    ---
    
    ## Phase 0: Preflight
    
    Same checks as agami-query / agami-connect:
    
    1. **Plan-mode check** per [`shared/plan-mode-check.md`](../../shared/plan-mode-check.md). This skill needs Bash + Read + Write — refuse if locked in plan mode. **DO NOT write a plan file. DO NOT call `ExitPlanMode`.** Refusal text: *"I can't reconcile in plan mode — each row runs a live query and writes a receipt. Switch to **Auto** or **Edit Automatically** mode (Shift+Tab to cycle) and re-invoke me with the same input."*
    2. **Credentials present** — read `<artifacts_dir>/local/credentials` for the active profile. If missing, invoke `/agami-connect` to set up first; this skill needs a working DB connection.
    3. **Model present** — `<artifacts_dir>/<profile>/datasource.yaml` must exist. If not, invoke `/agami-connect`. This skill needs an introspected model to generate questions against.
    4. **Input — accept any of four shapes, or a mix; the user needn't know which.** Detect what they gave:
       - **A screenshot / image** of a dashboard (Metabase, Power BI, Tableau, Looker, a spreadsheet) — the common case. Go to Phase 1's **vision branch**.
       - **A CSV** — a path in `$ARGUMENTS`, or pasted inline (write inline CSV to `/tmp/agami-reconcile-<ts>.csv`). Go to Phase 1's **CSV branch**. A third column holding SQL is the statement behind each tile: Phase 1n reads it as a statement, never as part of the label.
       - **Numbers pasted inline** as a list/table — treat as inline CSV.
       - **SQL the person trusts** — one or more statements, pasted or in a `.sql` file, alone or beside the question each answers. Go to Phase 1's **statement branch**. The statement is evidence, never the answer: Phase 1.5 grades every part of it before it is compared with anything.
       - **A list of questions** with no answers — one per line. Go to Phase 1's **questions branch**.
       A screenshot and the SQL behind its tiles may arrive together; Phase 1n joins them by label. A filled `.csv` in `<artifacts_dir>/local/reconcile/inbox/` is the CSV branch: check the inbox before asking, so a person who filled the template and came back to say **reconcile** is not asked again. **If they gave nothing** (or just asked "can you check my dashboard?"), ask once the way `agami-connect` asks which database: **AskUserQuestion**, the four shapes as options, the lowest-friction one first, and the rarer inputs named in the prompt so they are visibly welcome (*"Something else, a pasted `label: value` list or a JSON export? Choose **Other** and paste it."*).
    
       | label | description |
       |---|---|
       | `A screenshot of the dashboard` | Metabase, Power BI, Tableau, Looker, a spreadsheet: whatever you have. I read the tiles and confirm what I read with you before anything runs. |
       | `A CSV or an export` | Two columns, label and value, or the template I can write for you (label, value, sql, question). |
       | `The SQL you trust` | One or more statements, pasted or in a `.sql` file. Each is graded part by part before anything is compared. |
       | `A list of questions` | One per line. Agami answers each, the run checks the query behind each answer, and you decide on the report page. |
    
       **When they pick the CSV and have nothing to hand, write the template and hand off**, the way connect writes `credentials.example`: with the Write tool, `<artifacts_dir>/local/reconcile/inbox/reconcile.example.csv`:
       ```
       # One line per number to check. Keep the header; these # lines are skipped.
       # label: the tile's name.  value: the number as shown ($4.2M, 12,450, 3.1%).
       # sql: the SQL behind it, if you have it.  question: the question in your words (optional).
       label,value,sql,question
       ```
       Then: *"Fill in `local/reconcile/inbox/reconcile.example.csv`, one line per number, and come back and say **reconcile**. Nothing runs until you do."* **End the turn.** On re-entry, Phase 1n reads every `.csv` in the inbox through `reconcile.py intake`; a file still holding only the header and comments is reported as empty (exit `4`) and the person is asked again, never guessed at.
    5. **If they gave a file path, validate it exists.** If not, surface the error and stop.
    
    ---
    
    ## Phase 1: Extract the (label, value) pairs
    
    Whatever the input shape, the goal is the same normalized rows JSON. **Number parsing is always deterministic — it goes through `reconcile.py`, never the LLM eyeballing a value** (a misread expected number would manufacture a false mismatch on a verification surface).
    
    ### Vision branch — a dashboard screenshot
    
    1. **Read the image** and extract every labeled number you can see — KPI tiles, table cells, chart value labels — as `(label, raw_value)` pairs. Keep the label the user would recognize ("Total Revenue", "Active Users — Apr"), and the value **exactly as shown, verbatim** (`$4.2M`, `₹2.16Cr`, `42%`, `1,234`) — don't convert it; the normalizer does that.
    2. **Write the pairs as a 2-column CSV** (Write tool — never a heredoc/`python3 -c`) to `/tmp/agami-reconcile-<ts>.csv`, then run the SAME normalizer as the CSV branch (below) so value parsing stays deterministic.
    3. **Confirm before reconciling — vision can misread.** Show the extracted pairs as a small table and ask the user to fix any misread label/number: *"I read these N numbers off your screenshot — correct anything I got wrong, then I'll reconcile."* This confirm step is **mandatory**: a wrong expected-value isn't a model bug but it reads like one. If a tile is ambiguous or partly cut off, say so and skip it rather than guess.
    
    ### CSV branch — a CSV path or inline-pasted CSV
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" intake --file "<csv_path>" \
      --source "<the person's own words for where this came from>" > /tmp/agami-reconcile-rows-<ts>.json
    ```
    
    The helper (used by **both** branches) handles:
    - Header detection (with-or-without first-row column names)
    - 2-column or 3+ column inputs (3rd onward are appended to the label as context)
    - Currency symbols / magnitude suffixes / accounting parens / percent (`$4.2M`, `₹2.16Cr`, `(123.45)`, `42%`)
    - Null sentinels (`n/a`, `—`, blank)
    
    Read the JSON. Each row is `{label, expected_value, raw_value}`. Discard rows where `expected_value` is null (unparseable) — surface a one-liner: *"Skipped 2 rows where the value couldn't be parsed: 'X', 'Y'."*
    
    ### Statement branch — SQL the person trusts
    
    If the SQL was pasted, write it to `/tmp/agami-reconcile-<ts>.sql` with the Write tool (several statements separated by `;`); if it came as `question,sql` pairs, write those as a CSV. Take a file path as given. A statement that arrives without a question needs one before agami can be asked the same thing: derive the question the statement answers, show it, and let the person correct the wording. **Do not run the statement here.** It is evidence, and Phase 1.5 is where its parts are graded.
    
    ### Questions branch — a list of questions and no answers
    
    Write the lines to `/tmp/agami-reconcile-<ts>.txt` with the Write tool, one question per line. These rows carry no expected value: Phase 2 asks agami each question, and until the person grades the answers there is nothing to compare against. Say so up front: *"No answers to compare with, so I'll ask agami each one, check the query behind each answer, and you decide from there."*
    
    ### 1n — Normalize every input into evidence rows
    
    A filled template in `<artifacts_dir>/local/reconcile/inbox/` is one of the files here, passed with its own `--file`.
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" intake --file <path> [--file <path> ...] \
      --source "<the person's own words for where this came from>" > /tmp/agami-reconcile-rows-<ts>.json
    ```
    
    `parse` stays the number-only reader; `intake` reads everything `parse` reads and the other shapes too, so a run with any SQL or questions in it goes through `intake` for all of its files, the vision CSV included. One row per thing to check, `{label, question, statement, expected, raw_value, provenance}`, with any of the three evidence fields missing. Several files merge by label under a case-and-whitespace fold, so the SQL behind a tile joins the tile's row. Exit `2` is a file that could not be opened; exit `4` is an input with no question, statement or number anywhere. The shapes, the detection rules and the row are in [`shared/evidence-row.md`](../../shared/evidence-row.md).
    
    Create the run directory `<artifacts_dir>/local/reconcile/<ts>/`, and `rows/<n>/` under it for every row that carries a statement.
    
    **Show what was read before anything runs, and hand off**, the way `agami-connect` shows the prune page before it introspects. The intake page lists every row: the question we will ask agami (read from a label, and editable), the number expected, whether SQL came with it, and which file and line it came from. The person fixes a question we read wrong or unticks a row, generates the block, and pastes it back. It is the same design language and the same paste-back grammar as the report page.
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/render_reconcile_intake.py" --title "What we read · <profile>" \
      --profile <profile> --run <ts> --intake-file /tmp/agami-reconcile-rows-<ts>.json \
      --out "<artifacts_dir>/local/reconcile/<ts>/intake.html"
    ```
    
    Then say it in two lines and **end the turn**:
    > I read `<N>` rows from `<the screenshot / csv_path / the statements / the questions>`: `<n>` numbers, `<m>` with your SQL, `<k>` questions. Open the page, fix any question I read wrong or untick a row, and paste the block back; then I run. Typically `<N> × 5–15s` per row.
    
    **On re-entry** (the block arrives, `profile:` / `reconcile-run:` / `intake:` / one JSON array / `done`), never hand-edit the rows: pipe the block to the parser, which applies it to the rows file and says what it did:
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/parse_reconcile_intake.py" --block-file /tmp/agami-reconcile-intake-<ts>.txt \
      --rows-file /tmp/agami-reconcile-rows-<ts>.json --run <ts> --out "<artifacts_dir>/local/reconcile/<ts>/intake.json"
    ```
    
    `intake.json` in the run directory is the run's own copy of what runs: Phase 2 reads the next rows from it, a resume on a later day reads it, and nothing under `/tmp` is needed again. `ok: true` with `kept`, `dropped` and `edited` counts means it holds exactly what runs; an edited question carries `provenance.question_from`, so it is never mistaken for one we read. A `needs_judgment` (another run's block, a row the file does not have, a question that is not text, a missing section) applies nothing: ask for the block again. If the person answers in chat instead ("looks right, go ahead"), run with the rows as read: Phase 2's first `next-chunk --rows-file` call copies them into the run directory unchanged. A per-row "is this the question?" in chat is never asked.
    
    ---
    
    ## Phase 1.5: Grade a supplied statement, part by part
    
    For every row that carries a `statement`. Skip this phase for a row that does not.
    
    **The person's statement runs through the guard agami's own SQL runs through, and never on a command-line tier.** [`shared/statement-check.md`](../../shared/statement-check.md) is the procedure, step by step, and [`shared/part-ledger.md`](../../shared/part-ledger.md) is what each file means. Write `statement.sql` in `rows/<n>/` first, verbatim, then check the row with one call:
    
    ```bash
    "$PY" "$AGAMI_PLUGIN_ROOT/scripts/check_statement.py" --profile <profile> --area <area> \
      --row-dir "<artifacts_dir>/local/reconcile/<ts>/rows/<n>"
    ```
    
    Every step that reaches the database runs inside that script, and every statement it sends goes through `execute_sql`'s guarded chokepoint with the built-in executor, never with `--no-safety`. psql, mysql, snowsql, sqlite3 and DuckDB are never used here, whatever tier the profile queries on: they have no read-only gate, no scope gate and no bound, and a statement that writes must not meet a database through them. **Never run the statement or a probe yourself.** Exit `0` means the row was checked, whatever the statement did; exit `3` stops the run (below); exit `2` means it could not start, and `statement-check.md` says why; any other exit is a crash, which stops the run too. Checking a row again is the same call, and it first clears every file the last check wrote. What the script does:
    
    - **1.5a — Is it one read-only SELECT?** The guard's own read-only gate reads it first, per [`shared/sql-generation-rules.md`](../../shared/sql-generation-rules.md), and then its recon gate, which refuses a call that reads the server's own metadata (its version, the session's identity, a privilege check). A statement either gate refuses gets `status: "refused"` in `run.json` and the gate's `rule` (`read_only` or `recon`); nothing runs after it, and nothing is probed. Mark the row `error`.
    - **1.5b — Run it the way agami runs its own.** The zero-row check, then `sm prepare`, then the statement through the guard: its result to `statement.csv`, and into `run.json` the `status`, `exit`, classifier `kind`, guard `rule`, `detail` and `remediation`, never the raw error text. **A refusal is a finding, not a crash**: a `table_scope` or `column_scope` refusal grades `scope: model_gap`, because the person wanted a table or column the semantic model does not expose; `select_star` grades `runs: query_defect`. Never rewrite the statement and never retry. Other failures carry the kind [`shared/db_error_classifier.md`](../../shared/db_error_classifier.md) names; `auth`, `dsn`, `network`, `permission` and `driver_missing` exit `3`, and so does an `engine_mismatch` refusal, which means the semantic model declares an engine its credentials do not connect to. Exit `3` stops the run as `agami-query` Phase 3b stops it: tell the person the `remediation` from `run.json`. Nothing in this phase writes `query_log.jsonl`: `agami-save-correction` reads that log's last successful line as the question to correct, and a probe there would be corrected instead of the answer. The phase keeps its own record instead: `run.json` for the statement, `zero-row.run.json` for its wrap, and a `.run.json` beside every probe's CSV, so every execution and every refusal here is written down. A wrap the guard refuses is that check not run, never a fault in the statement, and the statement is still checked on its own.
    - **1.5c — Its receipt, and the semantic model's words.** `sm receipt` to `statement-receipt.json`, and beside it `sm mentions` to `mentions.json`: every description, caveat, glossary line, narrative paragraph and prompt example that mentions a table or column the statement reads. The ledger puts those words beside any part that falls short, so the caveat that shaped the SQL is read next to the number that went wrong. Quoted, never graded.
    - **1.5d — Probes.** `sm join-probes` and `sm filter-values plan` emit SQL; each probe is written to its own `.sql` file, and the row's probes run as one plan through `execute_sql`'s batch door (`statement-check.md` step 8): the semantic model is resolved once and the connection kept open, every probe still through the guard on its own, each to the CSV `part-ledger.md` names with a `.run.json` beside it. The near-miss probe for a typed value runs only when its `exists` probe counted 0. Then `sm filter-values judge`. A probe the guard refuses or the database fails leaves an empty CSV; leave it there, the ledger reads it as a probe that failed. A file the ledger expects and does not find, or finds empty, is a part it grades `unresolved`, never clean.
    
    After the script, three steps are yours. 1.5g is a judgment made by reading, and no script makes it.
    
    - **1.5e — The ledger, run once.** `python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" ledger --row-dir rows/<n>` writes `ledger.json`: one grade per part, `confirmed`, `model_gap`, `query_defect` or `unresolved`, and the weakest grade as the row's `ledger_verdict`; a part may also be `noted`, a fact the run states and never judges, which never decides the verdict. **Run it once per row, in Phase 2e, after the comparison**, with `--with-claims` when agami's statement exists too and without it when agami's run failed. The files this phase wrote are what it reads, so nothing is lost by waiting, and a ledger written here and again later is the same ledger twice. A part reaches `model_gap` only by measurement; the statement asserting something is never the evidence for it.
    - **1.5f — Its result is the expected value.** For a row that came with no number, `expected` is the single cell `statement.csv` returned (its text folded to a number by `reconcile.py diff`, which reads `$4.2M` and `47,238,221.00` alike), or the table's shape when it returned several rows. For a row that came with a tile number too, run `reconcile.py diff` between the tile and the statement's own result: a disagreement means the statement is not the tile's statement, or the data moved; flag the row in Phase 3b.5 and keep the tile's number as `expected`.
    - **1.5g — Does the statement answer the question?** For every statement row, write `question_fit.json`; when the row carries a question, read the two side by side first, before anything is compared. Doubtful when the grain differs (a count of items for a question about orders), the measure differs (revenue for a question about a count), a filter is present the question never asked for or absent when it did, or the time window differs. Write `question_fit.json` in the row directory: `{"fit": "plausible" | "doubtful" | "no_question", "reason": "<one sentence, or null>"}`, with `no_question` only for a statement that came alone: against a row that carries a question it is a contradiction, and the findings verb refuses to keep such a row. This is a judgment made by reading, the one part of the ledger that is; it can withhold a row from the keep-offer and never proves anything about the semantic model. A doubtful row grades `match_unverified` at best, shows in Phase 3b.5 with the reason, and the person settles it by rewording the question or the statement and re-running that row. Phase 3e keeps a `match` row as a worked example, which teaches the AI a question-to-SQL pairing, and a sound statement paired with the wrong question is the most harmful thing that step could keep.
    
    ---
    
    ## Phase 2: Generate questions + execute
    
    **Work five rows at a time.** `rows.jsonl` is the run's checkpoint: Phase 2d appends one record per finished row, and the next rows to run are read from it, never chosen by hand:
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" next-chunk --run-dir "<artifacts_dir>/local/reconcile/<ts>" \
      --rows-file /tmp/agami-reconcile-rows-<ts>.json    # seeds the run's intake.json once; later calls need only --run-dir
    ```
    
    Exit `0` hands back `chunk`, the next five rows not yet in `rows.jsonl`, with `finished`, `remaining`, `chunk_index` of `chunks_total`, and `progress`, the counts by status over the rows done so far, read from the checkpoint and never tallied by hand. Take the five through Phase 1.5 (statement rows) and 2a to 2f below; each lands in `rows.jsonl` as it finishes. When the chunk is done: build the report items for every row finished so far and render the report page (3a.5), so the person can already read the first rows, say one progress line, and **end the turn**:
    
    > Rows 6 to 10 of 50 done. So far: `<progress as colored words, in 3a's wording>`. Page: `<artifacts_dir>/local/reconcile/<ts>/report.html`. Say **continue** for the next five, or **continue all** to run the rest without stopping.
    
    `continue` calls `next-chunk` again. `continue all` runs chunk after chunk, a progress line per chunk and no stop, until exit `4`. Exit `4` means every row is in the checkpoint: go to Phase 3. **A run interrupted anywhere resumes with the same call**: a row in `rows.jsonl` is never run twice, and "resume the reconcile" on a later day is `python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" resume --reconcile-dir "<artifacts_dir>/local/reconcile"`, which names the newest run directory that still has rows remaining (exit `4` when none does), then `next-chunk` on it. Exit `2` is a refusal and stderr says which: a run directory or rows file that is not there, a rows file that is not a list of row objects, or a checkpoint line that cannot be read. When it names a `rows.jsonl` line, fix or remove that line before continuing, never skip it, since skipping would run its row again. The first five are a smoke test: a wrong profile, a misread file or a question in the wrong words is caught at five rows, not fifty. The keep-offer stays one per run (3e), never one per chunk.
    
    For each row in the chunk:
    
    ### 2a — Generate the NL question
    
    A row that already carries a `question` (a `question,sql` pair, or a statement whose question the person confirmed in Phase 1) uses it as written; do not generate a second one. Otherwise use the LLM to translate `label` (+ context if present) into the most natural English question whose answer should be `expected`. Examples:
    
    | label | question |
    |---|---|
    | `Q3 2025 Revenue` | "What was total revenue in Q3 2025?" |
    | `Active customers (Apr 2026)` | "How many active customers did we have in April 2026?" |
    | `Pipeline value (open opps)` | "What's the total pipeline value across open opportunities?" |
    | `Mean order size last 30 days` | "What's the average order size over the last 30 days?" |
    
    The semantic model + examples library are loaded; let the LLM pick the right subject areas / entities / metrics that resolve the labeled term to a concrete query.
    
    ### 2b — Run via the agami-query pipeline
    
    **Agami's answer comes from a cold client, never from this session.** This session holds your statement, the ledger's grades and the fit reasoning; anything it wrote would not be what a person gets from agami. Ask the way the golden run asks, once per chunk, with the five rows `next-chunk` handed back:
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/run_golden_eval.py" --profile <profile> --via mcp \
      --ask-file /tmp/agami-reconcile-chunk-<ts>.json --out-dir "<artifacts_dir>/local/reconcile/<ts>/rows" --parallel 4
    ```
    
    `--ask-file` takes `next-chunk`'s output as it is (its `chunk`), and spawns the operator's own client per question, several at a time. `--via mcp` is what makes the answer a person's answer: each client is served agami's own MCP tools and nothing else, so it calls `get_datasource_schema` and `get_prompt_examples` itself, scoping them as it sees fit, and runs its own statement. That is the point of asking this way. A run that pre-fetched one schema into the prompt would never exercise the things that decide whether a real question succeeds, and could pass on a semantic model nobody could query.
    
    **It probes as much as it likes, and a query that did not answer ends the row.** Successful queries are free, and how many a row needed is the measurement: `probe_count` reads how much the semantic model failed to say up front. But the first query the database rejects or the safety check blocks stops the row, and the row's `error` is what the safety check or the database said about it. That is not a run that went wrong, it is the finding: a statement that did not run says the semantic model names something that is not there, and a client left free to retry would bury exactly that. The rule is enforced by the server, so it cannot be half-obeyed.
    
    `--via context` is the older way, kept for comparing the two surfaces against one intake: the run pre-fetches one schema into the prompt and the client gets no tools. Each row's answer file records which ran, as `mode`, because the two measure different things.
    
    Each row's file carries `rows/<n>/agami-answer.json` as `{row, question, sql, statements, error, mode, probe_count, probes, client_value}` (the last three only under `--via mcp`, where there is a trace to carry): `statements` is every statement the client wrote, in order, and `sql` is the last of them, the one whose result answers the question. What is reused across the chunk is what does not depend on the question; the session itself is never reused, because a fresh one is the thing being measured. One question at a time is `--ask "<question>" --out rows/<n>/agami-answer.json`. Per row, exit `0` for the batch, or a `sql` in the row's file, carries a statement: write it verbatim to `rows/<n>/agami.sql` (only `sql`; when `statements` has more than one, keep them all in the row record's `agami_statements` for the page and never run the earlier ones: the read-only rule refuses anything but a SELECT, so an earlier statement can only be a look at the data). **Under `--via mcp`, do not run it yourself:** the run has already written `rows/<n>/actual.csv` and `rows/<n>/agami-run.json`, in code. A statement the server did not run is never run again (its outcome is copied from the trace), and one that ran is run once more, only for its result, through `execute_sql`'s guarded envelope. No command-line tier is involved, and what runs is the server's own record of the statement, never the client's copy of it. Under `--via context`, run it through the profile's tier exactly as `agami-query` Phase 1e tabulates it, by file and never with `--no-safety` (stdout to `rows/<n>/actual.csv`, its exit to `rows/<n>/agami-run.json` in `run.json`'s shape). Then `sm receipt --sql-file rows/<n>/agami.sql` and the chart report, as agami-query Phase 3 does. A row whose file has no `sql` carries one of the generator's fixed sentences as its `error` (the client could not be started, timed out, exited without answering, answered without a statement, or, under `--via mcp`, reported a number it did not query or a statement it did not run); the batch exits `3` when any row is like that. A row whose query DID NOT RUN (the safety check blocked it, or the database rejected it) is different and keeps its statement, with what the safety check or the database said as its `error`. That row is `error` with the sentence as its `error`. **Never write agami's SQL yourself, and never retry with your own wording**; a row with no cold answer is an error row, and that is the finding. Exit `2` means the run could not start: the profile's context could not be built, or, under `--via mcp`, agami's tools did not come up on this machine. Stop the run and say which. A tool surface that will not start is broken tool fetching, which is a finding about the deployment — never answer the rows another way and print the numbers under agami's name.
    
    Capture, per row:
    
    - The generated SQL, verbatim, from `agami-answer.json` (its `sql`; and its `statements` when the client wrote several)
    - The result (one cell, or the columns and a row count)
    - The full chart-template HTML report (so the user can drill in for mismatches)
    - The trust receipt (with confidence, signed-off-by, etc.)
    
    The SQL you capture here is the one that lands in the row record (Phase 2d) — keep it verbatim. It is the only place the statement survives the run: the chart report at `report_path` is HTML, and nothing re-derives the statement from it afterwards.
    
    If the SQL fails OR the result isn't a single scalar (e.g., the LLM-generated question returned a multi-row table), capture an error: `Could not extract a single scalar from the result.` These rows show up as `error` status in the report. One case is different: a row that carries the person's `statement` may legitimately return a table. Keep `recorded` as `{"columns": [...], "rows": []}` for it (the columns, never the rows), write the result CSV to `rows/<n>/actual.csv`, and let Phase 2e compare it as a table.
    
    ### 2.5 — Check agami's query when there is nothing to compare against
    
    Only for rows that carry a question and neither a statement nor an expected value (the questions branch). Agami has answered each one in 2b and there is no number to diff against. **Two things are true of such a row, and the phase does one each.** Whether the answer is right is the person's call, because nothing here can measure it. Whether the query behind it holds up against the semantic model is not a matter of opinion, and this phase measures it.
    
    1. **Grade agami's query, part by part.** Take agami's statement from 2b through **Phase 1.5** exactly as a supplied statement goes through it, writing it as `statement.sql` in `rows/<n>/`: the files name the statement under test, whoever wrote it. Three differences, and only three:
       - **1.5f does not apply.** Agami's own result never becomes `expected`. A row whose expected value came from the query being graded compares agami against agami, proves nothing, and reads on the page as a match. This is the never-ground-truth rule at its sharpest: the side being checked cannot supply the answer key.
       - **1.5e runs without `--with-claims`.** Claims compare two statements and there is only one.
       - **1.5g still runs**, and it is the most useful check on the page: whether agami's query answers the question asked is exactly what the person is about to judge.
       A row where agami's run failed has no query to check; it skips this phase and says so.
    2. **A grade that falls short names agami, not the person.** `query_defect` on this row means agami wrote a query with a mistake in it, which the person fixes by teaching agami, not by editing anything of their own; `model_gap` is a gap in the semantic model whoever tripped on it. The card's words follow from the ledger and the row having no statement, so nothing here is written by hand.
    3. **Report it on the reconciliation report**, the one page every path ends on (3a.5). The row's status is `ungraded`: its verdict says agami answered and the call is the person's, and carries what the checks found so the call is an informed one. The card's three sections are the same three: **Data** shows up to five of agami's rows, because an answer nobody can see cannot be judged; **SQL** shows agami's query, and says it is the only one written for this row; **Checks** is the ledger just run. The decisions are the same six, and the person chooses `example` when agami's answer is right (it sends agami's query for this question to `/agami-save-correction`, so agami writes it that way again), `change` when a definition is wrong, `reword` when the question was read differently, and `nothing` to leave the row as it is. `fix` is not offered here: it means "fix your query", and on this row the person wrote none.
    
    
    ### 2c — Diff
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" diff \
      --expected "<expected_value>" \
      --actual "<actual_value_from_query>" \
      --tolerance 0.01 > rows/<n>/diff.json
    ```
    
    The diff is written to the row directory, where the record verb (2d) reads it.
    
    Default tolerance: ±1%. The user can override with `tolerance=N%` in their original ask (e.g., "reconcile with 5% tolerance"). Tolerance applies to numeric comparisons; for text values (rare), use exact match.
    
    Capture: `match` (bool), `delta`, `delta_pct`. When `expected` came from running the person's statement (Phase 1.5f), diff against it exactly the same way; the statement's result is the expected value, and its grades say how far to trust it.
    
    ### 2d — Build the row record
    
    Per row:
    
    ```json
    {
      "label":        "<from CSV>",
      "question":     "<LLM-generated NL question>",
      "expected":     <number>,
      "actual":       <number or null if errored>,
      "delta_pct":    <signed fraction or null>,
      "match":        true | false,
      "status":       "match" | "match_unverified" | "mismatch" | "expected_doubtful" | "error" | "ungraded",
      "report_path":  "<artifacts_dir>/local/charts/<profile>/<ts>.html",  // the full chart report for this query
      "sql":          "<the statement agami wrote, kept even when it failed; null only when none was written>",
      "recorded":     {"columns": ["<column name>"], "rows": [[<value>]]},  // what the query actually returned
      "error":        "<message if status=error, else null>",
      "provenance":   {"shape": "a|b|c|d", "source": "<the person's words>", "file": "...", "line": 2},
      "statement":    "<the person's SQL, verbatim, or null>",
      "statement_recorded":     {"columns": ["..."], "rows": [[<one cell>]]} | {"columns": ["..."], "row_count": 12} | null,
      "statement_receipt_path": "<rows/<n>/statement-receipt.json, or null>",
      "receipt_path": "<the receipt of agami's own statement>",
      "ledger":       <the ledger.json object from reconcile.py ledger: its rows, verdict and counts> | null,
      "ledger_verdict": "confirmed" | "model_gap" | "query_defect" | "unresolved" | null,
      "comparison":   {"scalar": <the diff>} | {"result_set": <the compare-results score>} | null,
      "claims":       <the sm claims diff between the two statements, or null>,
      "finding_keys": ["<keys of the findings this row contributed to>"],
      "agami_statements": ["<every statement the client wrote, in order; sql is the last>"]  // only when there were several, else []
    }
    ```
    
    The keys from `provenance` down are appended after `error` and every earlier key keeps its meaning; a reader that only knows the older shape keeps working. `status` gains three values beside the three it had. Their rules, applied by code and never by feel:
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" status --match <true|false|none> --ledger-verdict <verdict|none>
    ```
    
    | Status | When |
    |---|---|
    | `match` | the numbers match, and every graded part is `confirmed` (or there was no statement to grade) |
    | `match_unverified` | the numbers match, but a part of the person's statement is not `confirmed`. Phase 3e never sees it: a match nobody could verify may be luck. A query written differently from agami's is not this: the claims comparison notes it (`noted`, "same answer, different query" on the page) and the row stays `match`. A table that matched only because a column of yours paired with a differently named column of agami's, where one value fills most of your column's rows, is this: the two could be different columns holding the same values, so the ledger's `value_pair` part stays open and the card names both columns |
    | `mismatch` | the numbers differ and the person's statement has no `query_defect`, so agami is the likelier culprit |
    | `expected_doubtful` | the numbers differ and the person's statement has a `query_defect`, so the expected value itself is in doubt. Kept out of the mismatch tally |
    | `error` | the row could not run |
    | `ungraded` | agami answered and there was nothing to compare against: no statement of the person's and no number. Nothing failed and nothing has been decided, so the row waits for a person on the report page (Phase 2.5). Its expected value never comes from the run |
    
    `sql` is the statement captured in Phase 2b, written down verbatim. `recorded` is the result it returned, shaped as `columns` + `rows` — the same two keys the golden-dataset receipt uses — so whoever picks this row up later forwards it as-is instead of rebuilding it from a number and guessing at a column name.
    
    **On a `status: "error"` row `recorded` is `null` and the statement is kept.** An error row carries nothing a later reader could mistake for a verified ANSWER, which is what `actual`, `recorded` and `delta_pct` are; a statement that failed cannot be mistaken for one that answered, because the row's own error sentence says it did not, and reading it is how a person tells a semantic model declaring a column the warehouse lacks from a query agami wrote wrong. `sql` is `null` only when agami wrote no statement at all.
    
    **Build the record with the verb, never by hand:**
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" record --run-dir "<artifacts_dir>/local/reconcile/<ts>" --row <n> [--tolerance 0.01] [--report-path <path>]
    ```
    
    It reads the row directory (`intake.json` for the label, the question, your statement and the number you gave; `agami-answer.json`, `agami-run.json` and `actual.csv` for agami's side; `statement.csv`, `diff.json` or `comparison.json`, `claims.json` and `ledger.json`) and appends the record above to `rows.jsonl`, replacing an earlier record for the same row. Exit `2` names the file it needs and cannot find: fix the run, never type the record. A question-only row is recorded `ungraded` rather than refused or written as `error`: nothing failed, and it waits for a person on the report page. Its `expected` stays empty on purpose, and the verb takes no argument that could fill it: a value the run supplied would be agami's own answer coming back as its own answer key. The keys are additive — a reader that only knows the older shape keeps working.
    
    ### 2e — Compare, as a number or as a table, and name the part that differs
    
    For a row whose `expected` is one number, Phase 2c's diff is the comparison. For a row whose statement returned a table, compare the two result CSVs through the golden comparator, so a table-shaped answer is judged the way an answer key is:
    
    ```bash
    bash "$AGAMI_PLUGIN_ROOT/scripts/sm" compare-results "$ROOT" \
      --golden-csv rows/<n>/statement.csv --generated-csv rows/<n>/actual.csv \
      --match values --unordered > rows/<n>/comparison.json
    ```
    
    `accuracy` of `1.0` is a match. Row order is never part of this comparison (`--unordered`): the `ordering` claim below says whether the two statements sort the same way, and a different sort is a different query, not a different answer. The score also says how far the two tables agree when they do not match: `paired_row_share` and `column_agreement` beside `column_pairs`, which the report page reads as "9 of 10 rows match".
    
    When the row carries both statements, name where they differ before anyone reads two receipts side by side:
    
    ```bash
    bash "$AGAMI_PLUGIN_ROOT/scripts/sm" claims "$ROOT" --sql-file rows/<n>/agami.sql --against-sql-file rows/<n>/statement.sql > rows/<n>/claims.json
    python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" ledger --row-dir rows/<n> --with-claims
    ```
    
    **This is the row's one ledger run.** Every file Phase 1.5 wrote is still there, so the grades are the same ones 1.5 would have produced, plus the two claim parts and, for a table that matched, a `value_pair` part for each column of yours that matched a differently named column only through one repeated value. When agami's own run failed and there is no statement to compare against, run it here without `--with-claims`. Never run it twice.
    
    The eight claims (tables, what is selected, filter predicates, date window, group keys, join keys, ordering, limit) say which part differs; they never say who is right. Two statements are the same query only when every claim that could be read agrees, what they select included; when the data could not be compared, the page still says "same query, answer not compared" or "different query, answer not compared". Then set the row's `status` with `reconcile.py status`, from the diff's `match` and the ledger's verdict. For a table there is no `diff`: pass `--match true` when `compare-results` reports `accuracy` of `1.0`, `false` otherwise, and `none` when it could not score.
    
    ### 2f — Write the findings
    
    `reconcile.py record` has written every row record to `<artifacts_dir>/local/reconcile/<ts>/rows.jsonl`; then:
    
    ```bash
    python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" findings --run-dir "<artifacts_dir>/local/reconcile/<ts>"
    ```
    
    It writes `findings.json` (one entry per place the semantic model was shown to be missing or wrong, with every row that showed it), `query_defects.json` (the parts of the person's statements the data proved wrong, listed apart so nothing about the semantic model is proposed from them), and `ledger.json`. Question text and SQL only, never result rows beyond the one recorded cell. This is reconcile's own record for the person; nothing reads it but them.
    
    ---
    
    ## Phase 3: Present
    
    **Tell every row in four beats, in the reader's order.** What reconcile is for, given any input (SQL, a question, a chart, or a mix): (1) **how we read your input and how we checked it**, from Phase 1's shape and the question we read from a label, the parts of your statement that held and the ones that did not (the ledger), and whether the statement answers its question (1.5g); (2) **what agami did with the question and what it answered**, from Phase 2b, beside the value you expected; (3) **how agami got there**, from the receipt (the tables it read, the joins, the declared filters it applied, the metric it matched), the claim that differs between the two statements (2e), and the semantic model's own words about what it read (1.5c); (4) **what to change so the output matches, on whichever side the mistake is**, or **what to keep** when it already matches and every check passed: a definition through `/agami-save-correction`, a mistake in your query, a reworded question, or nothing; keep is Phase 3e's offer, made once for the batch and never per row. The summary comes first (3a), then every row in its four beats (3a.5), then the tables the beats drew from (3b to 3d), then the one offer (3e) and the close (3f). The tables and the offer keep their exact text; the beats decide what is read first.
    
    Everything this phase says to the person follows [`shared/plain-language.md`](../../shared/plain-language.md): name who did what (you and your query, agami and its answer, the semantic model and its caveats, filters, joins and metrics, the prompt examples, and the data), and never write the word "model" on its own, because it can mean the semantic model, the AI, the prompt examples or the database and a reader cannot tell which; name the thing and never the mechanism; one idea per sentence, cause then effect then the one action; quote a caveat when it decided something. The part ids and file names stay in the tables below and in the files. The sentences around them are plain, and the AI never speaks of itself steering, front-running or deciding the answer.
    
    ### 3a — Summary line first
    
    ```
    Reconciled <N> numbers: <M> match (within ±1%), <K> mismatch, <E> error.
    ```
    
    When any row carried a statement, add one more line, counting the two statuses that belong to neither `<M>` nor `<K>`. Say each status in the words `reconcile.py`'s `status_words` gives it and never the status itself: a person reading `expected_doubtful` has to look it up, and that is the row where their own query is the thing in doubt:
    
    ```
    <U> gave the same answer, but part of your query could not be checked; <D> gave a different answer, and your query has a problem.
    ```
    
    ### 3a.5 — Every row in four beats
    
    The four beats are told on the **report page**, one card per row for a batch and a checklist with a rail for a single audited query, and the chat carries only three lines: the summary of 3a with the counts as colored words, the report's path, and one line of next steps naming what to fix, what to decide and how many rows are ready to keep. A person reading the transcript later keeps the counts and the actions; the page carries the checks. When the page cannot be written, the four beats below are said in chat instead, one block per row, in the words of `shared/plain-language.md`. A row with no statement skips beat 1's parts and says so; a row that matched with every check passed has a one-line beat 4: keep it. Beat 4 never asks anything per row; the keep question is 3e's, once.
    
    What each card says, whether on the page or, without one, in chat:
    
    ```markdown
    **Q3 Revenue** (from your CSV, tile 3, with the SQL behind it)
    1. What you gave us: a question we read from the tile label as "What was total revenue in Q3 2025?", which you confirmed, and a query that passed every check but one: it leaves out the filter the semantic model declares on orders, `status != 'cancelled'`.
    2. What agami did: it asked the same question and answered $3,890,000; your number is $4,200,000, 7.4% apart.
    3. How it got there: agami read orders, applied the declared filter on status, and matched the metric "revenue". The two queries differ in one place, that filter. The semantic model's caveat on orders says: "cancelled orders are excluded from revenue".
    4. What to change: nothing on agami's side. Your query counts cancelled orders; add the filter and the numbers match. If cancelled orders belong in revenue for you, the caveat and the declared filter are the things to change, through /agami-save-correction.
    
    **Order count** (from your CSV, tile 1)
    1. What you gave us: a number, and a question we read from the label as "How many orders were placed in Q3 2025?", which you confirmed. No query to check.
    2. What agami did: 12,450; your number is 12,450.
    3. How it got there: agami read orders with the declared filter on status and matched the metric "order count".
    4. Keep it: the numbers match and there is nothing unconfirmed, so this row is offered below.
    ```
    
    Beat 4 names the side: "your query" for a defect, "the semantic model" for a gap, "the question" for a doubtful fit, and "agami's answer" for a mismatch where your statement passed every check (a worked example is the fix). It never says "the model".
    
    **Render the four beats as a page**, the way `/agami-connect` hands over the model explorer, so the run is shown the way the semantic model is shown. Every row is the same card: the question, one verdict carrying the measurement behind it, the one thing to do, and three sections closed at rest, **Data**, **SQL** and **Checks**, each with a one-line summary that names what did not pass when something did not, and a count when everything did. Render it after every chunk with every row finished so far; the render after Phase 2's exit `4` is the one 3a points at. The page filters by status, by who acts and by a word in the question or the beats; a filter narrows what is shown, never what the block sends, so a row decided and then filtered away is still in the block. The chat keeps 3a, the link and one line of next steps; 3b to 3d below still render when the page could not be written.
    
    1. **Build the report items file with the verb, never by hand.** The renderer runs `reconcile.py report-items --run-dir` itself from the run directory: one item per row of `rows.jsonl`, from each row's ledger, its comparison, its claims and the two receipts, every item carrying `diff` (one row per check, `{key, state, yours, agami, note, yours_hi?, agami_hi?}`, the check named first and the two sides as values with the differing tokens marked), `result` (`data`: matches, partly, differs or could_not_compare; `query`: same, different or not_comparable; the plain-word `label`; `unchecked`, the checks on your query that could not run; `differs_in`), `fix` (your query, the semantic model, the examples, the question, agami again, or nothing), `fix_words`, `prefill`, `sentence` and `change`. You hand it words only: a words file `{"<row>": {"sentence": "…", "change": ["…"]}}` with one entry per row whose sentence or change you rewrote in the plain words of `shared/plain-language.md`. You rewrite only `sentence` and `change`, and never touch `diff`, `result`, `fix` or any other field; a words file carrying another key is refused. (`report-items --run-dir` on its own prints the items for reading.)
    2. **Render from the run directory, check it, and point at it:**
       ```bash
       python3 "$AGAMI_PLUGIN_ROOT/scripts/render_reconcile_report.py" --title "Reconcile · <profile>" \
         --profile <profile> --run-dir "<artifacts_dir>/local/reconcile/<ts>" \
         --words-file /tmp/agami-reconcile-words-<ts>.json \
         --out "<artifacts_dir>/local/reconcile/<ts>/report.html"
       python3 "$AGAMI_PLUGIN_ROOT/scripts/reconcile.py" check-run --run-dir "<artifacts_dir>/local/reconcile/<ts>"
       ```
       The renderer writes `<run_dir>/report-items.json` beside the page (for the parser and for reading), stamps the page with the digest of the items it rendered, and prints three lines: the counts by result and by fix as words, the page's path, and the next step. `check-run` exits `0` only when every row in the checkpoint has its files and the page's stamp is the digest of the current items; on `4`, fix what it names and render again. **Say the three lines the renderer printed**, the counts as colored words (match, differs, a mistake on your side, could not check): the 3a summary, the report's path, and one line of next steps. The page offers `keep` only on rows whose status is `match` with a single recorded cell, Phase 3e's own predicate; the page draws that from the items, and the parser checks it again from the run's own files, because the offer's predicate is the ledger's and never the page's. **Never write `report.html`, `report-items.json` or `rows.jsonl` with the Write tool**; the only files this session writes by hand in Phases 2 and 3 are the words file and the chunk file.
    3. **Read the decisions back** when the block arrives (`profile:`, `reconcile-run:`, `decisions:` and one JSON array, `done`), handing the parser the run directory the page was rendered from. It reads which rows may be kept from `rows.jsonl` and each row's `ledger.json`, never from anything typed, and refuses a block whose `reconcile-run` is not this run:
       ```bash
       python3 "$AGAMI_PLUGIN_ROOT/scripts/parse_reconcile_report.py" --block-file /tmp/agami-reconcile-decisions-<ts>.txt --run-dir "<artifacts_dir>/local/reconcile/<ts>"
       ```
       A `needs_judgment` means the block did not parse, came from another run, or carries a decision that could not be applied as written (a keep the run does not allow, words beside a keep, a misspelt decision): ask for it again, apply nothing. Then, per decision: **`keep`** is the person's yes to Phase 3e's offer for that row, applied through 3e's own doors and its own rules (the split, the band, one call per row); **`change`** takes the finding and the person's words to `/agami-save-correction`, one definition at a time; **`example`** takes a statement and its question to `/agami-save-correction` as a prompt example, with the words as its note, so agami writes the query that way next time (the same door 3e's keep uses for an example, without 3e's split). **Which statement, on which row:** the person's when they supplied one; on a question-only row there is none, and the person choosing `example` there is saying agami's own query is right, so agami's is what gets sent; **`fix`** and **`reword`** come back to the person as the next thing to do, with the row's beat 4 repeated; **`nothing`** changes nothing. No decision writes anything this skill does not already write.
    
    ### 3b — Mismatches table (lead with what didn't match)
    
    Render the mismatches as a markdown table BEFORE the matches:
    
    ```markdown
    ### Mismatches
    
    | Label | Expected | Got | Δ | Drill-down |
    |---|---:|---:|---:|---|
    | Q3 2025 Revenue          | $4,200,000 | $3,890,000 | -7.4% | <artifacts_dir>/local/charts/&lt;profile&gt;/...html |
    | Active customers (Apr)   | 12,450     | 11,920     | -4.3% | <artifacts_dir>/local/charts/&lt;profile&gt;/...html |
    ```
    
    Cell formatting:
    - Numbers carry the same currency / magnitude suffix as the input where unambiguous (echo the user's `raw_value` for `Expected`, format `Got` with the same shape).
    - `Δ` is the signed percent (red wins emphasized in chat by ✗ prefix if Markdown rendering allows; otherwise plain text).
    - `Drill-down` links to the chart-template HTML for that query. Open these to see the full receipt — that's where the definitional disagreement lives.
    
    For each mismatch row, surface a one-line interpretation under the table:
    
    > **Q3 2025 Revenue** — agami reports $3.89M, your dashboard says $4.2M (-7.4%). Open the receipt; the metric `revenue` here is *gross of refunds in USD at invoice date*. If your dashboard nets refunds, that's the gap.
    
    This is where the trust win lands. The DE doesn't have to chase the disagreement — the receipt + your interpretation does it for them.
    
    ### 3b.5 — Your statements (where the evidence itself fell short)
    
    Only when a row carried a statement, and only for the parts that did not grade `confirmed`. One rule throughout: no SQL in chat; the part's name and its note are enough, and `findings.json` holds the rest. Three blocks, because the three kinds of part answer three different questions, and a reader scanning one table counts every row as a problem with the query. The table holds the grades that judge; the two blocks under it hold what was not judged.
    
    ```markdown
    ### Your statements
    
    | Label | Checked | Grade | What the ledger found |
    |---|---|---|---|
    | Delivered orders | value orders.status = 'Delivered' | a mistake in your query        | not one of the values the semantic model lists, and no row holds it; did you mean 'delivered' |
    | Paid revenue     | join orders to payments           | a mistake in your query        | the join is on a different key than the one the semantic model declares |
    | Q3 Revenue       | default filter on orders          | the semantic model is missing this | the semantic model declares this filter and the statement does not apply it |
    | Open items       | value list for items.state        | the semantic model is missing this | the column holds 6 distinct values and the semantic model lists none of them |
    
    **What couldn't be checked**
    - Q3 Revenue, double counting in SUM(total): the pre-flight could not bind this aggregate to one table: a column inside the aggregate could not be attributed to one table
    - Orders placed, answers the question: the statement may not answer the question: the question asks how many orders were placed and the statement counts order items; reword the question or the statement and re-run this row
    
    **What this run noticed**
    - Open items, rows the join leaves out, items to users: 3 of 8345 items rows have no users partner and are dropped by this inner join; counted over the whole table, before the statement's own filters; rows of users with no items partner were not counted
    
    **What the semantic model says in words**
    - items.state, column caveat: "open is state NOT LIKE 'Closed%'"
    - items, table caveat: "pending fulfillment is state IN ('Open','Pending','Work in Progress')"
    - example "How many pending items?": WHERE state IN ('Open','Pending','Work in Progress')
    - Two of these name different values for items.state. The semantic model disagrees with itself here; which is right is the person's call.
    ```
    
    Say the kinds apart in one sentence each: a `query_defect` is the person's to fix, and nothing about agami changes because of it; a `model_gap` is a place the data proved their statement right where the semantic model is missing or wrong, and a single fix still goes through `/agami-save-correction` (an undeclared value list is a field's `choice_field`, the `field_metadata` route). An `unresolved` part is neither: it says what could not be checked and why, and it sits in its own block so nobody counts it as a defect. Every grade and every part name already has its words in `reconcile.py` (`_STATUS_WORDS`, `_STATE_WORDS`, `_PART_WORDS`, `_PART_KEYS`), which is what the report page renders; use those, so the table in chat and the card on the page call the same thing the same name. Say every one of these in the words of [`shared/plain-language.md`](../../shared/plain-language.md)

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related