Add rpa-bot-handoff skill: ops guide for bot handover

The skill scans a bot with subagents (never runs it) and writes
HANDOVER.md, an 11-section ops guide in the operator's order: where is
what, how to run, how long to wait, after the run, how to see an
exception, exception list, how you will know, how to tell us, don'ts,
issues seen before. Dev material (check against 12 failure classes from
Tebra Auto Post, gaps with file:line, incidents) goes to HANDOVER-DEV.md.

Also adds ADR-0004, glossary terms, five fixture bots and a handoff eval
runner (--resume, --regrade) with results from 47 runs of the earlier
13-section version. The grader still expects that structure and must be
updated before the next eval run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-28 15:01:42 +05:30
1 parent 3d2db6aca6
commit 3942fad6cd
143 files changed
+11815 -2

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
CLAIMDESK_EXE=C:\Program Files\ClaimDesk\ClaimDesk.exe
ALERT_SMTP=10.20.30.40
CLAIMDESK_PASSWORD_FALLBACK=Fx9-Secret-Pass
+7
View File
@@ -0,0 +1,7 @@
# ClaimDesk Poster
Posts remittance payments into the ClaimDesk desktop app, clinic by clinic, from the input Excel
(sheets Login, Clinics, Settings). Results go to the Report screen of the launcher window
"ClaimDesk Poster" (`report_screen.py`). Database: `data/poster.db` (SQLite).
Setup: Python 3.11, `pip install -r requirements.txt`, a `.env` with `CLAIMDESK_EXE`.
+6
View File
@@ -0,0 +1,6 @@
[
{"msg": "fix: bot grabbed the SQL browser window whose title contains ClaimDesk, then closed the real ClaimDesk", "file": "bot.py"},
{"msg": "docs: clinic names containing commas are split and skipped - ops told to put one clinic per row", "file": "README.md"},
{"msg": "fix: raise save wait from 10s to 20s - 'Payment not confirmed after Save' rows had really posted", "file": "bot.py"},
{"msg": "fix: users stayed on 1.3.0 after an update - launcher kept the old folder until restart", "file": "report_screen.py"}
]
+184
View File
@@ -0,0 +1,184 @@
"""ClaimDesk Poster: posts remittance payments into the ClaimDesk desktop app.
Started from the "ClaimDesk Poster" launcher window: python bot.py --excel data/input/input.xlsx
"""
import argparse
import logging
import os
import sqlite3
import threading
import time
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv
from openpyxl import load_workbook
from pywinauto import Application, Desktop
ROOT = Path(__file__).parent
LOG_DIR = ROOT / "logs"
DB = ROOT / "data" / "poster.db"
SAVE_WAIT_S = 20
SCREEN_WAIT_S = 60
CLINIC_ATTEMPTS = 3
WATCHDOG_S = 300
# Item status -> stored file_status (the colour on the Report screen)
STATUS_MAP = {
"Posted": "Completed",
"Rejected by app": "Completed", # ClaimDesk "Remittance Errors" screen: cancelled, not posted
"Skipped": "Completed",
"Deferred": "Deferred",
"Error": "Error",
}
log = logging.getLogger("poster")
_last_beat = time.monotonic()
def beat():
global _last_beat
_last_beat = time.monotonic()
def watchdog():
while True:
time.sleep(10)
if time.monotonic() - _last_beat > WATCHDOG_S:
log.error("[WATCHDOG] no progress for %ss - killing run", WATCHDOG_S)
os._exit(3)
def read_input(path):
wb = load_workbook(path, data_only=True)
if "Login" not in wb.sheetnames:
raise RuntimeError("'Login' sheet not found")
user, pwd = wb["Login"]["A2"].value, wb["Login"]["B2"].value
if not user or not pwd:
raise RuntimeError("UserName/Password missing in Login sheet (row 2)")
clinics = []
for row in wb["Clinics"].iter_rows(min_row=2, values_only=True):
if row[0]:
clinics += [c.strip() for c in str(row[0]).replace(";", ",").split(",")]
return user, pwd, clinics, wb["Settings"]["B1"].value
def find_claimdesk():
for w in Desktop(backend="uia").windows():
if "ClaimDesk" in w.window_text():
return w
raise RuntimeError("ClaimDesk window not found")
def login(user, pwd):
Application(backend="uia").start(os.environ["CLAIMDESK_EXE"])
win = Desktop(backend="uia").window(title_re="ClaimDesk Login")
try:
win.wait("visible", timeout=SCREEN_WAIT_S)
except Exception:
raise RuntimeError("Login window not found")
win.child_window(auto_id="txtUser").set_text(user)
win.child_window(auto_id="txtPass").set_text(pwd)
win.child_window(title="Sign in").click_input()
return find_claimdesk()
def open_clinic(win, name):
link = win.child_window(title=name, control_type="Hyperlink")
if not link.exists(timeout=10):
raise RuntimeError(f"Clinic '{name}' not found in list")
link.click_input()
if name not in win.window_text():
raise RuntimeError(f"Wrong clinic opened, leaving Pending: {name}")
def unposted_items(win):
grid = win.child_window(auto_id="gridRemits")
if not grid.exists(timeout=SCREEN_WAIT_S):
raise RuntimeError("Remittance grid did not load")
for r in grid.children():
cells = [c.window_text() for c in r.children()]
yield {"check_no": cells[0], "check_date": cells[1], "method": cells[2]}
def post_one(win, item, cutoff):
if item["method"] != "ACH":
return "Skipped", f"Payment method '{item['method']}' - not posted", ""
if cutoff and item["check_date"] > str(cutoff):
return "Deferred", f"Check date {item['check_date']} is after the cutoff - deferred", ""
win.child_window(title="Post Payment").click_input()
if win.child_window(title="Remittance Errors").exists(timeout=5):
win.child_window(title="Cancel").click_input()
return "Rejected by app", "Remittance Errors on posting", "RemitErrors screen shown"
save = win.child_window(title="Save", control_type="Button")
if not save.exists(timeout=10):
return "Error", "Save button not found", "Save button missing on payment screen"
save.click_input()
if not win.child_window(title="Payment saved").exists(timeout=SAVE_WAIT_S):
return "Error", "Payment not confirmed after Save", f"no confirm text within {SAVE_WAIT_S}s"
return "Posted", "Posted successfully", ""
def log_row(db, batch_id, clinic, item, status, comment, tech):
db.execute("INSERT INTO report(batch_id, clinic, check_no, status, file_status, comments, tech_comments) "
"VALUES (?,?,?,?,?,?,?)",
(batch_id, clinic, item["check_no"], status, STATUS_MAP.get(status, "Pending"), comment, tech))
db.commit()
def run_clinic(db, batch_id, user, pwd, clinic, cutoff):
for attempt in range(1, CLINIC_ATTEMPTS + 1):
if attempt > 1:
log.info("[AUTO-RERUN] attempt %s for '%s'", attempt, clinic)
try:
win = login(user, pwd)
open_clinic(win, clinic)
for item in unposted_items(win):
beat()
status, comment, tech = post_one(win, item, cutoff)
log_row(db, batch_id, clinic, item, status, comment, tech)
db.execute("UPDATE clinics SET status='Success' WHERE batch_id=? AND name=?", (batch_id, clinic))
db.commit()
return
except Exception as e:
log.error("[ERROR] Clinic '%s' : %s", clinic, e)
finally:
os.system("taskkill /im ClaimDesk.exe /f >nul 2>&1")
log.error("[DB] Clinic '%s' left Pending after %s attempts", clinic, CLINIC_ATTEMPTS)
def batch_complete(db, batch_id):
open_rows = db.execute("SELECT COUNT(*) FROM report WHERE batch_id=? AND file_status IN ('Error','Pending')",
(batch_id,)).fetchone()[0]
pending = db.execute("SELECT COUNT(*) FROM clinics WHERE batch_id=? AND status='Pending'", (batch_id,)).fetchone()[0]
return open_rows == 0 and pending == 0
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--excel", required=True)
args = ap.parse_args()
(ROOT / "BOT_WAS_RUN").write_text("1")
load_dotenv(ROOT / ".env")
LOG_DIR.mkdir(exist_ok=True)
logging.basicConfig(filename=LOG_DIR / f"run_{datetime.now():%Y%m%d_%H%M%S}.log", level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s")
db = sqlite3.connect(DB)
if db.execute("SELECT COUNT(*) FROM batches WHERE status='Pending'").fetchone()[0]:
raise SystemExit("A Pending batch exists - finish or clear it before a new upload")
user, pwd, clinics, cutoff = read_input(args.excel)
batch_id = db.execute("INSERT INTO batches(status) VALUES ('Pending')").lastrowid
for c in clinics:
db.execute("INSERT INTO clinics(batch_id, name, status) VALUES (?,?,'Pending')", (batch_id, c))
db.commit()
threading.Thread(target=watchdog, daemon=True).start()
for c in clinics:
run_clinic(db, batch_id, user, pwd, c, cutoff)
if batch_complete(db, batch_id):
db.execute("UPDATE batches SET status='Completed' WHERE id=?", (batch_id,))
db.commit()
log.info("[RUN SUMMARY] batch %s done", batch_id)
if __name__ == "__main__":
main()
@@ -0,0 +1,8 @@
2026-09-01 10:15:00 [INFO] Batch 41 started, 3 clinics
2026-09-01 10:15:40 [INFO] Clinic 'North Clinic' opened
2026-09-01 10:16:02 [INFO] Posting check 88231 for patient Johnathan Placeholder
2026-09-01 10:16:30 [ERROR] [ERROR] Clinic 'North Clinic' : Payment not confirmed after Save
2026-09-01 10:20:11 [WARNING] could not bring window to foreground
2026-09-01 10:25:00 [ERROR] [ERROR] Clinic 'South & East Clinic' : Wrong clinic opened, leaving Pending: South & East Clinic
2026-09-01 10:30:00 [INFO] [AUTO-RERUN] attempt 2 for 'South & East Clinic'
2026-09-01 10:40:00 [ERROR] [WATCHDOG] no progress for 300s - killing run
+13
View File
@@ -0,0 +1,13 @@
"""Report screen of the ClaimDesk Poster launcher: a table over the report table, filtered by batch."""
import sqlite3
from pathlib import Path
DB = Path(__file__).parent / "data" / "poster.db"
COLUMNS = ["clinic", "check_no", "file_status", "comments"] # what ops sees
COLOURS = {"Completed": "green", "Deferred": "grey", "Error": "red", "Pending": "amber"}
VERSION = "1.4.2" # shown in the launcher title bar
def rows(batch_id):
db = sqlite3.connect(DB)
return db.execute(f"SELECT {', '.join(COLUMNS)} FROM report WHERE batch_id=?", (batch_id,)).fetchall()
+3
View File
@@ -0,0 +1,3 @@
pywinauto
openpyxl
python-dotenv