Presentation Signals — the surface tells (auxiliary, never a verdict)

SkillMedia

Checkable-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.

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 at minor (SURFACE_ONLY_SKILLS + SURFACE_PATTERNS in tools/adjudicate_findings.py) — at most SOFT_FLAGS, never HARD_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. See references/hack-pattern-taxonomy.md §F.

🔒 Do not wrap this skill in /loop, /schedule, or CronCreate. 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 deterministic HP-DUP-TABLE / HP-PIPELINE-ARTIFACT checks 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:

  1. 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);
  2. 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 tagged false_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)

AuditorQuestion it answersLevel
presentation-signals (this)Surface tells a reviewer notices first (dup tables, pipeline artifacts, thin/LLM figures, padding) — AUXILIARY, labelled surface-classL0
ai-style-impressionsPure AI writing-style impressions (AI-flavor, defensive hedging, broken narrative arc, jargon-stuffing, invented codenames) — zero verdict weight (AIS track)L0
consistency-auditDoes the paper contradict ITSELF / described method = evaluated method?L0
experiment-forensicsAre the reported numbers what the code actually computes? (fake GT, self-norm, phantom)L2
baseline-comparison-auditAre the right baselines present, tuned, and is "SOTA" earned?L0 stated / L2 verified
citation-forensicsDo the cited papers exist and support the claim they are used for?L0
adversarial-case-builderStrongest 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.md Layer 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-reports false_positive_risk. It is the evidence-extractor, not the judge.
  • Fresh thread per run. codex-reply is intentionally absent from allowed-tools; never carry one run's conclusions into another (the bias guard).
  • Detect-only. No Edit in allowed-tools; the reviewer sandbox is read-only. Write is 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-ledger first. This skill never re-reads the raw PDF and invents its own structure (contract rule 1).
  • TABLE_SECS = 0Step 1 (dup-table) will correctly emit []: with no parsed table_cell claims there are no tables to compare (common on a pure PDF-text run). Keep the empty file; continue to Step 2.
  • CAPTION_CL = 0 and/or PDF_FILE = NONEHP-LLM-FIGURE has no caption anchor and no image to inspect; the reviewer will almost certainly hold it at info. 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__codex call (gpt-5.6-sol, xhigh) — never codex-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 info and even anchored surface findings cap at minor. 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