| name | dispute-fighter |
| description | Evaluate whether a Stripe credit card dispute (chargeback) is worth fighting, and assemble a ready-to-submit evidence package to fight it. Pulls live context from Stripe plus your configured support, docs, chat, analytics, and order-data sources through Airbyte (Agent MCP or SDK), maps it to Stripe's dispute evidence fields, and produces both a structured JSON payload and a human-readable rebuttal document. Also posts a daily triage digest to your team chat. Use whenever a user mentions a dispute, chargeback, "representment," a Stripe dispute ID (dp_...), a disputed charge (ch_/pi_), asks whether a dispute is winnable or worth contesting, needs to gather or write up evidence for a chargeback, asks to respond to a card network / issuer dispute, wants a daily/recurring summary of disputes posted to Slack, or asks to set up / configure the skill or choose which data sources to connect — even if they don't say "Airbyte" or "evidence package." Always stop for human review before anything is submitted to Stripe. |
Dispute Fighter
Help a merchant decide whether to fight a Stripe dispute and, when it's worth it, build the
evidence package that gives the best shot at winning. The skill has three capabilities; the first
two usually run back-to-back, but each can run alone:
- Evaluate — is this dispute worth fighting? Produce a clear recommendation with reasoning.
- Build — assemble the evidence package (Stripe
evidence JSON + a readable rebuttal doc).
- Daily digest — post a triage summary of all open disputes needing a response to Slack, sorted
by urgency, so nothing slips past its deadline. See Daily digest.
The hard part of winning a chargeback is matching the right evidence to the specific reason code
the issuer cited, and being honest about when a dispute is unwinnable so the merchant doesn't waste
the dispute fee. This skill encodes that judgment.
Guardrail: never auto-submit
Disputes involve real money and are effectively irreversible once submitted — you usually get one
submission per dispute. Always present the recommendation and the package for a human to review.
Do not call any Stripe write/submit API on your own. If the user explicitly asks you to submit
after reviewing, confirm once more, then proceed only if they have a Stripe write tool available.
Setup: data access via Airbyte (two modes)
All source data is reached through Airbyte. There are two access modes — read
references/airbyte-data-access.md for the details. Both feed the
same JSON files to the local scripts, so the rest of the workflow is identical either way.
- Mode A — Agent MCP (preferred). The Airbyte MCP server (
https://mcp.airbyte.ai/mcp) is
connected to the client once via browser OAuth — no pip install, and no credentials typed
into a terminal or into chat. In this mode you (the agent) call the connected Airbyte MCP tools
to fetch data, then write the results to the JSON files the scripts expect (dispute_context.json,
a disputes dump, an outcomes map). A shell subprocess can't reach MCP, so don't run
fetch_dispute_context.py in this mode — gather via MCP and hand files to the other scripts.
- Mode B — Agent SDK (fallback). For headless/cron use with no connector. The scripts import
airbyte-agent-sdk and read AIRBYTE_CLIENT_ID / AIRBYTE_CLIENT_SECRET from the environment.
Prefer Mode A whenever the Airbyte MCP is connected (you'll see its tools available). Fall back to
Mode B only if it isn't. Either way: never invent data — if you can't reach a source, say so.
Configurable sources (portable across organizations)
This skill is built to be shared. Stripe is pinned as the disputes system of record, but the
supporting sources are defined by role, and each role points at whatever connector an org uses:
All roles are filled by Airbyte Agent connectors (the airbyte-agent-sdk catalog,
https://docs.airbyte.com/ai-agents/connectors) — not classic ELT connectors.
| Role | Default connector | What it provides | Swap for (Agent connectors) |
|---|
customer_comms | zendesk-support | customer contact, refund offers, cancellation requests | Intercom, Freshdesk, Salesforce, HubSpot |
policies | google-drive | refund/cancellation/ToS/shipping policy docs | Notion, Confluence, Airtable |
internal_chat | slack | internal discussion about the customer/order | (Slack only today) |
service_usage (optional) | — (off) | proof the customer used the service → access_activity_log | Amplitude (event usage), Orb (usage billing) |
order_data (optional) | — (off) | order line items, shipping/fulfillment/tracking, provided address | Shopify, WooCommerce, Amazon Seller Partner |
digest target | slack | where the daily digest is posted | (Slack; Teams/email need a non-Agent tool) |
The mapping lives in a config file at the skill root (loaded by scripts/config.py). If no config is
present, the defaults above apply. Code and the fetched-context JSON are keyed by role, never by
tool name, so the evaluation and evidence-building logic is identical no matter which tools are
configured.
Most users should not hand-edit config files — use Guided setup below instead.
Guided setup (configure by conversation, no file editing)
When a user asks to set up or configure the skill, or to change which tools they connect, run the
setup as a conversation and write the config file for them — they should never need to touch YAML or
the command line. Steps:
- Run
python scripts/configure.py --list to see the roles and the Agent connectors available for
each. Then ask the user, in plain language and one role at a time, which tool they use — offering
the supported connectors as choices. Make it easy: Stripe is already handled; the optional roles
(internal_chat, service_usage, order_data) can be skipped with a simple "we don't use one."
Keep it short — accept the default entity/match_field unless the user has a reason to change it,
and only ask follow-ups (e.g. WooCommerce's billing.email, Amazon's order-id matching) when the
chosen connector needs them (see references/configuration.md).
- Confirm a few settings:
- Digest channel — where the digest posts.
- Money settings — the dispute fee (
stripe.dispute_fee_cents), the minimum amount worth
evaluating (evaluation.min_amount_cents; below it → SKIP), and the high-value FIGHT cutoff
(evaluation.high_value_cents). These are currency-sensitive, so ask first whether the account
processes more than one currency:
- One currency: note which. Set each as a single int in that currency's smallest unit —
cents for 2-decimal currencies (USD/EUR:
$500 → 50000), but whole units for zero-decimal
currencies (JPY/KRW: ¥50,000 → 50000, not 500).
- Multiple currencies: set each as a per-currency map, e.g.
{"USD": 50000, "EUR": 45000, "default": 40000} — one entry per currency they use plus a
default, each in that currency's smallest unit. Don't use a lone scalar across currencies —
50000 would mean $500 for USD but ¥50,000 for JPY. No FX conversion is ever applied.
- Defaults:
min_amount_cents 0 (evaluate everything), high_value_cents 20000, dispute_fee_cents
your Stripe fee. In MCP mode, peek at the currencies on recent disputes via the Stripe
connector and pre-fill the currency list so the user only fills in the ones they actually use.
- Minimum winnability (
evaluation.min_winnability, 0..1) — a currency-independent
probability floor; below it → ACCEPT. Default 0 = no floor; note anything above ~0.6 fights almost
nothing (Stripe's ~60% ceiling).
- Storage — packages + the self-heal knowledge base save locally to
storage.local_dir
(default ~/dispute-fighter-data); only ask if they want a different path.
- Persist their answers. Pick the path that fits the surface:
- Chat / Claude.ai with a Project (persists, no reinstall): build the config, then run
python scripts/configure.py --emit config.json and give the user the emitted block to paste
into their Project's custom instructions (or save as a Project knowledge file). That block is
the durable source of truth; each session the skill rebuilds ./config.json from it (see "Config
in a Project"). Skip the repackage step below — it isn't needed here.
- Claude Code / SDK (writable install): write the answers to
config.json at the skill root
with the file-writing tool. Best practice: start from python scripts/configure.py --scaffold config.json (defaults with every optional role disabled) and edit — configs merge over defaults,
so to disable an optional role set it to null, don't just omit it.
- Validate with
python scripts/configure.py --validate config.json and read back the configured
sources to the user for confirmation. Fix anything it flags.
- Explain how they actually connect to Airbyte — recommend the no-terminal path:
- Preferred (Agent MCP): connect the Airbyte MCP once in their client (Claude Desktop:
Settings → Connectors → Add custom connector →
https://mcp.airbyte.ai/mcp; Claude Code:
claude mcp add --transport http airbyte-agent https://mcp.airbyte.ai/mcp). Auth is browser
OAuth, and each data source is authorized via a secure browser URL — no pip install, and no
credentials entered into a terminal or into chat. Then they authorize the connectors they chose.
- Fallback (SDK): only for headless/cron use —
pip install airbyte-agent-sdk and set
AIRBYTE_CLIENT_ID / AIRBYTE_CLIENT_SECRET in the environment.
Offer to walk them through whichever they pick.
- (Code/SDK installs only — skip on Chat/Project.) Repackage with the config to reinstall or share.
Once the config is written and validated, run
python scripts/package_self.py — it bundles the
skill plus config.json into a
fresh dispute-fighter.skill (refusing if the config somehow contains secret-like keys) and prints
reinstall instructions. Relay those to the user: the output .skill path, and how to reinstall
(Claude Code: unzip -o <path> -d ~/.claude/skills/; Claude Desktop: re-upload it under
Settings → Capabilities → Skills). Be clear that reinstalling is only needed to share the
pre-configured skill or when the install folder is read-only — if config.json was written into an
already-installed, writable skill folder, the skill uses it immediately with no reinstall.
Manual editing still works for power users — copy config.example.yaml or
config.example.json and edit — but the guided flow above is the recommended
path. Full field reference: references/configuration.md.
Config in a Project (Claude.ai / Desktop — persists with no reinstall)
On the Chat surface the installed skill is read-only at runtime and the sandbox filesystem is
wiped between sessions, so a config.json written mid-chat does not persist. Instead, keep the
config in the Project (which does persist) and rebuild the scratch file each session.
Config resolution — at the start of every run, load config in this order:
- A Project config block — look in the Project's custom instructions (and knowledge files) for a
block between the marker lines
=== DISPUTE-FIGHTER CONFIG (do not remove) === and
=== END DISPUTE-FIGHTER CONFIG ===. If present, parse its JSON and write it to ./config.json
in the working dir so the scripts can read it this session (it's scratch — the Project is the
source of truth).
- Otherwise a writable
config.json already next to the skill (Code/SDK installs).
- Otherwise built-in defaults.
To change settings, the user edits the block in the Project — no reinstall, no repackaging. config.json
holds no secrets, so it's safe to keep in the Project; credentials still come from MCP OAuth / env vars.
Testing / no-credentials mode: if you're handed a context JSON file (a saved dump of the
dispute and its related records) instead of live access, skip the fetch step and work from that
file. The evaluation and package-building logic is identical.
Workflow
Step 0 — Identify the dispute(s)
You need a Stripe dispute id (dp_...) or the disputed charge / payment intent (ch_... / pi_...).
If the user gives a customer name, order number, or Zendesk ticket instead, resolve it to a dispute
first. If more than one dispute matches an ambiguous reference, list them and ask which one.
Multiple disputes at once. The user can hand you several ids in one request (e.g. "evaluate
dp_1, dp_2, dp_3" — from the digest, a list, or a paste). When they do, run the full workflow
(Steps 1–4) independently for each dispute, then present a consolidated summary — one row per
dispute (id, amount, reason, verdict FIGHT/ACCEPT/SKIP, deadline, package location) — followed by the
individual recommendations/packages. Keep going through the whole list even if some are ACCEPT/SKIP;
don't stop after the first. Apply the amount threshold (Step 2) to each, and still stop for human
review at the end — never auto-submit any of them.
If the consolidated summary is being shared to Slack, render it as a table: author a JSON of the
results ({"disputes":[{id, amount, currency, reason, verdict, due, package}]}) and run
scripts/evaluation_summary.py, which writes markdown, blocks, and
text (same forms/styling as the digest). Post the markdown string on an OAuth Slack MCP
connector, or blocks via a bot token / SDK. In chat, the plain markdown is fine as-is.
Step 1 — Gather context
Produce a single dispute_context.json keyed by role. How you produce it depends on the access mode:
- MCP mode (preferred): use the connected Airbyte MCP tools to fetch the records below, then write
them into
dispute_context.json yourself (shape per
references/airbyte-data-access.md → "The file handoff"). Don't
run the fetch script — a subprocess can't reach MCP.
- SDK mode (fallback): run scripts/fetch_dispute_context.py
with the dispute id; it reads the config, pulls everything via the SDK, and writes the same file.
Either way, gather:
- Stripe (pinned) — the dispute (reason, amount, status,
evidence_details.due_by, current
evidence), the charge, payment intent, customer, invoice/subscription, line items/product, refunds,
and the charge's outcome / risk signals (AVS, CVC, IP, risk level).
customer_comms — tickets/comments tied to the customer email (proof of contact, refund
offers, cancellation requests, delivery complaints). Default Zendesk; configurable.
policies — the relevant internal policy docs (refund, cancellation, ToS, shipping/SLA).
Default Google Drive; configurable.
internal_chat — internal discussion about this customer or order, if any. Default Slack;
configurable.
service_usage (optional) — product/usage events for the customer (logins, feature use,
downloads). Off unless configured. When present, it's high-value evidence: it populates Stripe's
access_activity_log and is often decisive for subscription_canceled and fraudulent/
unrecognized disputes. Treat its absence as "no usage data," not "no usage."
order_data (optional) — the order behind the charge: line items, the shipping address the
customer provided, fulfillment status, carrier/tracking. Off unless configured. Feeds
product_description, shipping_address/shipping_carrier/shipping_tracking_number, and
receipt; essential for product_not_received / product_unacceptable and for spotting a
gift ship-to mismatch. Default Shopify; also WooCommerce or Amazon Seller Partner.
If the script can't run (no creds in this environment), gather the same fields yourself using the
patterns in the data-access reference, or accept a pre-built dispute_context.json.
Step 2 — Evaluate whether to fight
First, learn from past outcomes (self-heal). Read learnings.md and pitfalls.md from the
storage local_dir (default ~/dispute-fighter-data/) if they exist — they capture, per reason
code, what won and what lost previous disputes. Apply the relevant lessons to this evaluation and to
how you'll build the package. These files are maintained automatically by the daily outcome review
(see "Outcome review & self-heal" below), so the skill gets sharper over time.
Work in the dispute's own currency (no FX). Judge every dispute in its currency. Stripe's
amount is in that currency's smallest unit (cents for USD/EUR/GBP, but whole units for
zero-decimal currencies like JPY/KRW — amount: 1000 = ¥1,000, not ¥10.00). The money config fields
(dispute_fee_cents, min_amount_cents, high_value_cents) are in that same smallest unit and may be
per-currency maps — resolve the entry for this dispute's currency (map[CURRENCY] → default →
the scalar). Never convert between currencies; compare and reason within the one currency.
Amount threshold (check first). If evaluation.min_amount_cents (resolved for this currency) is
set and this dispute's amount is below it, the user has decided it's not worth the effort. Recommend
SKIP with a one-line reason (e.g. "¥30,000 is below your ¥50,000 JPY evaluate threshold — accept
it") and don't build a package — unless the user explicitly asked to evaluate this dispute anyway, in
which case proceed but note it's below threshold. A threshold of 0 (default) means evaluate everything.
Winnability floor (company policy). After you estimate the win probability, compare it to
evaluation.min_winnability (0..1). This is a probability, so it's currency-independent — the same
floor applies to every dispute. If your estimate is below the floor, recommend ACCEPT — the
company won't fight odds that low — and state both numbers (e.g. "estimated ~40% vs. your 50% floor").
A floor of 0.0 (default) imposes no policy limit. The user can override per dispute, but note it.
Economics likewise use this currency's dispute_fee_cents and the disputed amount directly —
EV ≈ win% × amount − fee, all in-currency.
Then read references/evaluation.md for the full rubric, and assess:
- Reason code & base winnability — different reason codes have very different odds and require
different evidence (see the reason-code table in
references/stripe-disputes.md).
- Evidence strength — do we actually have the evidence this reason code needs (e.g. delivery
confirmation for
product_not_received, a disclosed refund policy for subscription_canceled)?
Missing the keystone evidence usually means don't fight.
- Economics — disputed amount vs. the non-refundable dispute fee and the effort; small amounts
with weak evidence are usually not worth contesting.
- Risk / friendly-fraud signals — AVS/CVC match, prior successful charges, product access logs,
prior customer contact all strengthen a
fraudulent/unrecognized rebuttal.
- Deadline — is there time left before
evidence_details.due_by? If it's passed, you can't fight.
- Visa CE 3.0 — for
fraudulent/unrecognized, check whether the dispute is CE 3.0 eligible
(Stripe badges it). If so, prior undisputed transactions with the same cardholder become the
strongest evidence and should raise your assessment. See the disputes reference.
- Realistic ceiling — even strong cases top out around ~60% per Stripe; don't overstate odds.
Output a recommendation using the Recommendation template below. Be decisive and honest — a
well-reasoned "accept the loss" is a valid, valuable answer.
Step 3 — Build the evidence package (only if fighting)
Run scripts/build_evidence_package.py on the context JSON, or
assemble by hand. It produces two artifacts:
evidence.json — keyed by Stripe's exact dispute.evidence field names (verified list in
references/stripe-disputes.md). Only include fields that are
genuinely supported by gathered evidence. Text fields get the actual text; file fields get the
path/Drive link to the supporting document plus a note that it must be uploaded as a Stripe File.
evidence_package.md — a human-readable rebuttal built from
assets/evidence_package_template.md: the narrative
uncategorized_text argument, the field-by-field evidence list, a checklist of files to upload,
and any gaps the reviewer should be aware of.
Map evidence to fields according to the reason-code playbook in the disputes reference — for
example, a product_not_received rebuttal centers on shipping_carrier / shipping_tracking_number
/ shipping_date / shipping_documentation, while duplicate centers on duplicate_charge_id and
duplicate_charge_explanation.
Follow Stripe's submission best practices (see stripe-disputes.md →
"Submission best practices") — they're enforced/surfaced by the build script:
- Concise, chronological narrative in
uncategorized_text; add a one-line "what this proves" per
evidence item (pass {"value": ..., "proves": ...} for text, {"file": ..., "proves": ...} for files).
- One file per evidence type — if several items support one field, pass
{"files": [...]} and the
script flags them to be merged into a single upload.
- Excerpt policies, don't dump or link out; include a checkout-presentation screenshot for
disclosure fields. The script warns if a policy file is an external URL.
- Visa CE 3.0: for
fraudulent/unrecognized, if eligible, pass a visa_ce3 block with the
qualifying prior transactions. For an already-refunded charge, pass a credit_issued block.
- The script also emits Stripe's file-format requirements (PDF/JPEG/PNG, <4.5 MB, <50 pages,
portrait, 12-pt+) into the package's upload checklist.
Step 3.5 — Save the package
After building, save the package so the outcome loop can track it. Put the generated files
(recommendation.md, evidence.json, evidence_package.md) in one folder, then:
python scripts/save_package.py --dispute <dp_id> --dir <that folder> \
--reason <reason_code> --amount <amount> --currency <CUR>
This copies the package into local storage (storage.local_dir, default ~/dispute-fighter-data/,
under pending/dp_<id>/) and records the dispute in the local ledger as pending. Storage is local
in both access modes — the same folder holds learnings.md, pitfalls.md, and the ledger.
Step 4 — Hand off for review
Summarize: the recommendation, the strongest evidence, the weakest points, what files still need to
be uploaded, and the due_by deadline. Note where the package was saved. Stop here for the human.
Recommendation template
ALWAYS use this structure for the evaluation output:
# Dispute <dp_id> — Recommendation: FIGHT | ACCEPT | SKIP (below threshold)
**Reason code:** <code> — <plain-English meaning>
**Amount:** <amount currency> **Dispute fee:** <fee> **Respond by:** <due_by> (<days left>)
## Verdict
<One-paragraph call: fight or accept, and the core reason.>
## Why
- Winnability: <base odds for this reason code + how our evidence shifts them>
- Evidence we have: <bullets>
- Evidence we're missing: <bullets, or "none material">
- Economics: <amount vs. fee/effort>
## If fighting, the package will center on
<the key Stripe evidence fields for this reason code>
Daily digest (triage to Slack)
Post a once-a-day summary of disputes that need a response, as a table, so the team sees what's on the
clock without opening Stripe. This is lightweight triage, not full evaluation — the point is to
surface and prioritize, then a human picks ones to work and runs the full Evaluate/Build flow.
Incremental by default: each run shows disputes opened since the last time the digest was posted
(tracked by a local digest_state.json watermark), so the channel isn't spammed with the same disputes
daily. Deadline safeguard: it also re-surfaces any still-open dispute that has entered the
"due soon" (red) window even if it was posted before — flagged as a due-soon reminder — so nothing
slips toward its deadline unseen. Pass --all for a full digest of every open dispute.
When to run: on a daily schedule (see "Scheduling" below) or whenever someone asks for a summary of
disputes needing attention. The daily run also performs the outcome review & self-heal below.
Outcome review & self-heal (runs with the daily digest)
Closed disputes are how the skill learns. As part of every daily run, after (or before) posting the
digest:
-
Scan for resolved disputes. In MCP mode, look up the current Stripe status of each
still-pending dispute in the ledger via the Airbyte MCP, write a {dispute_id: status} map, and
run scripts/review_outcomes.py --scan --outcomes-file <map>.json. In
SDK mode, run --scan alone (it queries Stripe via the SDK). It prints a worklist of disputes
now won or lost, and only returns still-pending ones, so nothing is reviewed twice.
-
Record an anonymized lesson per resolved dispute. For each item in the worklist, read its saved
package and append one concise, pattern-level, PII-free bullet to the knowledge file:
- won →
learnings.md (what evidence/argument worked for that reason code)
- lost →
pitfalls.md (what was missing or unconvincing)
Keep it general enough to help future disputes (e.g. "subscription_canceled: wins needed the
checkout cancellation-policy screenshot, not just the policy text"). No customer names/emails.
-
Finalize (move + mark). Run review_outcomes.py --finalize <dp_id> won|lost. This moves the
package into the won/ or lost/ folder and flips its ledger status, so it's never reviewed again.
These learnings.md / pitfalls.md files are read back in Step 2 of the main workflow, closing
the loop: outcomes shape future recommendations and packages.
How it works
-
Pull open disputes. In MCP mode, fetch all disputes with status needs_response /
warning_needs_response (with each one's charge/customer for amount and reason) via the Airbyte MCP
tools, write them to a dump, and run
scripts/daily_dispute_digest.py --from-file <dump>.json. In
SDK mode, just run the script (it fetches via the SDK). Fetch all actionable disputes (not
only new ones) so the script can apply the incremental filter and the deadline safeguard: it
keeps disputes created after the last_posted_at watermark (digest_state.json) plus any
still-open dispute now within the "due soon" window (marked a reminder). Use --all to include
every open dispute regardless. It then computes a per-dispute quick lean and urgency.
- Quick lean (
FIGHT / ACCEPT / REVIEW / SKIP) is a cheap heuristic from the reason code's
base winnability (see references/stripe-disputes.md), amount, and
the min_winnability floor — it deliberately does not gather evidence. SKIP means below
evaluation.min_amount_cents.
- Urgency is driven by
evidence_details.due_by.
-
Format the message. The script writes digest.json with three ready-to-post forms: markdown
(a |-delimited Markdown table — the default to post), blocks (native Block Kit table,
bot-token/SDK only), and text (a plain ASCII code-block table, last-resort fallback). Disputes
are sorted by soonest deadline then largest amount. The script also prints the markdown form to
stdout by default.
-
Post to Slack — default to the markdown table. Unless you are posting through a raw
SLACK_BOT_TOKEN, post the markdown string from digest.json as the message. This is the
default and the right choice for the normal user path — posting via an OAuth MCP Slack connector
(Cowork / Claude Desktop, which posts as the user, has no bot token, and does not accept
blocks). The connector renders the Markdown table as a native Slack table. Do not pass blocks
on the connector path (it's dropped/rejected), and do not post the ASCII text here.
- Only with a bot token / SDK: use
--post, which sends blocks (native Block Kit table)
with text as the notification fallback.
- The plain
text (ASCII code-block) form is a last resort — use it only on a surface that
renders neither Markdown nor Block Kit.
After a successful post, advance the watermark so the next run only shows newer disputes:
--post does this automatically; if you posted the markdown via a connector, run
daily_dispute_digest.py --mark-posted. Don't @-mention individuals unless asked.
Digest format
The disputes render as a table in all three forms (markdown pipe table, blocks native table, text
code-block). Structure:
- Header:
🛡️ Disputes needing a response — {date}.
- Summary:
{N} new since last digest · {R} due-soon reminder(s) · {totals} at risk · {M} due within 48h, where {totals} lists per-currency amounts (e.g. USD 129.00 + EUR 120.00)
— currencies are never summed together (no FX conversion). The reminder count shows only when
re-surfaced disputes are present; says "open" instead of "new since last digest" under --all;
appends "· {k} below evaluate threshold" when any are below the amount floor.
- Table — columns Dispute · Amount · Reason · Due · Lean · Customer. In the
markdown and
blocks forms the Due cell is prefixed with an urgency dot (🔴 ≤ red threshold · 🟠 ≤ orange
· 🟢 beyond · ⚪ no deadline); the plain text code-block omits the dot so its monospace columns stay
aligned. A re-surfaced item is marked "(reminder)". The Lean cell carries the hint
(FIGHT/ACCEPT/REVIEW/SKIP/EXPIRED). (In blocks: Amount right-aligned, Reason/Customer wrap.)
- Footer:
Paste evaluate <dispute-id> into Claude … — copy a Dispute id to work it.
The blocks table caps at ≤100 rows / ≤20 cols / ≤10k chars; very large digests show the first ~90 and
note the overflow (the full list stays in text / digest.json). If there are no new disputes, it's
just the header + "✅ No new disputes since the last digest."
Acting on evaluate dp_<id>: this phrase is the handoff from the digest. When a user sends it, run
the full workflow (Steps 0–4) for that dispute id — the real evaluation, an honest fight/accept
recommendation, and (only if fighting) the evidence package. It is not the digest's quick lean;
gather evidence and decide properly.
Scheduling
The skill describes what to post; a scheduled task makes it recur. Set up a daily run with the
schedule skill (a cron-style cloud agent) whose prompt is roughly: "Run the dispute-fighter daily
digest and post it to the disputes channel." The schedule needs the time, timezone, and target
channel — keep those in the schedule, not hard-coded here, so they're easy to change.
Works on any OS. The recommended path — the Claude client's scheduling feature — runs in the
cloud, so it's platform-independent (Windows, macOS, Linux). If instead you schedule the script
directly on your own machine, use cron on macOS/Linux or Task Scheduler (schtasks) on
Windows; the scripts are pure Python 3.10+ with no UNIX-only dependencies, so they run on Windows
as well. (The #!/usr/bin/env python3 shebang lines are ignored on Windows — invoke with
python scripts\daily_dispute_digest.py.)
Intended default for this workspace: 9:00 AM daily, posting to a dedicated disputes channel
(create one if it doesn't exist yet). Adjust freely when you create the schedule.
Output files
Write artifacts next to the context JSON (or to a user-specified folder):
dispute_context.json, evidence.json, evidence_package.md, digest.json.