From f4c42f29564dc8744edfabaf6bca2a2a4167d0f2 Mon Sep 17 00:00:00 2001 From: Vasanth Date: Wed, 28 Oct 2026 15:01:30 +0530 Subject: [PATCH] rpa-bot-handoff: end with HANDOVER.docx for the ops team New last step: templates/to_docx.py turns the finished HANDOVER.md into a Word document (python-docx), the skill checks it and gives that path to the user as the deliverable. --selfcheck converts the Tebra example and is run by tests/test_templates.py. Co-Authored-By: Claude Opus 5.5 --- skills/rpa-bot-handoff/README.md | 4 + skills/rpa-bot-handoff/SKILL.md | 12 ++- skills/rpa-bot-handoff/templates/to_docx.py | 113 ++++++++++++++++++++ tests/test_templates.py | 8 ++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 skills/rpa-bot-handoff/templates/to_docx.py diff --git a/skills/rpa-bot-handoff/README.md b/skills/rpa-bot-handoff/README.md index 771fb4c..f070c10 100644 --- a/skills/rpa-bot-handoff/README.md +++ b/skills/rpa-bot-handoff/README.md @@ -16,6 +16,8 @@ A Claude skill that writes the **ops guide** for an RPA bot that is being handed 10. Don'ts, and the right way to stop the bot 11. Issues seen before +**`HANDOVER.docx`**: the same ops guide as a Word document. This is what you give the ops team at the end. + **`HANDOVER-DEV.md`**: for developers. The check against 12 failure classes learned from Tebra Auto Post, the gaps to fix before handover (file:line), and the incident list. ## Install @@ -26,6 +28,7 @@ In your bot project, ask Claude: *"write the ops guide for this bot"* or *"docum 1. Scans the code and history with subagents. It never runs the bot. 2. Asks up to three things: how ops reaches the dev team, what ops can open, and any unclear statuses. 3. Writes both files and checks them. +4. Turns the ops guide into `HANDOVER.docx` and gives you that document to hand to ops. ## Files | File | What it is | @@ -35,3 +38,4 @@ In your bot project, ask Claude: *"write the ops guide for this bot"* or *"docum | `templates/HANDOVER-DEV.md` | The dev notes skeleton | | `templates/failure-classes.md` | 12 failure classes from Tebra's first months with ops | | `templates/reference/tebra-auto-post.md` | Both files filled in for Tebra, scrubbed | +| `templates/to_docx.py` | Turns `HANDOVER.md` into `HANDOVER.docx` (`--selfcheck` tests it) | diff --git a/skills/rpa-bot-handoff/SKILL.md b/skills/rpa-bot-handoff/SKILL.md index 075ac24..7c954fe 100644 --- a/skills/rpa-bot-handoff/SKILL.md +++ b/skills/rpa-bot-handoff/SKILL.md @@ -15,6 +15,7 @@ Write two files in the bot's repo: | `templates/HANDOVER-DEV.md` | The dev notes skeleton. | | `templates/failure-classes.md` | 12 failure classes from Tebra Auto Post. Every one must be checked against the bot. | | `templates/reference/tebra-auto-post.md` | A filled example of both files for Tebra, scrubbed. Copy its depth and wording, not its content. | +| `templates/to_docx.py` | Turns the finished `HANDOVER.md` into `HANDOVER.docx`, the document handed to ops. Needs `python-docx`. | Rules: - **Never run the bot.** Real bots act on live systems. Read code, config, logs and history only. @@ -22,7 +23,7 @@ Rules: - **Every wait is a number.** Take timeouts, retries and watchdog limits from the code, in seconds or minutes. Take normal durations from the logs when they show them; otherwise write "not measured". - **Write what the code does, not what it should do.** If a status is misleading (an item needing follow-up shows green), say so plainly in the ops guide and add it to the gaps in `HANDOVER-DEV.md`. - **Never copy** patient data (PHI), customer names, credentials, internal IPs, keys or `.env` values into either file. Logs are read for message types and durations only. -- Change no code. The only output is the two files. +- Change no code. The only output is the two files and `HANDOVER.docx`, the Word copy of the ops guide that is handed to ops at the end. ## 1. Scan (subagents are required) @@ -86,4 +87,11 @@ Copy `templates/HANDOVER-DEV.md` and fill it: 4. Search both files for PHI, customer names, credentials, IPs, keys and `.env` values. There must be none. 5. The ops guide has no file:line, class or function names, or SQL. 6. `HANDOVER-DEV.md` accounts for all of F1-F12. -7. Report to the user: sections filled, number of exceptions listed, failure classes covered vs not applicable, and the gaps for the dev team. + +## 6. Hand over the document + +The skill always ends with one document the user can give to ops. +1. Run `python /templates/to_docx.py HANDOVER.md`. It writes `HANDOVER.docx` next to it. If `python-docx` is missing, run `pip install python-docx` first. If that is not allowed, say so and hand over `HANDOVER.md` instead. +2. Open the `.docx` with python-docx and check that it has 11 numbered section headings and that no `**` or `|---` is left in its text. +3. Give the user the path to `HANDOVER.docx` as the deliverable: "Give this to the ops team." `HANDOVER-DEV.md` stays in the repo for developers and is not handed to ops. +4. Report: sections filled, number of exceptions listed, failure classes covered vs not applicable, and the gaps for the dev team. diff --git a/skills/rpa-bot-handoff/templates/to_docx.py b/skills/rpa-bot-handoff/templates/to_docx.py new file mode 100644 index 0000000..7ab6052 --- /dev/null +++ b/skills/rpa-bot-handoff/templates/to_docx.py @@ -0,0 +1,113 @@ +"""Turn the ops guide (HANDOVER.md) into a Word document ops can open, print and pass around. + + python to_docx.py HANDOVER.md # writes HANDOVER.docx next to it + python to_docx.py HANDOVER.md -o guide.docx + python to_docx.py --selfcheck + +Handles what the template uses: # headings, paragraphs, - bullets, - [ ] checkboxes, 1. steps, +| tables |, ``` blocks, **bold** and `code`. Needs python-docx (pip install python-docx). +""" +import argparse +import re +import sys +import tempfile +from pathlib import Path + +from docx import Document +from docx.shared import Pt + +INLINE = re.compile(r"(\*\*[^*]+\*\*|`[^`]+`)") + + +def add_runs(par, text): + for part in INLINE.split(text): + if part.startswith("**") and part.endswith("**"): + par.add_run(part[2:-2]).bold = True + elif part.startswith("`") and part.endswith("`"): + par.add_run(part[1:-1]).font.name = "Consolas" + elif part: + par.add_run(part) + + +def cells(line): + return [c.strip() for c in line.strip().strip("|").split("|")] + + +def convert(md_text, out_path): + doc = Document() + doc.styles["Normal"].font.name = "Calibri" + doc.styles["Normal"].font.size = Pt(10.5) + lines = md_text.splitlines() + i = 0 + while i < len(lines): + line = lines[i] + if line.startswith("```"): + block = [] + i += 1 + while i < len(lines) and not lines[i].startswith("```"): + block.append(lines[i]) + i += 1 + run = doc.add_paragraph().add_run("\n".join(block)) + run.font.name = "Consolas" + run.font.size = Pt(9.5) + elif line.startswith("|"): + rows = [] + while i < len(lines) and lines[i].startswith("|"): + if not re.match(r"^\|[\s:|-]+\|\s*$", lines[i]): # skip the |---| separator + rows.append(cells(lines[i])) + i += 1 + i -= 1 + width = max(len(r) for r in rows) + table = doc.add_table(rows=len(rows), cols=width) + table.style = "Table Grid" + for r, row in enumerate(rows): + for c, text in enumerate(row): + par = table.cell(r, c).paragraphs[0] + add_runs(par, text) + if r == 0: + for run in par.runs: + run.bold = True + elif m := re.match(r"^(#{1,3})\s+(.*)", line): + doc.add_heading(m[2], level=len(m[1]) - 1 if len(m[1]) > 1 else 0) + elif m := re.match(r"^\s*- \[ \]\s+(.*)", line): + add_runs(doc.add_paragraph(style="List Bullet"), "☐ " + m[1]) + elif m := re.match(r"^\s*[-*]\s+(.*)", line): + add_runs(doc.add_paragraph(style="List Bullet"), m[1]) + elif m := re.match(r"^\s*\d+\.\s+(.*)", line): + add_runs(doc.add_paragraph(style="List Number"), m[1]) + elif line.strip() and line.strip() != "---": + add_runs(doc.add_paragraph(), line.strip()) + i += 1 + doc.save(out_path) + return doc + + +def selfcheck(): + example = Path(__file__).parent / "reference" / "tebra-auto-post.md" + guide = example.read_text(encoding="utf-8").split("# File 2:")[0].split("# File 1: `HANDOVER.md`")[1] + out = Path(tempfile.mkdtemp()) / "HANDOVER.docx" + convert(guide, out) + doc = Document(out) + heads = [p.text for p in doc.paragraphs if p.style.name.startswith("Heading")] + assert [h.split(".")[0] for h in heads if h[:1].isdigit()] == [str(n) for n in range(1, 12)], heads + assert len(doc.tables) >= 6, len(doc.tables) + assert doc.tables[0].cell(0, 0).text == "Where", doc.tables[0].cell(0, 0).text + assert not any("**" in p.text or "|---" in p.text for p in doc.paragraphs) + print(f"selfcheck ok: {len(heads)} headings, {len(doc.tables)} tables -> {out}") + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("md", nargs="?") + ap.add_argument("-o", "--out") + ap.add_argument("--selfcheck", action="store_true") + a = ap.parse_args() + if a.selfcheck: + selfcheck() + sys.exit(0) + if not a.md: + ap.error("give the HANDOVER.md path, or --selfcheck") + src = Path(a.md) + out = Path(a.out) if a.out else src.with_suffix(".docx") + convert(src.read_text(encoding="utf-8"), out) + print(f"wrote {out}") diff --git a/tests/test_templates.py b/tests/test_templates.py index b2d99fb..2a303ac 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -2,6 +2,7 @@ import re import shutil import subprocess +import sys from pathlib import Path import pytest @@ -60,3 +61,10 @@ def test_handoff_lists_twelve_failure_classes(): def test_handoff_ops_guide_has_eleven_sections_in_order(): guide = (ROOT / "skills" / "rpa-bot-handoff" / "templates" / "HANDOVER.md").read_text(encoding="utf-8") assert [int(n) for n in re.findall(r"^## (\d+)\.", guide, re.MULTILINE)] == list(range(1, 12)) + + +def test_handoff_docx_selfcheck(): + pytest.importorskip("docx") + script = ROOT / "skills" / "rpa-bot-handoff" / "templates" / "to_docx.py" + r = subprocess.run([sys.executable, str(script), "--selfcheck"], capture_output=True, text=True) + assert r.returncode == 0, r.stderr