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:
1 parent
3d2db6aca6
commit
3942fad6cd
143 files changed
+11815
-2
No files matched your search
+2
@@ -0,0 +1,2 @@
|
||||
LEDGER_USER=bot01
|
||||
LEDGER_PASSWORD=Lp-Fake-77
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# LedgerBot
|
||||
|
||||
Reconciles the daily bank statement Excel with LedgerPro (desktop accounting app) and posts the
|
||||
missing entries by clicking LedgerPro's buttons (image matching, `assets/*.png`).
|
||||
Shipped as `LedgerBot.exe` built with PyInstaller (`ledgerbot.spec`); users start `launcher.exe`,
|
||||
which checks for updates first. Output: `data/output/Reconcile_<time>.xlsx` (Result: OK, Missing, Mismatch, Error).
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"msg": "fix: update never applied on two laptops - launcher swallowed the SSL error from the self-signed update server", "file": "launcher.py"},
|
||||
{"msg": "fix: clicks missed Save on laptops at 125% display scaling - recaptured buttons at 100%, ops must keep 100%", "file": "app.py"},
|
||||
{"msg": "fix: assets missing in the exe build - add datas to the spec", "file": "ledgerbot.spec"}
|
||||
]
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
"""Ledger reconciler: compares the bank statement Excel with LedgerPro and posts missing entries.
|
||||
Shipped as LedgerBot.exe (PyInstaller) and started through launcher.exe."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pyautogui
|
||||
from openpyxl import Workbook, load_workbook
|
||||
|
||||
VERSION = "3.2.0" # shown in the window title "LedgerBot v3.2.0"
|
||||
FROZEN = getattr(sys, "frozen", False)
|
||||
BASE = Path(sys.executable).parent if FROZEN else Path(__file__).parent
|
||||
ASSETS = Path(getattr(sys, "_MEIPASS", BASE)) / "assets"
|
||||
SHORTCUT_DIRS = [Path(os.environ.get("USERPROFILE", "")) / "Desktop", Path(r"C:\Users\Public\Desktop")]
|
||||
CONFIDENCE = 0.9 # image match; buttons were captured at 100% display scaling, 1920x1080
|
||||
FIND_WAIT_S = 20
|
||||
TOLERANCE = 0.01
|
||||
|
||||
logging.basicConfig(filename=BASE / "logs" / f"ledger_{datetime.now():%Y%m%d}.log", level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s")
|
||||
log = logging.getLogger("ledger")
|
||||
|
||||
|
||||
def find_shortcut():
|
||||
for d in SHORTCUT_DIRS:
|
||||
lnk = d / "LedgerPro.lnk"
|
||||
if lnk.exists():
|
||||
return lnk
|
||||
raise RuntimeError("LedgerPro shortcut not found on the Desktop")
|
||||
|
||||
|
||||
def click(image):
|
||||
end = time.time() + FIND_WAIT_S
|
||||
while time.time() < end:
|
||||
hit = pyautogui.locateCenterOnScreen(str(ASSETS / image), confidence=CONFIDENCE)
|
||||
if hit:
|
||||
pyautogui.click(hit)
|
||||
return
|
||||
time.sleep(1)
|
||||
pyautogui.screenshot(str(BASE / "logs" / f"notfound_{image}"))
|
||||
raise RuntimeError(f"Button {image} not found on screen")
|
||||
|
||||
|
||||
def ledger_entries(path):
|
||||
"""LedgerPro export (File > Export > Excel) saved by the bot to data/ledger_export.xlsx."""
|
||||
ws = load_workbook(path, data_only=True).active
|
||||
return {str(r[0]): float(r[1]) for r in ws.iter_rows(min_row=2, values_only=True) if r[0]}
|
||||
|
||||
|
||||
def reconcile(statement, ledger):
|
||||
for ref, amount in statement.items():
|
||||
if ref not in ledger:
|
||||
yield ref, amount, "Missing", "Not in LedgerPro - posting"
|
||||
elif abs(ledger[ref] - amount) > TOLERANCE:
|
||||
yield ref, amount, "Mismatch", f"LedgerPro has {ledger[ref]:.2f}"
|
||||
else:
|
||||
yield ref, amount, "OK", ""
|
||||
|
||||
|
||||
def post_entry(ref, amount):
|
||||
click("new_entry.png")
|
||||
pyautogui.write(ref)
|
||||
pyautogui.press("tab")
|
||||
pyautogui.write(f"{amount:.2f}")
|
||||
click("save_btn.png")
|
||||
|
||||
|
||||
def main(statement_path):
|
||||
(BASE / "BOT_WAS_RUN").write_text("1")
|
||||
subprocess.Popen(["cmd", "/c", "start", "", str(find_shortcut())])
|
||||
time.sleep(15)
|
||||
click("export_excel.png")
|
||||
time.sleep(10)
|
||||
ledger = ledger_entries(BASE / "data" / "ledger_export.xlsx")
|
||||
ws_in = load_workbook(statement_path, data_only=True).active
|
||||
statement = {str(r[0]): float(r[1]) for r in ws_in.iter_rows(min_row=2, values_only=True) if r[0]}
|
||||
out = Workbook()
|
||||
ws = out.active
|
||||
ws.append(["Reference", "Amount", "Result", "Note"])
|
||||
counts = {"OK": 0, "Missing": 0, "Mismatch": 0, "Error": 0}
|
||||
for ref, amount, result, note in reconcile(statement, ledger):
|
||||
if result == "Missing":
|
||||
try:
|
||||
post_entry(ref, amount)
|
||||
note = "Posted"
|
||||
except RuntimeError as e:
|
||||
result, note = "Error", str(e)
|
||||
counts[result] += 1
|
||||
ws.append([ref, amount, result, note])
|
||||
log.info("%s %s %s", ref, result, note)
|
||||
out.save(BASE / "data" / "output" / f"Reconcile_{datetime.now():%Y%m%d_%H%M}.xlsx")
|
||||
print(json.dumps({"summary": counts}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1])
|
||||
@@ -0,0 +1 @@
|
||||
placeholder png
|
||||
@@ -0,0 +1 @@
|
||||
placeholder png
|
||||
@@ -0,0 +1 @@
|
||||
placeholder png
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
"""launcher.exe: checks for a newer LedgerBot build, installs it, then starts LedgerBot.exe."""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from app import VERSION
|
||||
|
||||
UPDATE_URL = "https://updates.example.test/ledgerbot/latest.json"
|
||||
BASE = Path(sys.executable).parent
|
||||
|
||||
|
||||
def latest():
|
||||
try:
|
||||
with urllib.request.urlopen(UPDATE_URL, timeout=10) as r:
|
||||
return json.load(r)
|
||||
except Exception:
|
||||
return None # offline, or the server's certificate is not trusted: run the current build
|
||||
|
||||
|
||||
def main():
|
||||
info = latest()
|
||||
if info and info["version"] != VERSION:
|
||||
zip_path = BASE / "update.zip"
|
||||
urllib.request.urlretrieve(info["zip_url"], zip_path)
|
||||
with zipfile.ZipFile(zip_path) as z:
|
||||
z.extractall(BASE)
|
||||
subprocess.Popen([str(BASE / "LedgerBot.exe"), *sys.argv[1:]])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,2 @@
|
||||
# PyInstaller spec
|
||||
# a = Analysis(["app.py"], datas=[("assets", "assets")])
|
||||
@@ -0,0 +1,3 @@
|
||||
2026-09-05 18:00:20 INFO TX-5512 OK
|
||||
2026-09-05 18:00:31 INFO TX-5513 Error Button save_btn.png not found on screen
|
||||
2026-09-05 18:00:40 INFO TX-5514 Mismatch LedgerPro has 410.00 (Placeholder Traders)
|
||||
@@ -0,0 +1,4 @@
|
||||
pyautogui
|
||||
opencv-python
|
||||
openpyxl
|
||||
pyinstaller
|
||||
Reference in new issue
Block a user