Presentation Signals — the surface tells (auxiliary, never a verdict)
SkillMediaCheckable-ish surface presentation signals a reviewer notices first — duplicate/near-identical tables, leftover pipeline/template strings, too-few or LLM-looking figures, and page-padding. AUXILIARY ONLY and weak by design: a deterministic pass (tools/check_presentation.py — dup-table + pipeline-artifact) plus a fresh cross-model GROSS-cases-only semantic pass (thin-float, LLM-figure, page-padding), every above-info finding span-anchored to the evidence ledger (claims.json). The adjudicator CAPS everything at minor (SURFACE_ONLY_SKILLS + SURFACE_PATTERNS) — these contribute at most SOFT_FLAGS, never a HARD verdict — default false_positive_risk:high. NOTE: the pure AI writing-STYLE impressions (AI-flavor prose, defensive 'not-X-but-Y' hedging, narrative-arc, jargon-stuffing, invented codenames) MOVED to the zero-verdict-weight AIS track — for those use skills/ai-style-impressions, NOT this. Emits presentation-signals.findings.json; NEVER computes the verdict. Triggers: \"presentation signals\", \"surface check\", \"duplicate tables\", \"排版信号\".
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Presentation Signals — the surface tells (auxiliary, never a verdict) skill
What this skill tells your AI
The instructions your AI receives, as published by wanshuiyin/anti-autoresearch in skills/presentation-signals/SKILL.md and read by ahel’s review.
Run surface signal checks for: $ARGUMENTS (requires claims.json from
/evidence-ledger). Emit span-anchored presentation-signals.findings.json. This
skill computes no verdict.
⚠️ This skill is deliberately weak by design. A polished paper can be fraudulent and a rough paper can be honest, so surface signals must never drive a verdict. Everything here is emitted under skill
presentation-signals, which the adjudicator caps atminor(SURFACE_ONLY_SKILLS+SURFACE_PATTERNSintools/adjudicate_findings.py) — at mostSOFT_FLAGS, neverHARD_FLAGS. This is not an AI-text classifier; for authorship detection use a dedicated tool (Pangram / GPTZero / Binoculars). Our only job is to add "combine with the substantive findings and look closer" context. Seereferences/hack-pattern-taxonomy.md§F.
🔒 Do not wrap this skill in
/loop,/schedule, orCronCreate. It is verdict-bearing input — it proposes the surface findings the deterministic adjudicator turns into the report. Re-firing it on a wall-clock timer adds no signal: its output changes only when the paper / ledger changes, not with the clock. Schedule the external wait that precedes it — ledger built → check once. (Mirrors ARIS's external-cadence doctrine.)
Why this exists
Real reviewers notice surface tells before they read a single number — and they say so out loud: "两张表一模一样" (two tables are identical), "图还是大模型生成的" (the figure is LLM-generated), "就这还没写满9页" (couldn't even fill 9 pages), "堆砌名词吗" (just stuffing jargon?), "本文不是什么什么,而是什么什么…论文应该直接表达 做了什么" (stop hedging "this paper is not X but rather Y" — just say what you did), "摘要写的像实验分析,读不到引言" (the abstract reads like an experiment log; the introduction is unreadable). An autoresearch pipeline (or a rushed human) produces exactly these artifacts: a table copy-pasted and never updated, an oversized float to pad the page limit, a decorative generated illustration in place of a real results plot, paragraphs of generic LLM boilerplate, draft text so densely over-hedged that every sentence defends against an objection, and an abstract that dumps experiment notes instead of telling a background → contribution → evidence story.
These signals are real in the sense that reviewers react to them — but they are weak evidence of misconduct. A concise honest paper has few floats; a careful honest author uses LLM assistance for prose; a legitimate teaser figure can look "generated". So this skill's contract is narrow and permanent:
- it emits only the five §F surface patterns, all labelled surface-class;
- it defaults every semantic finding to
false_positive_risk: high(the deterministicHP-DUP-TABLE/HP-PIPELINE-ARTIFACTchecks set their own — the latter is low-FP); - it never says a paper is "AI-generated" or implies fabrication;
- silence is the common, correct output — most papers should produce few or zero surface findings.
It exists to add context to the substantive auditors (consistency-audit,
experiment-forensics, baseline-comparison-audit, citation-forensics), not to
stand alone. If a surface tell sits next to a real numeric contradiction, the
substantive finding carries the weight; the surface note just says "look closer."
Core principle
Ledger-anchored, span-verified, capped-at-minor, reviewer ≠ adjudicator, NOT a detector. Two passes feed the pipeline:
- a deterministic pass (no model) — the objective, reproducible surface signals
computable without judgment: duplicate tables (
HP-DUP-TABLE) and leftover pipeline/ template strings (HP-PIPELINE-ARTIFACT); - a fresh cross-model GROSS-cases-only semantic pass — the three judgment-call
signals (
HP-THIN-FLOAT,HP-LLM-FIGURE,HP-PAGE-PADDING), each span-anchored and taggedfalse_positive_risk: high.
Both emit findings conforming to schemas/finding.schema.json. Every above-info
finding cites a ledger claim_id + a verbatim span (references/integrity-forensics-contract.md
rules 1–2). Because the ledger holds only checkable claims (numbers, scope,
captions, citations, table cells) and almost no free prose, a surface signal that
cannot land on an extracted claim stays info — a note, never a flag. That is the
design working: a free-prose surface impression that lands on no extracted claim
structurally almost never becomes even a minor flag. The model proposes;
tools/adjudicate_findings.py decides
(references/reviewer-independence.md Layer 2). This skill computes no verdict.
How this differs from the other auditors (route correctly)
| Auditor | Question it answers | Level |
|---|---|---|
presentation-signals (this) | Surface tells a reviewer notices first (dup tables, pipeline artifacts, thin/LLM figures, padding) — AUXILIARY, labelled surface-class | L0 |
ai-style-impressions | Pure AI writing-style impressions (AI-flavor, defensive hedging, broken narrative arc, jargon-stuffing, invented codenames) — zero verdict weight (AIS track) | L0 |
consistency-audit | Does the paper contradict ITSELF / described method = evaluated method? | L0 |
experiment-forensics | Are the reported numbers what the code actually computes? (fake GT, self-norm, phantom) | L2 |
baseline-comparison-audit | Are the right baselines present, tuned, and is "SOTA" earned? | L0 stated / L2 verified |
citation-forensics | Do the cited papers exist and support the claim they are used for? | L0 |
adversarial-case-builder | Strongest evidence-bound rejection memo (no verdict weight) | any |
The pure AI writing-style impressions — AI-flavor, defensive hedging, broken narrative
arc, jargon-stuffing, and invented codenames — moved out of this skill in v0.5 to
the zero-verdict-weight AIS track owned by skills/ai-style-impressions; route any
AI-writing-style question there, not here.
Stay in lane. This skill emits only the five §F surface (HP-DUP-TABLE,
HP-PIPELINE-ARTIFACT, HP-THIN-FLOAT, HP-LLM-FIGURE, HP-PAGE-PADDING)
patterns — nothing else. Do NOT raise here (hand off instead): if
two "duplicate" tables actually report contradictory numbers for the same setting →
that is a numeric self-contradiction for consistency-audit, not HP-DUP-TABLE; if a
figure misrepresents a result vs its caption → consistency-audit
(HP-CAPTION-MISMATCH); if a thin float count sits under a "SOTA / comprehensive"
empirical claim → the substantive scope question goes to
baseline-comparison-audit / consistency-audit (you may also emit HP-THIN-FLOAT
as a capped surface note); a fabricated reference → citation-forensics. A surface
finding is the weakest thing in the report by construction — never use it to carry a
substantive accusation.
Constants & Reviewer Calling Convention
REVIEWER_MODEL = gpt-5.6-sol # different family from executor (Claude)
REVIEWER_REASONING = xhigh # always; effort never lowers reviewer quality
REVIEWER_SANDBOX = read-only # detect-only; never mutate the paper
REVIEWER_CWD = <paper-dir> # so it can read claims.json + pdf-text + the PDF directly
THREAD_POLICY = fresh mcp__codex__codex per run; NEVER mcp__codex__codex-reply
TAXONOMY_VERSION = 0.5 # references/hack-pattern-taxonomy.md §F
DETERMINISTIC_PATTERNS = HP-DUP-TABLE, HP-PIPELINE-ARTIFACT # Step 1 (tool)
SEMANTIC_PATTERNS = HP-THIN-FLOAT, HP-LLM-FIGURE, HP-PAGE-PADDING # Step 2 (reviewer)
SEVERITY_CAP = minor # SURFACE_ONLY_SKILLS + SURFACE_PATTERNS in adjudicate_findings.py
DEFAULT_FP_RISK = high # every surface finding; this is not optional
OBS_REQUIRED = 0 # every F-pattern is decidable at L0 (PDF-only)
DETERMINISTIC_FINDINGS = presentation-signals.deterministic.findings.json # Step 1, ids PRES###
SEMANTIC_FINDINGS = presentation-signals.findings.json # Step 3, ids F### (validated)
TRACE_POLICY = forensic (never silently dropped)
TRACE_DIR = .aris/traces/presentation-signals/<YYYY-MM-DD>_run<NN>/
- Executor (Claude) builds none of the judgment: it locates the ledger + the PDF,
passes paths + the ledger + the checklist to the reviewer, validates the
reviewer's spans, caps severity, and writes the findings file. It never summarizes
the paper, pre-judges "this looks AI-written", or leaks an opinion into the prompt
(
reviewer-independence.mdLayer 1). - Reviewer (codex / gpt-5.6-sol) reads
claims.json+ the PDF-text + the PDF itself (visually only if it can render it; otherwise caption text only — see HP-LLM-FIGURE), proposes gross-only surface signals, and self-reportsfalse_positive_risk. It is the evidence-extractor, not the judge. - Fresh thread per run.
codex-replyis intentionally absent fromallowed-tools; never carry one run's conclusions into another (the bias guard). - Detect-only. No
Editinallowed-tools; the reviewer sandbox isread-only.Writeis used only for this skill's own findings / trace artifacts, never the audited paper. This is a third-party forensics tool, never a co-author.
Step 0 — Preconditions: locate the ledger, read the level, find the PDF
The ledger is the only structure this skill reasons over for anchoring. Resolve
it, read the run's observability level L and paper_id, count the float-bearing
claims, and locate the PDF + text source the reviewer will read (each Bash block is
self-contained — shell state does not persist between calls, so re-derive paths):
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
# $ARGUMENTS is a paper-dir OR a claims.json path:
LEDGER="$ARGUMENTS"; [ -d "$LEDGER" ] && LEDGER="$LEDGER/claims.json"
# Only the NO-ARGUMENT case defaults to the CWD ledger. An EXPLICIT argument that
# resolves to a missing claims.json must NOT silently fall back to $(pwd) — that
# could audit the wrong paper; let the NO_LEDGER check below fire instead.
[ -z "$ARGUMENTS" ] && LEDGER="$(pwd)/claims.json"
python3 - "$LEDGER" <<'PY'
import json, sys, os
p = sys.argv[1]
if not os.path.isfile(p):
sys.exit("NO_LEDGER: claims.json not found. Run /evidence-ledger FIRST "
"(it writes artifact_manifest.json + claims.json).")
d = json.load(open(p, encoding="utf-8"))
cl = d.get("claims", [])
caps = [c for c in cl if c.get("type") == "caption"]
tabs = sorted({(c.get("location") or {}).get("section","") for c in cl
if c.get("type") == "table_cell"
and str((c.get("location") or {}).get("section","")).startswith("table:")})
scope = [c for c in cl if c.get("type") == "scope"]
print("LEDGER =", os.path.abspath(p))
print("PAPER_DIR =", os.path.dirname(os.path.abspath(p)) or ".")
print("PAPER_ID =", d.get("paper_id", "?"))
print("RUN_LEVEL_L =", d.get("observability_level", 0))
print("CLAIMS =", len(cl))
print("CAPTION_CL =", len(caps), " (anchors for HP-LLM-FIGURE)")
print("TABLE_SECS =", len(tabs), tabs, " (float count for HP-THIN-FLOAT / HP-DUP-TABLE)")
print("SCOPE_CL =", len(scope), " (anchors for HP-THIN-FLOAT / HP-PAGE-PADDING)")
paper_dir = os.path.dirname(os.path.abspath(p)) or "."
srcs = d.get("source_files", [])
for sf in srcs:
print("SOURCE =", sf.get("kind"), sf.get("path"))
# Deterministically pick the prose source + the PDF the reviewer will read, FROM the
# ledger's source_files (authoritative); fall back to a sorted glob. source_files paths
# may be relative to PAPER_DIR or absolute. This is the ONLY selection (no shell `ls`
# later), so the PDF-only/L0 path with no pdf source resolves to NONE.
import glob
def _resolve(rel):
cand = rel if os.path.isabs(rel or "") else os.path.join(paper_dir, rel or "")
return os.path.abspath(cand) if os.path.isfile(cand) else ""
def _pick(kinds, globs):
for sf in srcs:
if sf.get("kind") in kinds:
r = _resolve(sf.get("path"))
if r:
return r
for g in globs:
hits = sorted(glob.glob(os.path.join(paper_dir, g)))
if hits:
return os.path.abspath(hits[0])
return ""
print("PDF_TEXT_FILE=", _pick({"text", "latex"}, ["*.txt", "*.tex"])
or "NONE (prose signals limited to ledger spans)")
print("PDF_FILE =", _pick({"pdf"}, ["*.pdf"])
or "NONE (HP-LLM-FIGURE limited to caption text)")
PY
Failure / edge handling.
NO_LEDGER→ stop; tell the user to run/evidence-ledgerfirst. This skill never re-reads the raw PDF and invents its own structure (contract rule 1).TABLE_SECS = 0→Step 1(dup-table) will correctly emit[]: with no parsedtable_cellclaims there are no tables to compare (common on a pure PDF-text run). Keep the empty file; continue to Step 2.CAPTION_CL = 0and/orPDF_FILE = NONE→HP-LLM-FIGUREhas no caption anchor and no image to inspect; the reviewer will almost certainly hold it atinfo. That is honest, not a failure.CLAIMS = 0(degenerate ledger) → every semantic surface signal will be unanchored →info. Run anyway; the file must exist.
Step 0 prints RUN_LEVEL_L, PAPER_ID, the absolute LEDGER / PAPER_DIR,
PDF_FILE, and PDF_TEXT_FILE. Shell variables do not persist across Bash calls,
so paste these literal absolute values into the <...> placeholders of each later
step — do not assume an exported $LEDGER survives between blocks.
Step 1 — Deterministic surface check (no LLM)
The one objective, eval-testable surface signal: duplicate tables — two tables whose
ordered numeric cells are identical after rounding to 4 decimals (HP-DUP-TABLE),
computed purely from the ledger's table_cell values. Runs before any model:
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
LEDGER="<abs path to claims.json from Step 0>"
python3 "$ROOT/tools/check_presentation.py" \
--ledger "$LEDGER" \
--out "$(dirname "$LEDGER")/presentation-signals.deterministic.findings.json"
This emits, per duplicate pair, a finding with pattern_id: HP-DUP-TABLE,
severity: minor, false_positive_risk: high, observability_level_required: 0,
reviewer.deterministic: true, finding_id: PRES###, and two evidence entries —
one per table section, each anchored to a representative table_cell claim of that
section (its verbatim text_span); the full matching ordered values appear in the
finding's description, not as per-cell evidence. Two tables qualify only if they share
an identical ordered sequence of ≥ MIN_CELLS (2) numeric cells (each rounded to 4
decimals before comparison).
Failure handling. If the tool errors, fix the invocation
(python3 "$ROOT/tools/check_presentation.py" --help) — do not hand-fabricate
deterministic findings. An empty output ([]) is a valid, expected result (no
identical tables, or no table_cell claims were extracted to compare); keep the file.
Step 2 — Cross-model GROSS-cases-only semantic pass (reviewer ≠ adjudicator)
The other three §F signals are judgment calls. Open a fresh mcp__codex__codex
thread (the Reviewer Calling Convention above), cwd = PAPER_DIR so it can read
claims.json, the PDF-text, and the PDF directly. First create the forensic trace dir
and fix the exact response path — shell state does not persist, so this prints the
literal paths to reuse:
LEDGER="<abs path to claims.json from Step 0>"; PAPER_DIR="$(dirname "$LEDGER")"
TS="$(date +%F)"; BASE="$PAPER_DIR/.aris/traces/presentation-signals"
NN=1; while [ -d "$(printf '%s/%s_run%02d' "$BASE" "$TS" "$NN")" ]; do NN=$((NN+1)); done
TRACE_DIR="$(printf '%s/%s_run%02d' "$BASE" "$TS" "$NN")"; mkdir -p "$TRACE_DIR"
echo "TRACE_DIR = $TRACE_DIR"
echo "PROPOSED = $TRACE_DIR/001-surface-semantic.response.md # save the raw reply here; reuse as PROPOSED in Step 3"
Replace the bracketed placeholders below with the real values from Step 0, send EXACTLY
this, and save the verbatim reviewer reply to the PROPOSED path above (the Step 3
input) before parsing:
mcp__codex__codex:
model: gpt-5.6-sol
config: {"model_reasoning_effort": "xhigh"}
sandbox: read-only
cwd: <absolute PAPER_DIR from Step 0>
prompt: |
You are checking PRESENTATION signals only — the kind of surface tell a reviewer
notices at a glance. You are explicitly NOT deciding whether the paper is
AI-written, and NOT whether it is fraudulent. You are NOT an AI-text classifier.
Your output is auxiliary "look closer" context that a deterministic adjudicator
is a surface-class signal reported under its own label; it does not carry an
integrity verdict on its own. Default to
SILENCE: an empty array [] is the expected, correct output for most papers.
INPUTS (in your working directory, read them directly):
- claims.json — the evidence ledger: the authoritative, span-anchored list
of every checkable claim {claim_id, type, text_span (VERBATIM source text),
location, value?}. This is the ONLY thing you may anchor a finding to.
- <PDF_TEXT_FILE from Step 0> — extracted PDF text (for prose / padding / jargon).
- <PDF_FILE from Step 0, if any> — the rendered PDF (for figure inspection).
RUN OBSERVABILITY LEVEL L = <L from Step 0>.
HARD RULES (a finding that breaks any of these is worthless):
1. GROSS ONLY. Flag only BLATANT cases. If you are unsure, do NOT flag. This is
especially binding for HP-LLM-FIGURE and HP-PAGE-PADDING (the most FP-prone).
2. ANCHOR. Every finding above severity "info" MUST carry >=1 evidence entry
{claim_id, span}, where claim_id EXISTS in claims.json and span is a VERBATIM
substring of THAT claim's text_span (no paraphrase, no added words). If you
cannot quote a verbatim ledger span for a signal, emit it at severity "info"
(a note) or drop it — it can NEVER be a flag. The ledger holds numbers, scope,
captions, citations, and table cells; generic prose is usually NOT in it, so
any surface impression that cannot quote such a claim will correctly remain "info".
3. SEVERITY + FP. Surface flags are a "look closer" class — `minor` is the honest
severity for one; say so rather than inflating. For any ANCHORED finding
(rule 2 satisfied) set severity = "minor"; an UNANCHORED signal stays "info"
(rule 2) — never promote it to "minor". NEVER use "major"/"critical" (the
adjudicator caps surface signals at minor regardless; do not argue past it). Set
false_positive_risk = "high" and observability_level_required = 0 for EVERY
finding (all L0-decidable).
4. NO ACCUSATION, NO AUTHORSHIP VERDICT. description and recommended_reviewer_action
say what a human should glance at / ask. NEVER write "AI-generated", "fabricated",
"reject", or imply misconduct. A surface tell is a prompt to look, nothing more.
5. STAY IN LANE. pattern_id MUST be exactly one of the three below. If you notice a
SUBSTANTIVE problem (numbers contradict, a citation looks fake, a baseline is
missing), do NOT encode it here — that belongs to consistency-audit /
citation-forensics / baseline-comparison-audit. Ignore it.
CHECKLIST (the THREE semantic surface patterns; one finding per concrete, blatant case):
HP-THIN-FLOAT — a full-length paper claiming broad/comprehensive empirical
results while containing almost no figures/tables. Anchor to the
SCOPE claim (e.g. "comprehensive evaluation across diverse
benchmarks"); put the actual float count in the description.
FP (high): legitimately theoretical or short-format work.
HP-LLM-FIGURE — a "figure" that is a generated/decorative illustration rather
than a real plot/diagram of results. Anchor to the figure's
CAPTION claim. If you cannot VISUALLY inspect the PDF, judge only
from the caption text (e.g. it literally describes a generated
illustration) and otherwise leave it at "info" /
needs_external_check — do NOT guess from a filename.
FP (high): legitimate conceptual/teaser figures; good diagrams.
HP-PAGE-PADDING — oversized floats, repeated content, or vacuous filler used to
reach (or conspicuously miss) the page limit. Anchor to a ledger
claim that the padding rests on (a caption / scope / table cell).
FP (high): legitimately concise work; venue length norms.
OUTPUT: a single JSON array, and NOTHING ELSE (no prose, no code fence). Each
element conforms to schemas/finding.schema.json:
{
"finding_id": "F001",
"skill": "presentation-signals",
"pattern_id": "HP-THIN-FLOAT | HP-LLM-FIGURE | HP-PAGE-PADDING",
"title": "short, neutral",
"description": "the surface observation, plus the explicit note that it is a weak signal to look closer (not evidence of AI-authorship or fraud)",
"severity": "minor",
"observability_level_required": 0,
"evidence": [{"claim_id": "C0xx", "span": "verbatim substring of that claim",
"location": {"file": "...", "section": "..."}}],
"verdict_local": "warn",
"false_positive_risk": "high",
"recommended_reviewer_action": "what to GLANCE AT or ASK — never 'reject', never 'AI-generated'"
}
If nothing is blatant, return []. That is the expected output for most papers.
Failure handling.
- MCP stall / hang (common in long sessions): re-invoke the identical prompt as a
fresh
mcp__codex__codexcall (gpt-5.6-sol, xhigh) — nevercodex-reply. - Reviewer returns prose, not a JSON array: the Step 3 validator extracts the
outermost
[...]; if there is none, re-ask once with "Output ONLY the JSON array, nothing else." Do not hand-author findings on the reviewer's behalf. - Reviewer over-flags a surface impression: that is what the Step 3 anchor gate + the
adjudicator's surface label are for — an unanchored impression falls to
infoand even anchored surface findings cap atminor. Do not pre-suppress; let the gates work.
Step 3 — Validate + anchor + cap (the anti-detector gate)
The executor enforces the ANCHOR gate and the surface cap before keeping
anything — exactly the rules tools/adjudicate_findings.py re-applies, so nothing you
keep is silently rejected downstream. The span must be a verbatim,
whitespace-normalized substring of the cited claim (span in base, never
base in span — appending hallucinated text to a real claim must fail):
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
LEDGER="<abs path to claims.json>"
PROPOSED="<the PROPOSED path printed in Step 2>" # the verbatim reviewer reply you saved
OUT="$(dirname "$LEDGER")/presentation-signals.findings.json"
python3 - "$LEDGER" "$PROPOSED" "$OUT" <<'PY'
import json, re, sys
ledger_path, proposed_path, out_path = sys.argv[1], sys.argv[2], sys.argv[3]
def nw(s): # mirror adjudicator _norm_ws (whitespace only)
return " ".join((s or "").split())
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 154
- Forks
- 8
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
presentation-signals- Source
- github.com/wanshuiyin/anti-autoresearch