rpa-bot-handoff: email To the developer with their manager in CC, and hand ops a PDF

- Section 5 of the SOP now names a real "To" (the developer) and "CC"
  (the developer's manager, not the operator's), and the email block to
  copy starts with To: and CC: lines. The skill asks for both addresses.
- to_docx.py --pdf also writes the PDF (Word through PowerShell on Windows,
  LibreOffice elsewhere) and prints the real page count against MAX_PAGES.
  The PDF is what ops gets.
- Tebra reference and README updated; new test for the To/CC lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-28 17:07:30 +05:30
1 parent 717f2c40c5
commit 0188de7403
6 files changed
+62 -22

No files matched your search

+33 -3
View File
@@ -1,16 +1,18 @@
"""Turn the Handover SOP (HANDOVER-SOP.md) into a Word document ops can open, print and pass around.
"""Turn the Handover SOP (HANDOVER-SOP.md) into a Word document, and a PDF, ops can open, print and pass around.
python to_docx.py HANDOVER-SOP.md # writes HANDOVER-SOP.docx next to it
python to_docx.py HANDOVER-SOP.md -o "<Project Name> Handover SOP.docx"
python to_docx.py HANDOVER-SOP.md -o "<Project Name> Handover SOP.docx" --pdf # + the .pdf next to it
python to_docx.py --selfcheck
Prints the word count: the SOP must stay at or under MAX_WORDS (about 4 pages).
Prints the word count (at most MAX_WORDS) and, with --pdf, the real page count (at most MAX_PAGES).
The PDF is made by Word on Windows (through PowerShell) or by LibreOffice (soffice) elsewhere.
Handles what the template uses: # headings, paragraphs, - bullets, - [ ] checkboxes, 1. steps,
| tables |, ``` blocks, **bold**, *italic* and `code`. Needs python-docx (pip install python-docx).
"""
import argparse
import re
import subprocess
import sys
import tempfile
from pathlib import Path
@@ -19,6 +21,7 @@ from docx import Document
from docx.shared import Cm, Pt
MAX_WORDS = 1400 # about 4 pages with the margins and table font below
MAX_PAGES = 4
INLINE = re.compile(r"(\*\*[^*]+\*\*|\*[^*\s][^*]*\*|`[^`]+`)")
@@ -96,6 +99,26 @@ def words(md_text):
return len(re.findall(r"[A-Za-z0-9][\w'’.-]*", re.sub(r"^\|[\s:|-]+\|\s*$", "", md_text, flags=re.M)))
def to_pdf(docx_path):
"""Write the PDF next to the docx. Returns (pdf path, page count or None); pdf path is None on failure."""
docx_path = Path(docx_path).resolve()
pdf = docx_path.with_suffix(".pdf")
pages = None
if sys.platform == "win32":
q = lambda p: str(p).replace("'", "''") # PowerShell single-quote escape
ps = (f"$w = New-Object -ComObject Word.Application; $w.Visible = $false; "
f"$d = $w.Documents.Open('{q(docx_path)}', $false, $true); "
f"Write-Output ('pages=' + $d.ComputeStatistics(2)); $d.SaveAs2('{q(pdf)}', 17); "
f"try {{ $d.Close(0) }} catch {{}}; try {{ $w.Quit() }} catch {{}}")
r = subprocess.run(["powershell", "-NoProfile", "-Command", ps], capture_output=True, text=True)
if m := re.search(r"pages=(\d+)", r.stdout):
pages = int(m[1])
else:
subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "--outdir", str(pdf.parent), str(docx_path)],
capture_output=True)
return (pdf if pdf.exists() else None), pages
def selfcheck():
example = Path(__file__).parent / "reference" / "tebra-auto-post.md"
sop = "# " + example.read_text(encoding="utf-8").split("\n---\n", 1)[1].split("\n# ", 1)[1]
@@ -116,6 +139,7 @@ if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("md", nargs="?")
ap.add_argument("-o", "--out")
ap.add_argument("--pdf", action="store_true", help="also write the PDF next to the docx")
ap.add_argument("--selfcheck", action="store_true")
a = ap.parse_args()
if a.selfcheck:
@@ -129,3 +153,9 @@ if __name__ == "__main__":
convert(text, out)
n = words(text)
print(f"wrote {out} ({n} words{'' if n <= MAX_WORDS else f', OVER the {MAX_WORDS}-word limit: cut it'})")
if a.pdf:
pdf, pages = to_pdf(out)
if not pdf:
sys.exit("no PDF: needs Word (Windows) or LibreOffice (soffice). Hand over the docx instead.")
over = pages and pages > MAX_PAGES
print(f"wrote {pdf} ({pages or '?'} pages{f', OVER the {MAX_PAGES}-page limit: cut it' if over else ''})")