{
  "version": 1,
  "generated_at": "2026-06-26T18:12:35.233034+00:00",
  "org": {},
  "departments": [],
  "agents": [],
  "processes": [],
  "foundation": {
    "models": [
      {
        "label": "claude-sonnet-4-6",
        "role": "orchestrator"
      },
      {
        "label": "gpt-5.5",
        "role": "content"
      }
    ],
    "functionalities": [
      {
        "key": "gmail",
        "icon": "📧",
        "title": "Gmail (per-user)",
        "summary": "Szukanie, wysyłka (z potwierdzeniem), drafty, załączniki, etykiety — w imieniu zalogowanego użytkownika.",
        "kind": "tools",
        "count": 9,
        "items": [
          {
            "label": "gmail_add_label_to_email",
            "desc": "Add one or more labels to a Gmail message. `message_id` from gmail_find_email; `label_ids` is comma-separated (get them from gmail_list_labels). System labels include 'STARRED', 'IMPORTANT', 'UNREAD'."
          },
          {
            "label": "gmail_create_draft",
            "desc": "Create a draft (NOT send) in the user's Gmail. Safer than gmail_send_email — Bartosz reviews in Gmail and sends manually. Same arguments as gmail_send_email. `attachment_paths` works the same way (comma-separated file paths, including the f"
          },
          {
            "label": "gmail_download_attachment",
            "desc": "Download a Gmail attachment to the current thread's `downloads/` directory and return its file path. NEVER returns raw base64 into the conversation — that would blow the context limit on any reasonably-sized PDF. For PDFs the tool also auto"
          },
          {
            "label": "gmail_find_email",
            "desc": "Search the user's Gmail mailbox. `q` uses Gmail search syntax (e.g. 'from:client@x.com newer_than:7d', 'has:attachment subject:Federale'). Returns up to `max_results` matches as LIGHTWEIGHT entries (id, thread_id, from, subject, snippet, ha"
          },
          {
            "label": "gmail_forward_email",
            "desc": "Forward a Gmail message — ATOMIC, attachments included automatically. Use this whenever the user says 'przekaż', 'forward', 'wyślij dalej', 'przeslij ten mail' etc. Fetches the source message, downloads ALL attachments from it, builds a pro"
          },
          {
            "label": "gmail_get_current_user",
            "desc": "Return the email address and a few profile fields of the Gmail account the current Slack user has connected. Useful when the user says 'from me' or 'my email'."
          },
          {
            "label": "gmail_list_labels",
            "desc": "List the user's Gmail labels (system labels like INBOX, SPAM, TRASH plus their custom ones). Returns id, name, type. Useful before gmail_add_label_to_email to pick the right id."
          },
          {
            "label": "gmail_list_thread_messages",
            "desc": "Fetch all messages in a Gmail thread as LLM-friendly slim records: from/to/subject/date + plain-text body (HTML stripped if no plain alternative, capped at 50k chars per message) + ATTACHMENT METADATA ONLY (filename, mime, size, attachment_"
          },
          {
            "label": "gmail_send_email",
            "desc": "Send an email from the calling user's Gmail mailbox. The mail goes out AS them (their address). DESTRUCTIVE/EXTERNAL: ALWAYS confirm in Slack first (via send_approval_request) before calling. `body_type` is 'plaintext' or 'html'. `cc`/`bcc`"
          }
        ]
      },
      {
        "key": "notion",
        "icon": "📝",
        "title": "Notion (per-user)",
        "summary": "Szukanie, czytanie jako Markdown, tworzenie i edycja stron, query baz danych.",
        "kind": "tools",
        "count": 6,
        "items": [
          {
            "label": "notion_append_to_page",
            "desc": "Append Markdown content to an existing Notion page. Use this to add notes, sections, follow-ups."
          },
          {
            "label": "notion_create_page",
            "desc": "Create a new Notion page. `parent_id` is the ID of the parent page or database. `title` is the page title. `markdown_content` is the page body (basic Markdown supported)."
          },
          {
            "label": "notion_fetch_page",
            "desc": "Fetch a Notion page's content as Markdown. `page_id` accepts both raw IDs and full URLs."
          },
          {
            "label": "notion_query_database",
            "desc": "Query a Notion database. `filter_json` and `sorts_json` are JSON strings matching the Notion API filter/sorts schema. Pass empty strings for none. Returns rows as list of dicts."
          },
          {
            "label": "notion_search",
            "desc": "Search Notion for pages and databases by title. Returns ID, title, type, and url. Use this first when looking for a doc. `query` is fuzzy."
          },
          {
            "label": "notion_update_page",
            "desc": "Update properties of an existing Notion page (e.g. set Status=Won, change a date, tick a checkbox, edit a select). `properties_json` is a JSON object: property name → new value. Values use simple Python types: string for text/select/status/"
          }
        ]
      },
      {
        "key": "drive",
        "icon": "💾",
        "title": "Google Drive (per-user)",
        "summary": "Szukanie, czytanie, pobieranie, upload, foldery, rename/move/share, usuwanie (z potwierdzeniem).",
        "kind": "tools",
        "count": 12,
        "items": [
          {
            "label": "gdrive_create_folder",
            "desc": "Create a new folder in Google Drive under the calling user's account. `parent_id` is empty to create at the root of My Drive, or a folder id to create inside another folder. Returns the new folder's id and webViewLink."
          },
          {
            "label": "gdrive_delete",
            "desc": "Move a Google Drive file to the trash (reversible — file restorable within ~30 days from Drive UI). `file_id` is the file's id. DESTRUCTIVE: confirm via send_approval_request before calling. Use empty trash from the UI for a hard delete."
          },
          {
            "label": "gdrive_download",
            "desc": "Download a Google Drive file into the workspace (workspace/downloads/). Use for binary files (PDF, images, xlsx) you want to process with bash/file tools. `file_id` from gdrive_search. Google-native docs are exported (Docs→PDF, Sheets→xlsx)"
          },
          {
            "label": "gdrive_export_as",
            "desc": "Export a Google-native Drive doc (gdoc/gsheets/gslides) into a specific Office format and save it to the workspace. `target_format` is one of: docx, xlsx, pptx, pdf, txt, rtf. Use this when you need an editable copy of a Google Docs/Slides"
          },
          {
            "label": "gdrive_list_folder",
            "desc": "List the contents of a Google Drive folder. `folder_id` is the folder's ID. WORKS FOR ANY folder — My Drive folders AND shared drive folders (Companies, People, etc.) — no special syntax needed. To list a shared drive's root, use the shared"
          },
          {
            "label": "gdrive_move",
            "desc": "Move a Google Drive file or folder to a different parent folder. `file_id` plus `new_parent_id`. The file's old parents are removed. Returns the updated metadata."
          },
          {
            "label": "gdrive_read_file",
            "desc": "Read the text content of a Google Drive file. Google Docs/Sheets/Slides are exported as text/CSV. Plain-text files are returned directly. For binary files (PDF, images) use gdrive_download instead. `file_id` from gdrive_search/gdrive_list_f"
          },
          {
            "label": "gdrive_rename",
            "desc": "Rename a Google Drive file or folder. `file_id` plus `new_name`. Returns the updated metadata."
          },
          {
            "label": "gdrive_search",
            "desc": "Search Google Drive for files and folders. SEARCHES BOTH My Drive AND ALL shared drives the user has access to (Companies, People, project-specific shared drives, etc.) — you do NOT need to specify a drive ID, `supportsAllDrives=true` is se"
          },
          {
            "label": "gdrive_share",
            "desc": "Share a Google Drive file or folder with another user. `file_id` is the file's id. `email` is the recipient. `role` is 'reader', 'commenter', or 'writer'. `notify` sends a Drive notification email. DESTRUCTIVE/EXTERNAL: confirm via send_app"
          },
          {
            "label": "gdrive_update_file",
            "desc": "Replace the content of an existing Drive file with a new version from the workspace. `file_id` is the Drive file id. `workspace_path` is a path under workspace/. Preserves the file id and shareable link — only the content changes. DESTRUCTI"
          },
          {
            "label": "gdrive_upload_file",
            "desc": "Upload a file from the workspace to Google Drive under the calling user's account. `workspace_path` is a path under workspace/ (e.g. 'uploads/report.pdf'). `parent_id` is an empty string for My Drive root, or a folder id. `name` overrides t"
          }
        ]
      },
      {
        "key": "sheets",
        "icon": "📊",
        "title": "Google Sheets (per-user)",
        "summary": "Czytanie i zapis danych, dodawanie wierszy, filtrowanie po kolumnach.",
        "kind": "tools",
        "count": 4,
        "items": [
          {
            "label": "sheets_append_row",
            "desc": "Append rows to the bottom of a Google Sheets range. `values_json` is a JSON array of rows. Sheets finds the last row in the given range and inserts after it. Same value semantics as sheets_write (USER_ENTERED — formulas and dates parse)."
          },
          {
            "label": "sheets_find_rows",
            "desc": "Find rows in a Google Sheets range where a specific column equals a value. `column_index` is 0-based (0 = first column in the range). Returns up to `max_results` matching rows with their row index inside the range. Useful for lookups like '"
          },
          {
            "label": "sheets_read",
            "desc": "Read values from a Google Sheets range. `spreadsheet_id` is the ID from the URL (docs.google.com/spreadsheets/d/<ID>/...). `range_a1` is in A1 notation, e.g. 'Sheet1!A1:D100' or 'A1:D'. Returns a list of rows (each row a list of cell values"
          },
          {
            "label": "sheets_write",
            "desc": "Write values to a Google Sheets range, replacing whatever was there. `values_json` is a JSON array of rows (e.g. '[[\"a\",\"b\"],[\"c\",\"d\"]]'). Values are interpreted by Sheets as if the user had typed them — formulas work, dates parse. DESTRUCT"
          }
        ]
      },
      {
        "key": "calendar",
        "icon": "📅",
        "title": "Google Calendar (per-user)",
        "summary": "Wydarzenia: lista, szukanie, tworzenie, edycja, usuwanie.",
        "kind": "tools",
        "count": 8,
        "items": [
          {
            "label": "calendar_create_event",
            "desc": "Create a Google Calendar event in the calling user's calendar. `calendar_id` defaults to 'primary'. `summary` is the event title. `start` and `end` are ISO 8601 datetimes (e.g. '2026-06-07T14:00:00+02:00'). `description` and `location` are"
          },
          {
            "label": "calendar_delete_event",
            "desc": "Delete a Google Calendar event. `calendar_id` defaults to 'primary'. `event_id` from calendar_list_events. Sends cancellation notice to attendees by default. DESTRUCTIVE/IRREVERSIBLE: confirm via send_approval_request before calling, especi"
          },
          {
            "label": "calendar_get_event",
            "desc": "Fetch full details of a single Google Calendar event. `calendar_id` defaults to 'primary'. `event_id` from calendar_list_events or calendar_search_events. Returns the full event object including description, recurrence, conference data, att"
          },
          {
            "label": "calendar_list_calendars",
            "desc": "List the calendars the calling user has access to (their own + shared). Returns id, summary, primary flag, accessRole, timeZone. Use the calendar id with the other calendar_* tools to operate on a non-primary calendar."
          },
          {
            "label": "calendar_list_events",
            "desc": "List Google Calendar events in a time range from the calling user's calendar. `calendar_id` defaults to 'primary' (the user's main calendar). `time_min` and `time_max` are ISO 8601 datetimes (e.g. '2026-06-07T00:00:00Z'). `max_results` is c"
          },
          {
            "label": "calendar_quickadd",
            "desc": "Create a Google Calendar event from a free-text description using Google's quick-add parser. `text` example: 'Lunch z Markiem jutro o 13:00' or 'Dinner Friday 7pm'. Google parses out the date/time/title. `calendar_id` defaults to 'primary'."
          },
          {
            "label": "calendar_search_events",
            "desc": "Search Google Calendar events by free-text query (matches in summary, description, location, attendee email). `query` is the search string. `time_min` and `time_max` are optional ISO 8601 datetimes to bound the search. `max_results` capped"
          },
          {
            "label": "calendar_update_event",
            "desc": "Update an existing Google Calendar event (partial — only fields you pass change). `calendar_id` defaults to 'primary'. `event_id` is the event's id. Optional updates: `summary`, `start`, `end`, `description`, `location`, `attendees_json` (r"
          }
        ]
      },
      {
        "key": "connect",
        "icon": "🔌",
        "title": "Łączenie kont (Connect)",
        "summary": "Jednorazowe podłączenie własnych kont (Gmail, Notion, Drive…) przez Pipedream OAuth.",
        "kind": "tools",
        "count": 2,
        "items": [
          {
            "label": "connect_integration",
            "desc": "Generate a personal link the current Slack user clicks to connect their own third-party account (Gmail, Notion, Google Drive, Google Calendar, …) through Pipedream's hosted OAuth flow. After they finish, Janusz can act on their behalf with"
          },
          {
            "label": "list_my_connections",
            "desc": "List the third-party accounts (Gmail, Notion, Google Drive, …) that the current Slack user has already connected through Pipedream. Use this to check whether the user has linked the app they need BEFORE calling a per-user tool — if they hav"
          }
        ]
      },
      {
        "key": "slack",
        "icon": "💬",
        "title": "Slack",
        "summary": "Odpowiedzi w wątkach, reakcje emoji, upload plików, lista kanałów, approvale.",
        "kind": "tools",
        "count": 7,
        "items": [
          {
            "label": "get_slack_reactions",
            "desc": "Read the emoji reactions on a Slack message. Returns each reaction with its emoji name, count, and the user IDs who reacted. Useful to check 👍/👎 feedback or whether someone acknowledged a message. `message_ts` is the ts of the message you w"
          },
          {
            "label": "list_my_approved_tools",
            "desc": "Show which destructive tools the current Slack user has PRE-APPROVED (via the 'Always approve this tool' button). Call this BEFORE `send_approval_request` — if the tool you're about to run is in the returned list, you can call it directly w"
          },
          {
            "label": "list_slack_channels",
            "desc": "List Slack channels the bot can see (id, name, is_member)."
          },
          {
            "label": "send_approval_request",
            "desc": "Send an INTERACTIVE approval request with three buttons: ✅ Approve / ✅ Always approve this tool / ❌ Reject. Use this INSTEAD of asking for confirmation in plain text BEFORE every destructive or sensitive action (gmail_send_email, gdrive_del"
          },
          {
            "label": "send_slack_message",
            "desc": "Send a Slack message. `blocks` is a JSON string of Block Kit blocks (recommended) OR plain text via a single section block. Set thread_ts to reply in a thread, replace_message_ts to edit a previous message. THIS IS YOUR ONLY VOICE — humans"
          },
          {
            "label": "slack_react",
            "desc": "Add an emoji reaction to a Slack message (e.g. emoji='eyes', 'white_check_mark'). Useful as quick acknowledgement."
          },
          {
            "label": "upload_to_slack",
            "desc": "Upload a workspace file to Slack and return its permalink. ALWAYS uploads into the current thread you are talking in — never to a channel root. Leave `channel_id` and `thread_ts` blank to inherit them from the current Slack context (the thr"
          }
        ]
      },
      {
        "key": "workspace",
        "icon": "🛠️",
        "title": "Workspace (kod i pliki)",
        "summary": "Pisanie i uruchamianie Python/bash, czytanie i zapis plików, wyszukiwanie (glob/grep).",
        "kind": "tools",
        "count": 6,
        "items": [
          {
            "label": "bash",
            "desc": "Execute a bash command inside the workspace. Returns stdout, stderr, exit_code. Use for running scripts, listing files, git, anything shell-y. REFUSES commands that would WRITE to protected paths: /app/repo/ (use repo_propose_edit instead),"
          },
          {
            "label": "file_edit",
            "desc": "Exact-string replace in a file. Set replace_all=true for every occurrence."
          },
          {
            "label": "file_read",
            "desc": "Read a file from the workspace. For large files pass offset (line number) and limit (lines)."
          },
          {
            "label": "file_write",
            "desc": "Write content to a file in the workspace. Creates parent directories. Overwrites if exists."
          },
          {
            "label": "glob_search",
            "desc": "Find files matching a glob pattern (supports ** for recursive). path is workspace-relative."
          },
          {
            "label": "grep",
            "desc": "Search workspace files for a regex. Returns matching lines."
          }
        ]
      },
      {
        "key": "browser",
        "icon": "🌐",
        "title": "Przeglądarka",
        "summary": "Sterowanie przeglądarką w chmurze (Browserbase) — portale, logowania, scraping.",
        "kind": "tools",
        "count": 8,
        "items": [
          {
            "label": "browser_click",
            "desc": "Click on a coordinate in the open browser. Use after browser_goto + browser_snapshot to know what to click. `x` and `y` are pixel coords from the screenshot's top-left. Returns a fresh snapshot after the click."
          },
          {
            "label": "browser_close",
            "desc": "Close a browser session and free Browserbase quota. Call this when you're done with a multi-step task (e.g. after extracting LinkedIn profile data). Returns the session's recording URL — useful for audit / debugging."
          },
          {
            "label": "browser_close_all",
            "desc": "Close EVERY open browser session in the current Slack thread, including persistent ones (recruit-/linkedin-/persist-/long- prefixes). Use when Bartosz says 'zamknij browser', 'koniec recrutingu', or otherwise signals the workflow is done. R"
          },
          {
            "label": "browser_get_text",
            "desc": "Return the visible TEXT content of the current page (no HTML). Cheaper than browser_snapshot when you just want to read a long article. Capped at 50k chars."
          },
          {
            "label": "browser_goto",
            "desc": "Open a URL in a cloud Chromium browser. Use when web_search isn't enough — JS-heavy single-page apps (KRS, KNF rejestr, LinkedIn), pages behind login, form filling, or scraping dynamic content. `session_name` lets you keep the same browser"
          },
          {
            "label": "browser_press_key",
            "desc": "Press a keyboard key in the browser (e.g. 'Enter', 'Tab', 'Escape', 'Control+A'). Use to submit forms, close dialogs, or shortcuts."
          },
          {
            "label": "browser_scroll",
            "desc": "Scroll the page. `direction` is 'up' or 'down', `amount` is rough wheel ticks."
          },
          {
            "label": "browser_type",
            "desc": "Type text into the currently-focused element in the browser. Use after browser_click on an input field. For Enter or shortcuts, use browser_press_key."
          }
        ]
      },
      {
        "key": "docgen",
        "icon": "📄",
        "title": "Generowanie dokumentów",
        "summary": "DOCX / PPTX / XLSX — z szablonów Asecurio lub od zera w stylu marki.",
        "kind": "tools",
        "count": 6,
        "items": [
          {
            "label": "docx_create_blank",
            "desc": "Generate a clean .docx in Asecurio brand style (Calibri body, blue #1E4D9B headers) from a list of sections. Each section is `{heading, body}`. Use when no template was found in Drive. Returns the saved workspace path."
          },
          {
            "label": "docx_from_template",
            "desc": "Open a .docx template from the workspace, apply {placeholder: value} replacements, save as a new .docx file in workspace/generated/. Use after `gdrive_download` (for native .docx in Drive) or `gdrive_export_as` (for Google Docs templates ex"
          },
          {
            "label": "pptx_create_blank",
            "desc": "Generate a clean .pptx deck from a list of slides. Each slide is `{title, bullets}` (bullets is a list of strings). Asecurio style: white background, blue title, black bullets. Use when no .pptx template was found. Returns the saved workspa"
          },
          {
            "label": "pptx_from_template",
            "desc": "Open a .pptx template, apply {placeholder: value} replacements across all slides/shapes/tables, save as a new .pptx in workspace/generated/. Use after `gdrive_download` for native .pptx or `gdrive_export_as(file_id, 'pptx')` for Google Slid"
          },
          {
            "label": "xlsx_create_blank",
            "desc": "Generate a clean .xlsx from a list of sheets. Each sheet is `{name, headers, rows}`. Asecurio style: blue header row, frozen pane, Calibri 11. Use when no .xlsx template found. Returns the saved workspace path."
          },
          {
            "label": "xlsx_from_template",
            "desc": "Open an .xlsx template, apply {placeholder: value} replacements across all sheets/cells, save as a new .xlsx in workspace/generated/. Preserves formulas. Use after `gdrive_download` for native .xlsx or `gdrive_export_as(file_id, 'xlsx')` fo"
          }
        ]
      },
      {
        "key": "convert",
        "icon": "🔁",
        "title": "Konwersja plików",
        "summary": "Zamiana plików (DOCX/XLSX/PPTX/RTF/PDF…) na czysty tekst / Markdown.",
        "kind": "tools",
        "count": 1,
        "items": [
          {
            "label": "file_to_markdown",
            "desc": "Convert any document to markdown text the LLM can read. Supports: PDF, DOCX, XLSX, PPTX, RTF, HTML, CSV, TSV, TXT, MD, JSON, YAML. Use this BEFORE trying to parse a file yourself with bash — it picks the right library and handles encoding f"
          }
        ]
      },
      {
        "key": "gpt",
        "icon": "🧠",
        "title": "Delegacja do GPT",
        "summary": "Treści i analizy przez GPT: tłumaczenia, podsumowania, drafty, ekstrakcja z prozy.",
        "kind": "tools",
        "count": 2,
        "items": [
          {
            "label": "openai_chat",
            "desc": "Delegate text analysis / generation to GPT. Bartosz' rule: Claude orchestrates, GPT writes and analyses. Call this whenever you need to: translate, summarise, classify, draft an email, write a Slack-style message, polish prose, brainstorm c"
          },
          {
            "label": "view_image",
            "desc": "Look at an image file in the workspace using vision and return a description / analysis. Use for screenshots, photos, diagrams, scanned docs the user uploaded (they land in workspace/uploads/). `file_path` is the workspace path. `prompt` sa"
          }
        ]
      },
      {
        "key": "admin",
        "icon": "🔐",
        "title": "Admin: self-edit kodu",
        "summary": "Tylko admin: czytanie i edycja kodu Janusza (propose → diff → Apply), audyt, rollback.",
        "kind": "tools",
        "count": 10,
        "items": [
          {
            "label": "repo_apply_all_pending",
            "desc": "[ADMIN ONLY — Bartosz] Apply ALL of Bartosz's currently-pending edits in one git commit and a single deploy. Use when Bartosz says 'apply all', 'zaaplikuj wszystkie', or after you've proposed >1 edits in this conversation and he confirms th"
          },
          {
            "label": "repo_list_files",
            "desc": "[ADMIN ONLY — Bartosz] List files in a directory of the repo working copy. `directory` is relative to repo root ('' or '.' for root). `pattern` is a glob filter ('*.py' for Python only, '*' for everything). Skips dot-files except .gitignore"
          },
          {
            "label": "repo_list_pending_edits",
            "desc": "[ADMIN ONLY — Bartosz] List pending edits awaiting Bartosz's approval. Each entry shows id, action, file_path, and short summary. Newest first. Use this to see what's queued up or to find an edit_id to re-surface for approval."
          },
          {
            "label": "repo_propose_create_file",
            "desc": "[ADMIN ONLY — Bartosz] Propose CREATING a new file. Stores a pending edit; the actual write only happens after Bartosz clicks ✅ Apply. `file_path` is repo-root-relative. Refuses if the file already exists (use repo_propose_edit instead). `s"
          },
          {
            "label": "repo_propose_delete_file",
            "desc": "[ADMIN ONLY — Bartosz] Propose DELETING an existing file. Stores a pending edit; the actual delete only happens after Bartosz clicks ✅ Apply. `file_path` is repo-root-relative. `summary` should explain why (shown to Bartosz)."
          },
          {
            "label": "repo_propose_edit",
            "desc": "[ADMIN ONLY — Bartosz] Propose an EDIT to an existing file. Generates a unified diff and stores a pending edit; the actual write only happens when Bartosz clicks ✅ Apply in the Slack approval message. `file_path` is repo-root-relative. `old"
          },
          {
            "label": "repo_rollback_last",
            "desc": "[ADMIN ONLY — Bartosz] Propose a ROLLBACK of the most recent Janusz commit on main. Walks `git log` for the latest 'Janusz: ...' commit, creates a pending revert that surfaces in Slack with the diff and an Apply button. On Apply: runs `git"
          },
          {
            "label": "repo_search_files",
            "desc": "[ADMIN ONLY — Bartosz] Search the repo for a regex pattern. Uses ripgrep when available, falls back to grep. `pattern` is the regex. `path` is the dir/file to search ('' = whole repo). `file_glob` filters by filename ('*.py' = Python only)."
          },
          {
            "label": "repo_show_audit_log",
            "desc": "[ADMIN ONLY — Bartosz] Show recent audit-log entries — every read/write/refusal/apply that went through the `repo_*` tool family. Returns newest first. Useful when Bartosz asks 'co Janusz dziś robił z kodem' or to verify a particular operat"
          },
          {
            "label": "repo_show_file",
            "desc": "[ADMIN ONLY — Bartosz] Read a file from the repo working copy. `file_path` is relative to repo root (e.g. 'workspace/apps/cv_pipeline/schema.py'). For long files pass `offset` (line) and `limit` (lines). Truncates at 32 KB to keep the promp"
          }
        ]
      },
      {
        "key": "skills",
        "icon": "📚",
        "title": "Skille (trwała pamięć)",
        "summary": "Wiedza i procesy w plikach SKILL.md — czytane automatycznie. Kliknij skill, aby zobaczyć treść.",
        "kind": "skills",
        "count": 14,
        "items": [
          {
            "label": "_archived-linkedin-recruiting",
            "desc": "ARCHIVED 2026-06-10. Do NOT auto-load. LinkedIn automated lookup was rolled back because Browserbase IP + Chromium fingerprint are detected by LinkedIn regardless of valid session cookies — every browser_goto to a LinkedIn profile hit /404/. Manual workflow now: Bartosz opens LinkedIn in his real browser, copy-pastes the profile content into Slack, Janusz extracts structured fields via openai_chat. To re-enable automated flow we'd need Browserbase Pro (stealth mode + residential proxy pool, ~$200/mc) — see tasks/etap11_browser_spec.md.\n",
            "body": "---\nname: _archived-linkedin-recruiting\ndescription: >\n  ARCHIVED 2026-06-10. Do NOT auto-load. LinkedIn automated lookup was\n  rolled back because Browserbase IP + Chromium fingerprint are detected\n  by LinkedIn regardless of valid session cookies — every browser_goto\n  to a LinkedIn profile hit /404/. Manual workflow now: Bartosz opens\n  LinkedIn in his real browser, copy-pastes the profile content into\n  Slack, Janusz extracts structured fields via openai_chat. To re-enable\n  automated flow we'd need Browserbase Pro (stealth mode + residential\n  proxy pool, ~$200/mc) — see tasks/etap11_browser_spec.md.\n---\n\n# ARCHIVED — LinkedIn automated cross-check\n\nThis skill is preserved as a reference for if/when we move to Browserbase\nPro tier. The recipe below documents the cookie persist approach we tried\non 2026-06-09 to 2026-06-10. Cookies were injected correctly (logs\nconfirmed `LinkedIn cookies injected for session 'recruit-test1' (3 cookies)`)\nbut LinkedIn still returned `/404/` because IP + fingerprint detection\nfired before cookies even mattered.\n\nFor now, use the manual fallback:\n\n```\nBartosz: <pastes LinkedIn profile content as text>\n       @Janusz wyciągnij z tego current_role, employer, experience_years, top 5 skills\nJanusz: [openai_chat ekstrakcja] {...}\n```\n\nOriginal recipe preserved below for future Pro-tier activation.\n\n---\n\n# LinkedIn cross-check for CV pipeline\n\n## When to fire\n\n- After a CV lands via career@asecurio.com → Notion page exists\n- Bartosz wants to verify the candidate before reaching out\n- Or proactively: cron once a week, \"for every Notion candidate without\n  linkedin_url, look them up\"\n\n## Why a browser, not web_search\n\nLinkedIn search results are gated behind a logged-in session. `web_search`\nreturns blue links but they redirect to a login wall. The cloud browser\n(Browserbase) sessions persist between tool calls — log in once per\nrecruitment cycle, scrape many candidates.\n\n## Recipe\n\n### Setup (one-time per recruiting cycle)\n\nLinkedIn blocks public scraping. We need to log in once and keep the\nsession warm. The Browserbase session is keyed by `session_name` and per-\nSlack-thread — open it once, reuse for the cycle.\n\n**CRITICAL**: `session_name` MUST start with `recruit-` (or `linkedin-`,\n`persist-`, `long-`). Sessions starting with other prefixes are\nauto-closed by `autoclose_thread_sessions` at the end of every turn,\nwhich would destroy the LinkedIn login cookies between Bartosz' Slack\nmessages. The `recruit-` prefix tells the autoclose to skip the session\nso it survives across multiple turns (up to a 30-min hard limit).\n\n```python\n# In a recruiting Slack thread (turn 1):\nresult = browser_goto(\n    \"https://www.linkedin.com/login\",\n    session_name=\"recruit-cycle-2026Q2\",\n)\n# Janusz reply to Bartosz must include the `live_view_url` from result:\n# \"Otworzyłem LinkedIn login. Otwórz live view: <live_view_url> i\n# zaloguj się jako recruiting@asecurio.com. Daj znać 'gotowe' jak\n# skończysz — sesja zostaje warm między wiadomościami.\"\n```\n\nBartosz then opens the live_view_url in his real browser, types LinkedIn\nemail + password (Janusz NEVER sees credentials — they go straight from\nBartosz' machine to LinkedIn via the Browserbase live view), clicks Sign\nIn, gets through any 2FA. He replies in Slack with \"gotowe\" / \"zalogowane\".\n\n### Per-candidate flow\n\n```python\n# 1. Search for the candidate\nbrowser_goto(\n    f\"https://www.linkedin.com/search/results/people/?keywords={url_quote(first_name + ' ' + last_name)}\",\n    session_name=\"recruit-cycle-2026Q2\",\n)\n# Snapshot returned — accessibility tree includes the \"first result\" link\n\n# 2. Click the first profile result\n# Coordinates from the snapshot — or use browser_press_key to navigate via Tab\nbrowser_click(x=..., y=..., session_name=\"recruit-cycle-2026Q2\")\n\n# 3. Wait for profile to load, then read text\nbrowser_get_text(session_name=\"recruit-cycle-2026Q2\")\n# Returns up to 50k chars of profile content — experience, education, etc.\n```\n\n### What to extract\n\nFrom the profile text, pull:\n\n| Field | How |\n|---|---|\n| `linkedin_url` | `handle.page.url` after click |\n| `current_title` | First line of \"Experience\" section |\n| `current_employer` | Company name on the same experience entry |\n| `experience_years` | Earliest job year → today |\n| `education_highest` | First entry in \"Education\" section |\n| `location` | Headline area, usually \"City, Country\" |\n| `mutual_connections` | If shown — relevance signal |\n\nDelegate the extraction itself to GPT via `openai_chat` (the\nDELEGATE_CONTENT_TO_GPT invariant — Claude orchestrates, GPT works on\ntext). Prompt:\n\n```\nYou are extracting LinkedIn profile facts. Output strict JSON with keys:\n  linkedin_url, current_title, current_employer, experience_years (int or null),\n  education_highest, location, mutual_connections (int or null).\nPage text follows the marker line.\n\n--- PAGE TEXT ---\n<output of browser_get_text>\n```\n\n### Update Notion candidate page\n\nAfter extraction, append to the existing Notion page (the one CV pipeline\ncreated):\n\n```python\nnotion_update_page(\n    page_id=<candidate_page_id>,\n    properties={\n        \"LinkedIn URL\": linkedin_url,\n        \"Current Role\": current_title,\n        \"Current Employer\": current_employer,\n        \"Verified Experience\": experience_years,\n    },\n)\nnotion_append_to_page(\n    page_id=<candidate_page_id>,\n    content=f\"## LinkedIn cross-check ({today_iso()})\\n\\n\"\n            f\"- Profile: [{first_name} {last_name}]({linkedin_url})\\n\"\n            f\"- Current: {current_title} @ {current_employer}\\n\"\n            f\"- Experience: ~{experience_years} years\\n\"\n            f\"- Education: {education_highest}\\n\"\n            f\"- Location: {location}\\n\"\n            f\"- Browser recording: {recording_url}\",\n)\n```\n\nThe `recording_url` from `browser_close` is the audit trail — if the\nLinkedIn lookup ever gets questioned, you can replay exactly what was\nclicked.\n\n## Close the session when done\n\n```python\nbrowser_close(session_name=\"recruit-cycle-2026Q2\")\n# Returns recording_url — paste it into Slack for Bartosz\n```\n\nIf you forget to close: the Browserbase session times out after 5 min of\ninactivity (their default), so it's not catastrophic — but $0.05/min adds\nup over a working day.\n\n## Anti-patterns\n\n- **Don't scrape > 30 profiles per hour from one session.** LinkedIn's\n  rate-limit detector kicks in. Spread across a day, or use 2-3 sessions\n  with different Bartosz logins.\n- **Don't log in with a personal account you care about.** LinkedIn can\n  shadow-ban or temp-restrict accounts that scrape. Use the dedicated\n  `recruiting@asecurio.com` account (once Bartosz creates it).\n- **Don't quote LinkedIn data verbatim into Slack.** Some profile fields\n  may be private to mutuals. Summarise + cite the LinkedIn URL.\n- **Don't run this against candidates who have opted out of being\n  contacted** — check the Notion `rodo.consent_status` field first.\n  RODO gate from etap 5.1 still applies.\n\n## Cost ballpark\n\nPer candidate (assuming session already open):\n\n| Step | Cost |\n|---|---|\n| `browser_goto(search)` + snapshot | ~$0.01 (15s session time) |\n| `browser_click` + load profile | ~$0.005 (5s) |\n| `browser_get_text` | ~$0.001 |\n| `openai_chat` extraction (~2k tokens in, 500 out) | ~$0.005 |\n| Notion writes | free |\n| **Total** | **~$0.02 per candidate** |\n\nPlus ~$0.05 one-time session warm-up (login + scroll past prompts).\n\n## Future work\n\n- Auto-resume sessions between Slack threads (currently each thread gets\n  its own session — wasteful when one HR thread spans days).\n- Crawl the candidate's posts for activity signal (Junior dev vs\n  active community member).\n- Cross-check against the Notion candidate database for duplicates\n  (same name + same university = probably same person).\n"
          },
          {
            "label": "asecurio_brand",
            "desc": "Asecurio brand identity — colors, fonts, layout rules. Apply whenever Janusz generates a deliverable for Asecurio: presentation, PDF, Word document, Excel workbook, email signature, social-media image, internal report. Trigger phrases include \"wygeneruj prezentację\", \"stwórz raport\", \"zrób dokument\", \"make a slide\", \"create CV\", \"draft a pitch deck\", anything that produces a Microsoft-Office / Google-Workspace / PDF artefact whose audience is Asecurio or its clients. Always pair with the format skill (pdf, docx, xlsx, pptx) — this one supplies the visual identity, the format skill supplies the API mechanics.\n",
            "body": "---\nname: asecurio_brand\ndescription: >\n  Asecurio brand identity — colors, fonts, layout rules. Apply whenever Janusz\n  generates a deliverable for Asecurio: presentation, PDF, Word document,\n  Excel workbook, email signature, social-media image, internal report.\n  Trigger phrases include \"wygeneruj prezentację\", \"stwórz raport\", \"zrób\n  dokument\", \"make a slide\", \"create CV\", \"draft a pitch deck\", anything that\n  produces a Microsoft-Office / Google-Workspace / PDF artefact whose audience\n  is Asecurio or its clients. Always pair with the format skill (pdf, docx,\n  xlsx, pptx) — this one supplies the visual identity, the format skill\n  supplies the API mechanics.\n---\n\n# Asecurio brand identity\n\nThis skill is **not optional**. Any document Janusz produces that bears the\nAsecurio name — or is sent to anyone outside the bot pipeline — must follow\nthis identity. Plain Helvetica/black on white is what we got on 2026-06-07,\nand it is the failure mode this skill exists to prevent.\n\n## Colors\n\n| Role | Hex | When to use |\n|---|---|---|\n| **Accent** | `#279AA8` | Bars, dividers, headings, call-out borders, link text, icons. The signature Asecurio teal. |\n| **Dark background** | `#0F172A` | Slide background on dark templates, hero PDF cover, dark-mode email blocks. Slate navy. |\n| **Light** | `#FFFFFF` | Background on light templates, text on dark slides, paper for PDFs. |\n| **Body text on light** | `#000000` | Body copy on white slides, body of PDFs, table cells on white. |\n| **Body text on dark** | `#FFFFFF` | Body copy on dark slides, hero text. |\n\n**Rule of thumb on contrast:** decide first whether the background is dark\nor light, then pick the body text color so contrast stays high. Never put\nblack on `#0F172A` or white on `#FFFFFF` — both fail accessibility and look\namateur.\n\nHex constants for Python code:\n\n```python\nASECURIO_ACCENT = \"#279AA8\"\nASECURIO_DARK_BG = \"#0F172A\"\nASECURIO_WHITE = \"#FFFFFF\"\nASECURIO_BLACK = \"#000000\"\n```\n\nRGB tuples for libraries that need them (reportlab, PIL, matplotlib):\n\n```python\nASECURIO_ACCENT_RGB = (39, 154, 168)   # #279AA8\nASECURIO_DARK_BG_RGB = (15, 23, 42)    # #0F172A\n```\n\n## Fonts\n\n| Role | Font | Weights |\n|---|---|---|\n| **Headings, titles, section labels** | **Montserrat** | Bold or ExtraBold |\n| **Body copy, captions, table cells** | **Lato** (preferred) or **Open Sans** (fallback) | Regular, Bold for emphasis |\n\n**Polish characters are mandatory.** The 2026-06-07 incident produced a CV\nwith `Imi■`, `wiedz■`, `Pracowa■` because the chosen library defaulted to\ncore Helvetica without Unicode glyphs. Every Python library used to render\ntext MUST register a Polish-capable font before drawing. See the per-library\nrecipes below.\n\n### reportlab (PDF generation)\n\n```python\nfrom reportlab.pdfbase import pdfmetrics\nfrom reportlab.pdfbase.ttfonts import TTFont\n\n# Bundled in workspace/shared/skills/asecurio_brand/fonts/ when missing system-wide.\n# Fallback to system DejaVuSans-Bold + DejaVuSans (both Unicode) if not present.\npdfmetrics.registerFont(TTFont(\"Montserrat-Bold\", \"/usr/share/fonts/truetype/montserrat/Montserrat-Bold.ttf\"))\npdfmetrics.registerFont(TTFont(\"Lato\", \"/usr/share/fonts/truetype/lato/Lato-Regular.ttf\"))\npdfmetrics.registerFont(TTFont(\"Lato-Bold\", \"/usr/share/fonts/truetype/lato/Lato-Bold.ttf\"))\n\nstyles.add(ParagraphStyle(\"AsecurioH1\", fontName=\"Montserrat-Bold\", fontSize=28, textColor=\"#279AA8\"))\nstyles.add(ParagraphStyle(\"AsecurioBody\", fontName=\"Lato\", fontSize=11, textColor=\"#000000\"))\n```\n\nIf the Asecurio TTFs are missing on the host, fall back to `DejaVuSans-Bold`\nand `DejaVuSans` — both ship with Debian/Ubuntu (`fonts-dejavu-core`) and\nsupport every Polish glyph. Use these in this priority order:\n\n1. Montserrat / Lato (when fonts are installed in the container)\n2. DejaVuSans / DejaVuSans-Bold (Linux fallback, Unicode-safe)\n3. `Helvetica` is **forbidden** — it lacks Unicode coverage.\n\n### python-pptx (slide generation)\n\n```python\nfrom pptx.util import Pt\nfrom pptx.dml.color import RGBColor\n\nACCENT = RGBColor(0x27, 0x9A, 0xA8)\nDARK_BG = RGBColor(0x0F, 0x17, 0x2A)\n\nfor shape in slide.shapes:\n    if shape.has_text_frame:\n        for paragraph in shape.text_frame.paragraphs:\n            for run in paragraph.runs:\n                run.font.name = \"Montserrat\" if is_heading else \"Lato\"\n                run.font.bold = is_heading\n                run.font.color.rgb = ACCENT if is_accent else RGBColor(0, 0, 0)\n```\n\n### python-docx (Word documents)\n\n```python\nfrom docx.shared import RGBColor\n\nfor paragraph in doc.paragraphs:\n    for run in paragraph.runs:\n        run.font.name = \"Montserrat\" if paragraph.style.name.startswith(\"Heading\") else \"Lato\"\n        if paragraph.style.name.startswith(\"Heading\"):\n            run.font.color.rgb = RGBColor(0x27, 0x9A, 0xA8)\n```\n\n### openpyxl (Excel)\n\n```python\nfrom openpyxl.styles import Font, PatternFill, Color\n\nheader_fill = PatternFill(\"solid\", fgColor=\"0F172A\")\nheader_font = Font(name=\"Montserrat\", bold=True, color=\"FFFFFF\", size=12)\nbody_font = Font(name=\"Lato\", size=11)\naccent_font = Font(name=\"Lato\", bold=True, color=\"279AA8\", size=11)\n```\n\n## Layout rules\n\nThese apply to slides and PDFs intended for external audiences (clients,\nteam integration decks, sales material). Internal-only artefacts (a quick\nstatus report) can be looser.\n\n1. **Hero/title slide** — `#0F172A` background, Montserrat ExtraBold white\n   title, `#279AA8` accent bar across the bottom (~4 px). Asecurio wordmark\n   bottom-left if available.\n2. **Content slide** — white background, Montserrat Bold heading in\n   `#279AA8`, Lato body in black. One H1 per slide.\n3. **Section divider** — `#0F172A` background, Montserrat ExtraBold white\n   number + label (style copied from the \"Strategia na wzrost\" deck).\n4. **Tables** — header row `#0F172A` fill with white Montserrat Bold; body\n   rows white with black Lato; accent row (totals / highlights) `#279AA8`\n   white text.\n5. **Charts** — primary series `#279AA8`, secondary series `#0F172A`,\n   never the matplotlib default blue/orange palette.\n\n## Don'ts (failure modes seen on real Asecurio work)\n\n- ❌ Default Helvetica or Times New Roman anywhere in a finished document.\n- ❌ Black-on-`#0F172A` body text (the 2026-06-07 CV regression).\n- ❌ Polish characters rendered as `■` or `?` (font-without-Unicode trap).\n- ❌ Multi-colour palettes beyond accent + dark + light + black + white.\n- ❌ Free-floating logos / clip-art unrelated to the deck topic.\n- ❌ Decorative serif fonts (Georgia, Garamond, Playfair) on Asecurio\n  artefacts — we are a tech/insurance brand, not a wedding planner.\n\n## Reference deck\n\nThe canonical reference is the team-integration deck **\"Strategia na wzrost\"**\n(Google Slides, owned by Bartosz, June 2026). Use it as visual ground truth\nwhen picking padding, font sizes, and accent placement. If a new artefact\nlooks visibly different from that deck — fix it before shipping.\n\n## How this skill plugs into the others\n\n| Format | Pair with |\n|---|---|\n| Generating a PDF (CV, report, one-pager) | `pdf` skill + this one |\n| Generating a Word doc (contract, profile) | `docx` skill + this one |\n| Generating a slide deck (sales, integration) | `pptx` skill + this one |\n| Generating a spreadsheet (bench tracker, lead list) | `xlsx` skill + this one |\n\nThe format skill supplies the library mechanics; this skill supplies the\ncolour and font choices. The chain is: receive request → load both skills →\ncall `openai_chat` for the prose content (per the `DELEGATE_CONTENT_TO_GPT`\ninvariant) → render with the format skill → apply the brand identity from\nthis skill → upload to the thread.\n"
          },
          {
            "label": "company",
            "desc": "Auto-loaded on every run. Asecurio company context — who we are, who's on the team, how we talk.",
            "body": "---\nname: company\ndescription: Auto-loaded on every run. Asecurio company context — who we are, who's on the team, how we talk.\n---\n\n# Asecurio\n\n## What we do\n- IT consultancy / staffing focused on the **insurance sector** (Poland + Europe).\n- Body-leasing + project-based engagements (subcontracting, fixed-price, T&M).\n- Fully remote, EU time zones.\n\n## Leadership\n- **Bartosz Wasilonek** — COO / CFO / co-founder. Operations, sales ops, finance, recruitment materials, company structure.\n- (Add other co-founders / leads here as Bartosz confirms them.)\n\n## Active clients & leads (to be confirmed by Bartosz)\n- Agro Ubezpieczenia\n- Federale\n- Uniqa\n- TUW TUW\n- D4X\n\n> ⚠️ Janusz: confirm details with Bartosz before quoting any client status in messages. Statuses in this file can go stale.\n\n## Brand voice\n- **Plain language** — ISO 24495-1:2023 is our doc standard. No corporate jargon, no fluff.\n- **Bilingual** — Polish internally, English with international clients. Match the language of the requester.\n- **Direct** — say the thing. If something is uncertain, label it \"Unverified\" instead of bluffing.\n- **Pragmatic** — propose concrete next steps, not abstract considerations.\n\n## Key abbreviations & shorthand\n- **B2B** — most consultants work on B2B contracts.\n- **bench** — consultants not currently assigned to a client engagement.\n- (Add more as Bartosz uses them in conversations — Janusz: capture acronyms you don't recognise and ask.)\n\n## How Janusz should work for Asecurio\n- Notion is the source of truth for internal docs (SOPs, playbooks, project pages, profiles).\n- Always check the `writing_docs` skill before drafting any Asecurio document.\n- For consultant profiles, look at the existing Gamma format (the `assecurio-profile` Cowork skill describes it).\n- Default to operating in Polish unless the requester writes in English.\n"
          },
          {
            "label": "contract_templates",
            "desc": "Create or format Polish legal contract templates (Umowa Zlecenie, Umowa o Dzieło, NDA, RODO clauses) for Asecurio in Google Docs. Use when asked to \"stwórz umowę\", \"sformatuj wzorzec\", \"napisz NDA\", \"edytuj kontrakt\", or anything that produces a Polish-law contract on Asecurio letterhead. Pair with `asecurio_brand` for colour/font. Pair with `docx_editing` when the contract needs to stay in `.docx`.\n",
            "body": "---\nname: contract_templates\ndescription: >\n  Create or format Polish legal contract templates (Umowa Zlecenie,\n  Umowa o Dzieło, NDA, RODO clauses) for Asecurio in Google Docs.\n  Use when asked to \"stwórz umowę\", \"sformatuj wzorzec\", \"napisz NDA\",\n  \"edytuj kontrakt\", or anything that produces a Polish-law contract on\n  Asecurio letterhead. Pair with `asecurio_brand` for colour/font.\n  Pair with `docx_editing` when the contract needs to stay in `.docx`.\n---\n\n# Contract Templates — Asecurio\n\n## The cardinal rule: GPT writes, Claude formats\n\n**Hard invariant from Bartosz.** Claude does NOT draft legal copy. Every\nclause goes through `openai_chat` (gpt-5.5) with the reference contract as\ncontext. Claude's job is the OOXML / Google Docs API manipulation that\napplies Asecurio formatting on top of GPT's prose.\n\n| Step | Owner | Action |\n|---|---|---|\n| 1. Content | **GPT** via `openai_chat` | Drafts the contract text using the reference template as tone/structure guide |\n| 2. Formatting | Claude | Applies the Asecurio paragraph styles via the Google Docs `batchUpdate` API |\n| 3. Review | Bartosz | Final approval before sending |\n\nThis is the same delegation as the `DELEGATE_CONTENT_TO_GPT` invariant in\n`src/config.py`, applied to legal copy.\n\n## Asecurio company data (insert verbatim into every contract)\n\n```\nAsecurio Sp. z o.o.\nKRS: 0001215617\nNIP: 5273199138\nREGON: 543671410\nAdres: ul. Aleja Jana Pawła II 27, 00-867 Warszawa\n```\n\nDo not edit or guess these. They are registered with KRS and must match\nthe corporate identity exactly. If a draft contains different numbers,\ncorrect them silently before saving — this is not a creativity-tolerant\nfield.\n\n## Existing reference templates on Google Drive\n\n| Contract | Google Doc ID | Where it lives |\n|---|---|---|\n| Umowa Zlecenie (reference) | `1yMsNXaujyoeZJfw-ouSl_CYn7F-lrHSUTlGLvu6tAQo` | Shared Drive `0AHX-KBH5FxlpUk9PVA` / Folder `1Yl4pRz4YZwQyJWUfEjQPYyxq-tbUFbkG` (People > 01_Templates) |\n| Umowa o Dzieło | `1UXOULVFTPefD-Z_TLqQ6zDgG5CnclGB1kO3Z9J_s7ss` | (same folder) |\n\nWhen creating a new contract, fetch one of these as the GPT reference.\nNever paste tone/structure from memory — the reference may have been\nedited since the skill was written.\n\n## Standard sections (every contract)\n\n1. Strony — the parties\n2. Przedmiot umowy — scope\n3. Czas / Termin realizacji — duration\n4. Wynagrodzenie — compensation\n5. Obowiązki stron — duties\n6. **Poufność** — confidentiality (NO penalty clauses — see Anti-patterns)\n7. Prawa Autorskie — IP transfer / licensing\n8. RODO — GDPR clause\n9. Rozwiązanie umowy — termination\n10. Odpowiedzialność — liability\n11. Postanowienia końcowe — final provisions\n12. Załączniki — attachments (if applicable)\n\n### Umowa o Dzieło specifics\n\n- **§4 Wynagrodzenie:** must include a clause acknowledging 50% KUP\n  (koszty uzyskania przychodu) for the creative nature of the work, plus\n  the explicit \"brak obowiązku ZUS\" line. This is the tax-relevant\n  distinguishing feature versus Umowa Zlecenie.\n- **§12 (or wherever the obligations live):** the RUD obligation\n  (Raport o Umowie o Dzieło, weekly to ZUS since 2021) sits with the\n  Zamawiający, not the Wykonawca. Always make this explicit.\n\n## Asecurio formatting standard (Google Docs API)\n\nApply these styles via `documents.batchUpdate`. Set ALL properties\nexplicitly per paragraph — `updateNamedStyle` is unreliable through the\nproxy (returns 400, see Quirks below).\n\n| Element | Alignment | Space before | Space after | Line height | Font | Size | Bold | Colour |\n|---|---|---|---|---|---|---|---|---|\n| **Title** (UMOWA O DZIEŁO) | CENTER | 0pt | 4pt | 100% | Montserrat | 18pt | Yes | `#279AA8` (Asecurio accent) |\n| **Subtitle** (na podstawie…) | CENTER | 0pt | 16pt | 100% | Lato | 11pt | No | `#000000`, italic |\n| **§ Headings** (§1. Strony) | START | 14pt | 7pt | 100% | Montserrat | 12pt | Yes | `#279AA8` |\n| **Body** | JUSTIFIED | 0pt | 8pt | 125% | Lato | 11pt | No | `#000000` |\n| **Parties** (1) / (2) | JUSTIFIED | 0pt | 10pt | 125% | Lato | 11pt | No | `#000000` |\n| **Sub-items** a) / b) / c) | JUSTIFIED | 0pt | 4pt | 125% | Lato | 11pt | No | `#000000` |\n| **Signature line** (______) | START | 24pt | 2pt | 100% | Lato | 11pt | No | `#000000` |\n| **Signature label** | START | 2pt | 2pt | 100% | Lato | 11pt | No | `#000000` |\n\n(Updated 2026-06-07 to current Asecurio brand `#279AA8`. Older versions of\nthis skill specified the legacy blue `#2E74B5`; replace it everywhere when\nre-formatting an older draft.)\n\n## Sub-items rule (critical)\n\n**Sub-items (a), (b), (c) ALWAYS on separate indented lines. Never inline.**\n\n```\n❌ WRONG\n§5. Wykonawca zobowiązuje się do: a) terminowego wykonania dzieła, b) zachowania poufności\n\n✅ RIGHT\n§5. Wykonawca zobowiązuje się do:\n    a) terminowego wykonania dzieła,\n    b) zachowania poufności,\n    c) informowania o postępach prac.\n```\n\nIn the Google Docs API: use `createParagraphBullets` for the lead-in\nparagraph and again for the sub-items, then `updateParagraphStyle` with\n`indentFirstLine: 18pt, indentStart: 36pt` for the sub-items.\n\n## Google Docs API quirks (learned the hard way)\n\n| Problem | Fix |\n|---|---|\n| `updateNamedStyle` returns 400 | Pipedream's Google Docs proxy does NOT support this method. Apply every text property explicitly per paragraph. |\n| `foregroundColor: {red:0, green:0, blue:0}` is treated as \"inherit\" | Pure black ends up as theme default. Either skip the property (default is already black) or use `{red: 0.001, …}` |\n| New Google Docs ship Arial-based named styles | These leak into HEADING_2 (not bold, 16pt instead of 13pt, wrong colour). Override every property in the API call — do not assume the document state. |\n| `.docx` on Google Drive | The Docs API refuses Office files. Download, edit with python-docx (`docx_editing` skill), upload back. |\n| Move operations on Shared Drive | Requires the actual parent folder ID, not \"root\". Use `?fields=id,name,parents` before moving. |\n\n## End-to-end process\n\n```\n1. Bartosz says \"stwórz umowę Zlecenie dla Marka, 5000 PLN/mies, start 1.07\"\n2. Janusz fetches Umowa Zlecenie reference (gdrive_read_file)\n3. Janusz calls openai_chat with:\n     prompt = \"Wygeneruj treść Umowy Zlecenie dla [Marek] na podstawie wzorca poniżej.\n               Stawka: 5000 PLN/miesiąc. Start: 2026-07-01. Asecurio Sp. z o.o. KRS 0001215617.\n               <reference template here>\n               \n               UWAGA: w §7 Poufność BEZ kar umownych — tylko obowiązek poufności + zakaz + termin.\n               Sub-pozycje (a)(b)(c) zawsze na osobnych liniach z wcięciem.\"\n4. Janusz reviews GPT output:\n     - §4 Wynagrodzenie has the agreed rate\n     - §7 Poufność has NO penalty clauses (kar umownych)\n     - Sub-items split correctly\n5. Janusz creates a new Google Doc in the People > Contracts folder\n6. Janusz inserts the text (insertText)\n7. Janusz applies formatting (batchUpdate with ~150+ requests for all paragraph styles)\n8. Janusz posts a link in Slack: \"Umowa gotowa do podpisu: <link>\"\n```\n\n## Anti-patterns\n\n- **Penalty clauses in §7 Poufność.** Hard rule from Bartosz. The\n  confidentiality section has obligation + scope + duration — never a\n  monetary penalty (kara umowna). If GPT inserts one, strip it before\n  saving.\n- **Using `#2E74B5` (legacy blue) for new contracts.** Current brand is\n  `#279AA8`. The old hex appears in pre-2026-06 drafts; replace it.\n- **Claude writing clauses.** Never. Always `openai_chat`.\n- **Skipping Bartosz's review.** Even after GPT + Claude, every contract\n  goes to Bartosz for approval before reaching the counterparty. Post a\n  Slack message with the link and wait.\n- **Inline sub-items** — see the \"Sub-items rule\" section above.\n\n## What this skill is NOT for\n\n- Negotiating contract terms (Janusz drafts; Bartosz decides)\n- Generic NDA / SaaS / EU-law contracts not specific to Asecurio (use\n  `pdf_creation` for branded layouts; `openai_chat` for the prose)\n- Signing the contract — `pdf_signing` adds a visual signature; qualified\n  e-signature is out of scope\n- Generating contracts in languages other than Polish — this skill is\n  Polish-law-specific; other jurisdictions need their own template\n"
          },
          {
            "label": "docx_editing",
            "desc": "Edit `.docx` Word documents — fill placeholders in a contract template, swap dates in a recurring report, update a CV before sending. Use when a `.docx` already exists on disk or on Google Drive. For creating a Word doc from scratch with bespoke design, prefer `pdf_creation` (HTML/CSS → PDF) unless the deliverable specifically needs to stay editable in Word. Pair with `asecurio_brand` for colour/font when adding new content.\n",
            "body": "---\nname: docx_editing\ndescription: >\n  Edit `.docx` Word documents — fill placeholders in a contract template,\n  swap dates in a recurring report, update a CV before sending. Use when\n  a `.docx` already exists on disk or on Google Drive. For creating a\n  Word doc from scratch with bespoke design, prefer `pdf_creation`\n  (HTML/CSS → PDF) unless the deliverable specifically needs to stay\n  editable in Word. Pair with `asecurio_brand` for colour/font when\n  adding new content.\n---\n\n# DOCX Editing — Asecurio\n\n## Mechanism\n\n`python-docx` reads and modifies `.docx` files (zip archives of XML) without\nopening Word. No headless office, no rendering — pure XML manipulation.\n\n## Workflow\n\n### Step 1 — Read the structure first\n\nText in `.docx` is divided into *runs* (fragments with identical formatting).\nA single sentence like \"Bartosz Wasilonek\" might be stored as three runs:\n`[\"Bartosz\", \" \", \"Wasilonek\"]`. This is invisible in the Word editor and\nbreaks naive `.replace()` calls.\n\n```python\nfrom docx import Document\n\ndoc = Document(\"document.docx\")\nfor i, para in enumerate(doc.paragraphs):\n    print(f\"\\nParagraph {i}: style={para.style.name!r}\")\n    print(f\"  Full text: {para.text!r}\")\n    print(f\"  Runs ({len(para.runs)}):\")\n    for j, run in enumerate(para.runs):\n        print(f\"    Run {j}: {run.text!r} | bold={run.bold} | size={run.font.size}\")\n```\n\n### Step 2 — Find text that's split across runs\n\n```python\ndef find_text_in_runs(paragraphs, search_text):\n    for para in paragraphs:\n        full = \"\".join(r.text for r in para.runs)\n        if search_text in full:\n            print(f\"Found in paragraph: {full!r}\")\n            print(f\"  Runs: {[r.text for r in para.runs]}\")\n```\n\n### Step 3 — Replace while preserving formatting\n\nThe safe pattern: collapse the full text, replace in the joined string,\nwrite the new value into the first run, blank the others.\n\n```python\ndef replace_in_paragraph(para, old_text, new_text):\n    \"\"\"Replace text in a paragraph, preserving formatting from the first run.\"\"\"\n    full_text = \"\".join(r.text for r in para.runs)\n    if old_text not in full_text:\n        return False\n    new_full = full_text.replace(old_text, new_text)\n    if para.runs:\n        para.runs[0].text = new_full\n        for run in para.runs[1:]:\n            run.text = \"\"\n    return True\n\ndoc = Document(\"contract_template.docx\")\nfor para in doc.paragraphs:\n    replace_in_paragraph(para, \"[IMIĘ NAZWISKO]\", \"Jan Kowalski\")\n    replace_in_paragraph(para, \"[DATA]\", \"07.06.2026\")\n\n# Tables are NOT included in doc.paragraphs — iterate separately\nfor table in doc.tables:\n    for row in table.rows:\n        for cell in row.cells:\n            for para in cell.paragraphs:\n                replace_in_paragraph(para, \"[KWOTA]\", \"5 000 PLN\")\n\ndoc.save(\"contract_filled.docx\")\n```\n\n### Step 4 — Verify\n\nAfter saving, reopen and confirm no placeholders survived.\n\n```python\ndoc2 = Document(\"contract_filled.docx\")\nfor para in doc2.paragraphs:\n    if \"[\" in para.text and \"]\" in para.text:\n        print(f\"⚠️ Placeholder left unfilled: {para.text!r}\")\n```\n\n## Rules\n\n### 1 — Never assign `paragraph.text = \"…\"`\n\nThat collapses every run into one, destroying every formatting variation\nin the paragraph (bold spans, font changes, colour highlights).\n\n```python\n# ❌ WRONG — destroys all runs\npara.text = \"new text\"\n\n# ✅ RIGHT — operate per run\nfor run in para.runs:\n    run.text = run.text.replace(\"old\", \"new\")\n```\n\n### 2 — Watch for text split across runs\n\nWord re-splits runs after every edit in the GUI. A user who typed\n\"Bartosz Wasilonek\" once may have edited it twice and ended up with\nthe name split across 5 runs. The `replace_in_paragraph` pattern above\nhandles this; naive `run.text.replace(…)` does not.\n\n### 3 — Iterate tables separately\n\nParagraphs inside `doc.tables[i].rows[j].cells[k].paragraphs` are NOT in\n`doc.paragraphs`. Forgetting tables is the #1 source of \"but I replaced\nthe placeholder, why is it still there?\" bugs.\n\n### 4 — `.docx` on Google Drive\n\nThe Google Docs API refuses to edit Office files (`.docx`), returning\n\"The document must not be an Office file\". The standard pipeline:\n\n1. `gdrive_download(file_id, \"contract.docx\")` — pull the file via Pipedream\n2. Edit locally with python-docx as above\n3. `gdrive_update_file(file_id, \"contract.docx\")` — PATCH the binary back\n   in place, preserving share permissions\n\n## Creating a new DOCX\n\nWhen the request specifically needs Word editability (a contract that the\nclient will edit further), generate a fresh document. Apply the Asecurio\nbrand on every run.\n\n```python\nfrom docx import Document\nfrom docx.shared import Pt, RGBColor\nfrom docx.enum.text import WD_ALIGN_PARAGRAPH\n\nACCENT = RGBColor(0x27, 0x9A, 0xA8)\n\ndoc = Document()\n\n# Title\ntitle = doc.add_heading(\"Tytuł dokumentu\", level=0)\ntitle.alignment = WD_ALIGN_PARAGRAPH.CENTER\nfor run in title.runs:\n    run.font.name = \"Montserrat\"\n    run.font.color.rgb = ACCENT\n\n# Body paragraph\npara = doc.add_paragraph(\"Treść dokumentu — body copy goes here.\")\nfor run in para.runs:\n    run.font.name = \"Lato\"\n    run.font.size = Pt(11)\n\n# Table with branded header\ntable = doc.add_table(rows=3, cols=2)\ntable.style = \"Light Grid Accent 1\"  # close enough to brand; override colours below\nheader = table.rows[0]\nfor cell in header.cells:\n    for para in cell.paragraphs:\n        for run in para.runs:\n            run.font.bold = True\n            run.font.name = \"Montserrat\"\n            run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)\n    cell._element.get_or_add_tcPr().append(_dark_fill_xml())  # #0F172A\n\ndoc.save(\"output.docx\")\n```\n\nFor complex layouts (multi-column, hero pages, charts) — generate as PDF\nvia `pdf_creation` instead. python-docx's layout primitives are limited.\n\n## Anti-patterns\n\n- `para.text = \"…\"` — destroys formatting (see Rule 1).\n- Editing only `doc.paragraphs` and forgetting tables (Rule 3).\n- Trying to edit a Google Drive `.docx` through the Docs API (Rule 4).\n- Mixing fonts outside Montserrat / Lato.\n- Inline colours from outside the brand palette (`#279AA8`, `#0F172A`, white, black).\n\n## What this skill is NOT for\n\n- Tracked changes / comments / revision history (different skill — would need\n  XML manipulation beyond python-docx's high-level API)\n- Reading `.doc` (legacy binary format) — convert to `.docx` first via\n  `libreoffice --headless --convert-to docx`\n- Generating a PDF from a DOCX — produce the PDF directly with `pdf_creation`\n  to keep the brand consistent\n"
          },
          {
            "label": "excel_editing",
            "desc": "Edit and modify Excel spreadsheets. Use when working with .xlsx files.",
            "body": "---\nname: excel_editing\ndescription: Edit and modify Excel spreadsheets. Use when working with .xlsx files.\n---\n\n# Excel Editing Skill\n\nWhen editing Excel files, always use openpyxl with `copy()` to preserve formatting, and prefer Excel formulas over hardcoded calculated values for dynamic updates.\n\n## Critical Rule: Use Formulas, Not Hardcoded Values\n\nAlways use Excel formulas instead of calculating in Python. This keeps spreadsheets dynamic.\n\n```python\n# ❌ WRONG - Hardcoding calculated values\ntotal = df['Sales'].sum()\nsheet['B10'] = total  # Hardcodes 5000\n\n# ✅ CORRECT - Using Excel formulas\nsheet['B10'] = '=SUM(B2:B9)'\n```\n\n## Zero Formula Errors\n\nEvery Excel file must have zero formula errors: `#REF!`, `#DIV/0!`, `#VALUE!`, `#N/A`, `#NAME?`\n\nAfter creating/modifying Excel files with formulas, validate them:\n```bash\n# Static validation (checks formula structure)\nuv run python skills/excel_editing/scripts/validate_excel.py output.xlsx\n\n# Full validation with formula recalculation (uses LibreOffice)\nuv run python skills/excel_editing/scripts/validate_excel.py output.xlsx --recalc\n\n# Strict mode (also checks for hardcoded values in formulas)\nuv run python skills/excel_editing/scripts/validate_excel.py output.xlsx --recalc --strict\n```\n\n**Note:** The validation script NEVER modifies the original file. When using `--recalc`, formulas are recalculated in a temporary copy, preserving all original formatting and structure.\n\n## Choosing the Right Library\n\n| Use Case                            | Library  | Why                                  |\n| ----------------------------------- | -------- | ------------------------------------ |\n| Data analysis, aggregations, pivots | pandas   | Vectorized operations, DataFrame API |\n| Formulas, formatting, styles        | openpyxl | Preserves Excel structure            |\n| Simple data export                  | pandas   | Quick `to_excel()`                   |\n| Modifying existing files            | openpyxl | Keeps formulas and formatting intact |\n\n## Reading Excel Files\n\n### With pandas (for data analysis)\n```python\nimport pandas as pd\n\ndf = pd.read_excel('file.xlsx')  # First sheet\nall_sheets = pd.read_excel('file.xlsx', sheet_name=None)  # All sheets as dict\ndf.to_excel('output.xlsx', index=False)\n\n# Performance tips for large files\ndf = pd.read_excel('file.xlsx', usecols=['A', 'B', 'C'])  # Only needed columns\ndf = pd.read_excel('file.xlsx', dtype={'id': str})  # Explicit types prevent inference issues\ndf = pd.read_excel('file.xlsx', parse_dates=['date_col'])  # Parse dates correctly\n```\n\n### With openpyxl (to preserve formulas)\n```python\nfrom openpyxl import load_workbook\n\nwb = load_workbook('existing.xlsx')\nsheet = wb.active\n\n# Read calculated values only (WARNING: saving will permanently lose formulas!)\nwb_data = load_workbook('file.xlsx', data_only=True)\n\n# Performance tips for large files\nwb = load_workbook('large.xlsx', read_only=True)  # Read-only mode, much faster\nwb = Workbook(write_only=True)  # Write-only mode for generating large files\n```\n\n### openpyxl Gotchas\n- **1-based indexing**: `sheet.cell(row=1, column=1)` is cell A1, not A0\n- **data_only trap**: Loading with `data_only=True` then saving will replace all formulas with their last cached values - the formulas are gone forever\n- **Formulas not evaluated**: openpyxl stores formulas as text, not calculated values. Use the validation script with `--recalc` to evaluate them\n\n## Creating Excel Files\n\n```python\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill, Alignment\n\nwb = Workbook()\nsheet = wb.active\n\n# Add data and formulas\nsheet['A1'] = 'Revenue'\nsheet['B1'] = 1000\nsheet['B5'] = '=SUM(B1:B4)'\n\n# Formatting\nsheet['A1'].font = Font(bold=True, color='0000FF')  # Blue = input\nsheet['B5'].font = Font(bold=True, color='000000')  # Black = formula\n\nsheet.column_dimensions['A'].width = 20\nwb.save('output.xlsx')\n```\n\n## Editing Existing Files\n\n```python\nfrom openpyxl import load_workbook\n\nwb = load_workbook('existing.xlsx')\nsheet = wb.active\n\n# Modify while preserving structure\nsheet['A1'] = 'New Value'\nsheet.insert_rows(2)\nsheet.delete_cols(3)\n\n# Add sheets\nnew_sheet = wb.create_sheet('Summary')\nnew_sheet['A1'] = 'Total'\n\nwb.save('modified.xlsx')\n```\n\n## Financial Model Standards\n\n### Color Coding (industry-standard)\n| Color             | Meaning                                  |\n| ----------------- | ---------------------------------------- |\n| Blue text         | Hardcoded inputs, user-changeable values |\n| Black text        | All formulas and calculations            |\n| Green text        | Links from other sheets in same workbook |\n| Red text          | External links to other files            |\n| Yellow background | Key assumptions needing attention        |\n\n### Number Formatting\n```python\n# Currency with units in header\nsheet['A1'] = 'Revenue ($mm)'\nsheet['B1'].number_format = '$#,##0'\n\n# Percentages with one decimal\nsheet['C1'].number_format = '0.0%'\n\n# Zeros as dashes\nsheet['D1'].number_format = '$#,##0;($#,##0);\"-\"'\n\n# Multiples\nsheet['E1'].number_format = '0.0x'\n```\n\n### Formula Best Practices\n```python\n# ✅ Reference assumptions in separate cells\nsheet['B2'] = 0.05  # Growth rate assumption\nsheet['C5'] = '=B5*(1+$B$2)'\n\n# ❌ Avoid hardcoded values in formulas\nsheet['C5'] = '=B5*1.05'  # Bad: magic number\n```\n\n## Common Formula Errors and Fixes\n\n| Error     | Cause                  | Fix                                      |\n| --------- | ---------------------- | ---------------------------------------- |\n| `#REF!`   | Invalid cell reference | Check deleted rows/columns               |\n| `#DIV/0!` | Division by zero       | Add `=IF(B1=0,0,A1/B1)`                  |\n| `#VALUE!` | Wrong data type        | Verify cell contains numbers             |\n| `#NAME?`  | Unrecognized function  | Check spelling, use `TEXT()` not `TXT()` |\n| `#N/A`    | VLOOKUP not found      | Add `IFERROR()` wrapper                  |\n\n## Verification Checklist\n\n- [ ] All formulas reference correct cells\n- [ ] Column mapping is correct (column 64 = BL, not BK)\n- [ ] Row offset accounts for 1-indexing (DataFrame row 5 = Excel row 6)\n- [ ] Division by zero is handled\n- [ ] Cross-sheet references use correct format (`Sheet1!A1`)\n- [ ] Run validation script to check for errors\n<!-- ══════════════════════════════════════════════════════════════════════════\n     END OF AUTOGENERATED CONTENT - DO NOT EDIT ABOVE THIS LINE\n     Your customizations below will persist across SDK regenerations.\n     ══════════════════════════════════════════════════════════════════════════ -->\n\n## Asecurio Brand Style (xlsx from scratch)\n\nWhen creating XLSX from scratch (no template found in Drive):\n\n- **Header row**: bold, font color `#1E4D9B` (Asecurio blue), background `#F2F5FC` (light blue)\n- **Font**: Calibri 11 for body, Calibri 12 bold for headers\n- **Borders**: thin grey `#CCCCCC` between cells, none on header\n- **Number formats**: `#,##0.00 zł` for PLN, `$#,##0.00` for USD, `0.0%` for percentages, `yyyy-mm-dd` for dates\n- **Column widths**: auto-fit based on content (`worksheet.column_dimensions[X].width = max_len + 2`)\n- **Freeze**: row 1 frozen (`worksheet.freeze_panes = 'A2'`)\n\nWhen editing an existing Asecurio template — **preserve everything**. Only fill the cells the user asked for. Do not restyle.\n\n## Asecurio File-Generation Workflow (etap 12.1)\n\nWhen invoked from `doc_gen.xlsx_from_template` or `doc_gen.xlsx_create_blank`:\n\n1. Output filename pattern: `<DocType>_<EntityName>_<YYYY-MM-DD>.xlsx`\n2. Never auto-decide where to upload — `drive_templates.drive_pick_destination` returns\n   candidates; Bartosz clicks the destination button in Slack.\n3. Delegate content (table rows, projections, formulas a priori) to GPT-5.5 — Claude\n   only orchestrates: find template → fill → upload. (DELEGATE_CONTENT_TO_GPT invariant.)\n\n"
          },
          {
            "label": "notion_asecurio",
            "desc": "How Notion is structured at Asecurio. Read before searching, creating, or editing any Asecurio Notion page.",
            "body": "---\nname: notion_asecurio\ndescription: How Notion is structured at Asecurio. Read before searching, creating, or editing any Asecurio Notion page.\n---\n\n# Notion @ Asecurio\n\n## Available tools (use them in this order)\n1. `notion_search(query)` — find pages/databases by title. **Start here.**\n2. `notion_fetch_page(page_id_or_url)` — read a page as Markdown.\n3. `notion_query_database(database_id, filter_json, sorts_json)` — structured queries against databases.\n4. `notion_create_page(parent_id, title, markdown_content)` — create a new page under a page or database.\n5. `notion_append_to_page(page_id, markdown_content)` — add content to an existing page.\n\n## Best practices\n\n### Before doing anything\n- **Search first.** Don't assume a page exists; search for it. Don't assume the title; try several phrasings.\n- **Confirm identity.** Before editing a page, fetch it and check the title/URL match what you expect.\n- **Never write to a page Bartosz didn't reference**, unless he explicitly told you \"create a new one\".\n\n### Writing pages\n- Always use the `writing_docs` skill's plain-language rules.\n- Polish content unless the page is for an international audience.\n- Structure: `## Section` headings, short paragraphs, bullet lists for enumerations.\n\n### Permissions\n- Janusz can only see pages explicitly *shared* with the Notion integration in Asecurio's workspace.\n- If `notion_search` returns nothing for an obvious query, the page probably isn't shared yet — tell Bartosz to add the integration to that page (open page → ··· → Connections → \"Janusz\").\n\n### Bartosz-specific shorthand for Notion\n- \"the SOP doc\" — usually means the master SOP index page (confirm by search).\n- \"the leads tracker\" — likely a database; query it with status filters.\n- (Janusz: append new shorthand here as you learn it.)\n\n## Common database fields you'll see\n- **Status** — usually a `status` or `select` field.\n- **Owner** — usually `people`.\n- **Client** — relation or text.\n- **Last contact** — date.\n\n## Things to NEVER do\n- Don't delete pages. Period.\n- Don't change page titles without confirming with Bartosz.\n- Don't add the integration to new pages on Bartosz's behalf (you can't anyway — that's a manual Notion UI action).\n"
          },
          {
            "label": "onboarding",
            "desc": "What Janusz should do on first runs — discover the environment, ask the right questions, fill knowledge gaps.",
            "body": "---\nname: onboarding\ndescription: What Janusz should do on first runs — discover the environment, ask the right questions, fill knowledge gaps.\n---\n\n# Onboarding — first conversations with Bartosz\n\nWhen the project is fresh, several skills will have placeholder data. Janusz: don't pretend to know what you don't know. Confirm.\n\n## First DM with Bartosz — checklist\n\nWhen Bartosz first DMs you (or on first @mention), if you notice that:\n- `company/SKILL.md` still has placeholder client names — ask him to confirm/update.\n- `users/<his_id>/SKILL.md` still uses `BARTOSZ_PLACEHOLDER` — ask him to confirm Slack ID and any preferences.\n- `notion_asecurio/SKILL.md` has empty shorthand — ask him what shortcuts he uses (\"the SOP doc\", etc.).\n\n**Don't dump every question at once.** Ask one, do the work, learn, repeat.\n\n## First Notion task — discovery checklist\n\nBefore doing real Notion work:\n1. Call `notion_search(\"\")` (empty query → list of everything shared with the integration).\n2. Identify which workspaces / top-level pages are available.\n3. Cache the top-level structure in this skill (append a \"## Known top-level pages\" section).\n4. **Then** do the actual task.\n\nIf `notion_search` returns nothing, tell Bartosz: \"Nie widzę żadnych stron Notion — czy podpiąłeś integrację 'Janusz' do workspace Asecurio? (otwórz dowolną stronę → ··· → Connections → Janusz)\".\n\n## Learning loop\nAfter any correction from Bartosz:\n1. Identify what general rule the correction implies.\n2. Update the right skill file (`company`, `user_bartosz`, `notion_asecurio`, `writing_docs`, etc.).\n3. Confirm the update in Slack briefly: \"Zapisane: <one-line summary>\".\n\n## Don't onboard forever\nAfter ~5–10 productive interactions, the placeholders should be replaced with real data. If they're still empty after a week, that's a signal to ask Bartosz explicitly: \"Mam nadal placeholdery w skill <name>. Chcesz je teraz uzupełnić?\"\n"
          },
          {
            "label": "pdf_creation",
            "desc": "Generate Asecurio-branded PDF decks (slides) and reports from scratch. Use whenever the user asks for \"wygeneruj prezentację w PDF\", \"stwórz raport\", \"make a one-pager\", \"create a sales deck\", \"draft a CV\", or any artefact whose final form is a PDF. Pair with `asecurio_brand` (mandatory) for colours/fonts/layout rules.\n",
            "body": "---\nname: pdf_creation\ndescription: >\n  Generate Asecurio-branded PDF decks (slides) and reports from scratch.\n  Use whenever the user asks for \"wygeneruj prezentację w PDF\", \"stwórz\n  raport\", \"make a one-pager\", \"create a sales deck\", \"draft a CV\", or\n  any artefact whose final form is a PDF. Pair with `asecurio_brand`\n  (mandatory) for colours/fonts/layout rules.\n---\n\n# PDF Creation — Asecurio\n\n## How it works\n\nTwo steps, no exceptions:\n\n1. **Write HTML + CSS** — every slide or report page is plain HTML with a\n   CSS stylesheet. This gives full control over layout, typography, and\n   colour without fighting a PDF library's bespoke API.\n2. **Render via WeasyPrint** — `from weasyprint import HTML; HTML(string=…).write_pdf(…)`.\n   WeasyPrint is a print engine: it understands `@page`, `break-inside: avoid`,\n   `@import` for Google Fonts, and clickable `<a href>` links.\n\nWeasyPrint, not reportlab, not Chromium. Reportlab is what produced the\n2026-06-07 CV with `Imi■` / `wiedz■` — its default Helvetica has no Polish\nglyphs and there is no nice escape hatch.\n\n## Output formats\n\n| Format | Dimensions | Use for |\n|---|---|---|\n| **Deck (slides)** | 1920 × 1080 px | Pitch decks, sales material, team-integration decks |\n| **Report** | A4 (Polish business default) or US Letter on request | CVs, one-pagers, written reports, contracts in PDF |\n\n## Workflow\n\n1. **Brief** → topic, audience, length, single main message\n2. **Title draft** → the slide titles alone must tell the story\n3. **Restructure content** → prose becomes tables, numbers, diagrams, pull-quotes\n4. **Generate the prose with `openai_chat` (gpt-5.5)** — per the\n   `DELEGATE_CONTENT_TO_GPT` invariant, Claude orchestrates and GPT writes\n5. **Apply the Asecurio brand** — load `asecurio_brand` skill, paste the\n   CSS tokens at the top of the stylesheet\n6. **Render with WeasyPrint** to PDF\n7. **Verify** with `fitz` (PyMuPDF) — no text beyond the safe area, no\n   `■` placeholders (means a font registration was missed)\n8. **Upload via `upload_to_slack`** into the active thread\n\n## Asecurio brand profile (the only profile we ship)\n\nAnchored in [asecurio_brand](../asecurio_brand/SKILL.md). The brand uses two\ncolours plus high contrast:\n\n- `#279AA8` — accent (headings, dividers, link text, callout borders)\n- `#0F172A` — dark surface (hero cover, section dividers, table headers)\n- White (`#FFFFFF`) and black (`#000000`) — body, depending on background\n\nFonts (load via Google Fonts in the HTML head):\n\n```html\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Montserrat:wght@700;800&family=Lato:wght@400;700&display=swap\" rel=\"stylesheet\">\n```\n\nIf the deployment box has no outbound HTTPS (e.g. an air-gapped run), fall\nback to DejaVuSans + DejaVuSans-Bold from `/usr/share/fonts/truetype/dejavu/`\n— both ship Unicode coverage for Polish and are present on every Debian /\nUbuntu container.\n\nCSS skeleton — paste this at the top of every stylesheet:\n\n```css\n:root {\n  --c-accent: #279AA8;\n  --c-dark-bg: #0F172A;\n  --c-ink: #000000;\n  --c-ink-on-dark: #FFFFFF;\n  --font-heading: \"Montserrat\", \"DejaVu Sans\", sans-serif;\n  --font-body: \"Lato\", \"Open Sans\", \"DejaVu Sans\", sans-serif;\n}\nbody { font-family: var(--font-body); color: var(--c-ink); }\nh1, h2, h3 { font-family: var(--font-heading); font-weight: 800; }\nh1 { color: var(--c-accent); letter-spacing: -0.02em; }\na { color: var(--c-accent); }\n```\n\n## Canvas + safe area\n\n### Deck (1920 × 1080)\n\n```css\n@page { size: 1920px 1080px; margin: 0; }\n.slide {\n  position: relative;\n  width: 1920px; height: 1080px;  /* NEVER min-height */\n  padding: 80px 120px 80px;\n  box-sizing: border-box;\n  overflow: hidden;               /* violations are clipped, not allowed to grow */\n}\n```\n\n### Report (A4)\n\n```css\n@page {\n  size: A4;\n  margin: 20mm 18mm;\n  @bottom-right { content: counter(page) \" / \" counter(pages); font: 10pt \"Lato\"; color: #888; }\n}\nh1, h2, h3 { break-after: avoid; }\n.card, figure, table, tr { break-inside: avoid; }\n```\n\n## WeasyPrint Python recipe\n\n```python\nfrom weasyprint import HTML, default_url_fetcher\n\ndef fetcher(url, t=15):\n    \"\"\"Tolerant fetcher — skip failed Google Font requests gracefully.\"\"\"\n    try:\n        return default_url_fetcher(url, timeout=t)\n    except Exception:\n        return {\"string\": b\"\", \"mime_type\": \"image/jpeg\"}\n\nhtml_content = build_html()  # your assembled HTML+CSS string\nHTML(string=html_content, url_fetcher=fetcher).write_pdf(\"/tmp/out.pdf\")\n```\n\n### WeasyPrint quirks worth knowing\n\n- SVG `fill=\"currentColor\"` does NOT work — always use an explicit hex.\n- Charts: inline SVG only. Embedded `<img src=\"file://…\">` or `data:` base64\n  works but is fragile across versions; SVG is reliable.\n- `<a href=\"https://…\">` automatically becomes a clickable link in the PDF.\n- Reports must add: `h1-h4 { break-after: avoid; }` and `.card, figure,\n  table, tr { break-inside: avoid; }`.\n\n## Post-render verification\n\n```python\nimport fitz  # PyMuPDF\ndoc = fitz.open(\"/tmp/out.pdf\")\n\n# WeasyPrint emits 1920×1080 decks as 1440×810 pt (1px = 0.75pt). Scale up.\nfor i, page in enumerate(doc):\n    scale = 1920 / page.rect.width\n    for x0, y0, x1, y1, text, *_ in page.get_text(\"blocks\"):\n        if not text.strip():\n            continue\n        if \"■\" in text or \"?\" in text:\n            print(f\"page {i+1}: replacement-char detected — font missing for: {text!r}\")\n        if y1 * scale > 1000:\n            print(f\"slide {i+1}: bottom safe-area violation at y={y1*scale:.0f}\")\n        if x0 * scale < 120 or x1 * scale > 1800:\n            print(f\"slide {i+1}: side safe-area violation\")\n```\n\n## Slide grammar\n\n| Slide type | Layout |\n|---|---|\n| Hero / title | `#0F172A` background, white Montserrat ExtraBold 96-128px title, `#279AA8` accent bar (4px) bottom edge |\n| Content | white background, Montserrat Bold 36-44px heading in `#279AA8`, Lato 18-22px body in black; one H1 per slide |\n| Section divider | `#0F172A` background, white ExtraBold number + label (style mirrors the \"Strategia na wzrost\" deck) |\n| Stat row | 3-4 cards, Montserrat 96-144px figure, Lato uppercase tracked label below |\n| Tables | header `#0F172A` fill, white Montserrat Bold; body rows white with black Lato; accent (totals) `#279AA8` fill with white |\n\n## Composition rules (mechanical invariants from Viktor — kept)\n\n- No `border-left` sidebar lines. No accent rails. The accent is `#279AA8`,\n  used sparingly, not as a frame.\n- `border-collapse: collapse` on every table; 1px hairlines top + bottom of\n  the table, 1px between rows, NO vertical borders.\n- Avoid runts (1-3 character orphan word on the last line of a paragraph).\n- Flexbox items always get `min-width: 0`.\n- Use grid for finite sets, never `flex-wrap` for layout.\n- Parallel slides: the same element (logo, page number, eyebrow) sits at\n  the same `x, y` across every slide. Misalignment reads as amateur.\n\n## Charts\n\n| Type | Spec |\n|---|---|\n| Line | 2px stroke in `#279AA8`, no markers, no fill. Gridlines `#E5E5E5` |\n| Bar | solid `#279AA8` fills, squared corners, ~40% bar width, value labels above in Lato Bold |\n| Stat row | 3-4 cards, Montserrat 96-144px figure, Lato uppercase tracked label below |\n| Tables | header `#0F172A`, body white, accent (totals) `#279AA8` |\n\nCharts are inline SVG (not `<img>` to a PNG), so they remain vector at any\nzoom and the colour is editable in the PDF.\n\n## What this skill is NOT for\n\n- Editing an existing PDF (use `pdf_form_filling` or `pdf_signing`)\n- Filling a form (use `pdf_form_filling`)\n- Generating prose — that goes through `openai_chat` per\n  `DELEGATE_CONTENT_TO_GPT`. This skill is the layout and rendering layer.\n- White-label / blind output. Asecurio is always Asecurio.\n"
          },
          {
            "label": "pdf_form_filling",
            "desc": "Fill in PDF forms programmatically — application forms, intake sheets, tax / KRS / RODO declarations the user uploaded. Works with both AcroForm PDFs (have proper interactive fields) and flat PDFs (no fields, write text at coordinates). Use whenever someone hands Janusz a PDF and says \"wypełnij to\", \"fill this in\", \"uzupełnij formularz\". This skill is brand-neutral — it preserves whatever styling the source PDF already had.\n",
            "body": "---\nname: pdf_form_filling\ndescription: >\n  Fill in PDF forms programmatically — application forms, intake sheets,\n  tax / KRS / RODO declarations the user uploaded. Works with both\n  AcroForm PDFs (have proper interactive fields) and flat PDFs\n  (no fields, write text at coordinates). Use whenever someone hands\n  Janusz a PDF and says \"wypełnij to\", \"fill this in\", \"uzupełnij\n  formularz\". This skill is brand-neutral — it preserves whatever\n  styling the source PDF already had.\n---\n\n# PDF Form Filling\n\n## Mechanism\n\n`PyMuPDF` (imported as `fitz`) is the workhorse — same library used for\npost-render verification in `pdf_creation`. It supports both AcroForm\nwidget editing AND visual overlay (drawing text at x, y) for flat PDFs\nthat have no fillable fields.\n\n## Workflow\n\n### Step 1 — Detect whether the PDF has fillable fields\n\n```python\nimport fitz\n\ndoc = fitz.open(\"form.pdf\")\ntotal = 0\nfor page in doc:\n    for widget in page.widgets():\n        total += 1\n        print(f\"Field: {widget.field_name!r}, type: {widget.field_type_string}, value: {widget.field_value!r}\")\nprint(f\"\\nTotal fields: {total}\")\n```\n\nIf widgets exist → **AcroForm path** (Step 2A).\nIf `total == 0` → **visual overlay path** (Step 2B).\n\n### Step 2A — AcroForm (fillable PDF)\n\n```python\nimport fitz\n\ndoc = fitz.open(\"form.pdf\")\nfor page in doc:\n    for widget in page.widgets():\n        if widget.field_name == \"imie\":\n            widget.field_value = \"Jan Kowalski\"\n            widget.update()\n        elif widget.field_name == \"data\":\n            widget.field_value = \"2026-06-07\"\n            widget.update()\n        elif widget.field_name == \"akceptacja_rodo\":\n            widget.field_value = True  # checkbox\n            widget.update()\n\ndoc.save(\"form_filled.pdf\")\ndoc.close()\n```\n\nSupported widget types:\n- `Text` — text fields\n- `CheckBox` — `True` / `False`\n- `RadioButton` — name of the selected option\n- `ComboBox` / `ListBox` — string of the selected entry\n\n### Step 2B — Visual overlay (flat PDF, no fields)\n\nUsed for scanned forms, government PDFs that ship without AcroForm,\nor PDFs flattened during a previous edit. Coordinates are in **points**\n(72pt = 1 inch). Origin is **bottom-left** in PDF land — but `fitz`\nflips Y so origin is **top-left**.\n\n```python\nimport fitz\n\ndoc = fitz.open(\"scanned_form.pdf\")\npage = doc[0]\n\n# Approximate field positions — measure once on the source PDF.\npage.insert_text((140, 220), \"Jan Kowalski\",\n                 fontname=\"helv\",  # fallback; switch to a Unicode font if Polish chars\n                 fontsize=11,\n                 color=(0, 0, 0))\n\n# For Polish characters, register DejaVuSans:\npage.insert_font(fontname=\"DejaVu\", fontfile=\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\")\npage.insert_text((140, 260), \"Łódź, ul. Piłsudskiego 12\",\n                 fontname=\"DejaVu\",\n                 fontsize=11)\n\n# Checkboxes: draw an \"X\" or a tick glyph\npage.insert_text((280, 340), \"X\",\n                 fontname=\"helv\",\n                 fontsize=14,\n                 color=(0, 0, 0))\n\ndoc.save(\"scanned_form_filled.pdf\")\n```\n\n### Step 3 — Verify\n\nRe-open the saved PDF and either:\n- list widgets again (AcroForm path) and confirm `field_value` matches what you set\n- extract text by region (`page.get_text(\"blocks\")`) and assert your insertions landed where intended\n\n```python\ndoc2 = fitz.open(\"form_filled.pdf\")\nfor page in doc2:\n    for widget in page.widgets():\n        print(f\"✅ {widget.field_name}: {widget.field_value!r}\")\n```\n\n## Working out coordinates for visual overlay\n\nThe fastest workflow:\n\n1. Open the source PDF in a viewer with rulers (Preview, Adobe Reader, Skim).\n2. Note rough positions in inches or points for each blank.\n3. In Python, get the page dimensions to scale:\n\n   ```python\n   page = doc[0]\n   width_pt, height_pt = page.rect.width, page.rect.height\n   print(f\"Page is {width_pt:.0f} × {height_pt:.0f} pt\")\n   ```\n\n4. Iterate: insert text, render to PNG (`page.get_pixmap().save(\"preview.png\")`),\n   open the PNG, adjust coordinates, repeat. Usually 2-3 iterations to land it.\n\n## Polish character support\n\n`fitz.insert_text` falls back to PDF core fonts (Helvetica, Times) if you\ndon't register a Unicode-capable font. Core fonts do NOT cover Polish\ndiacritics. Always register DejaVu (ships with Debian/Ubuntu) or your\nbrand font before inserting Polish text.\n\n```python\npage.insert_font(fontname=\"DejaVu\", fontfile=\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\")\n```\n\nIf the page already uses a Polish-capable font and you want consistency,\nextract the embedded font from the source PDF first:\n\n```python\n# List embedded fonts\nfor font in doc.embeddedfile_names():\n    print(font)\n```\n\n## Anti-patterns\n\n- Writing on top of existing text without checking — measure first, the\n  PDF may have invisible text overlays you can't see in a viewer.\n- Using PyPDF2 / pypdf for form filling — they have an AcroForm API but\n  it's fragile across PDF versions; `fitz` is more reliable.\n- Filling an AcroForm PDF via visual overlay — the result looks correct\n  but the actual form fields stay empty, which breaks downstream parsers\n  that read field values (HR systems, government portals).\n\n## What this skill is NOT for\n\n- Adding a digital signature — use `pdf_signing`\n- Creating a fillable form from scratch — out of scope (rare, use Adobe\n  Acrobat if needed)\n- OCR on a scanned form to detect field positions — out of scope (use\n  Tesseract or Vision in a separate step)\n"
          },
          {
            "label": "pdf_signing",
            "desc": "Add a handwritten-looking signature to a PDF — at the bottom of a contract, on a signature line, or anywhere the user designates. Uses `Kalam` Google Font (script-style) to imitate handwriting. Use when the user says \"podpisz to\", \"add my signature\", \"wstaw podpis Bartosza\", \"sign this contract\". This is a VISUAL signature (a glyph that looks like a signature), NOT a cryptographic one. For cryptographic PDF signing (PAdES / qualified e-signature) the user must go through the EU Trusted List provider — out of scope for this skill.\n",
            "body": "---\nname: pdf_signing\ndescription: >\n  Add a handwritten-looking signature to a PDF — at the bottom of a\n  contract, on a signature line, or anywhere the user designates.\n  Uses `Kalam` Google Font (script-style) to imitate handwriting.\n  Use when the user says \"podpisz to\", \"add my signature\", \"wstaw\n  podpis Bartosza\", \"sign this contract\". This is a VISUAL signature\n  (a glyph that looks like a signature), NOT a cryptographic one.\n  For cryptographic PDF signing (PAdES / qualified e-signature)\n  the user must go through the EU Trusted List provider — out of\n  scope for this skill.\n---\n\n# PDF Signing (visual)\n\n## Scope — visual only\n\nThis skill inserts a glyph rendered in `Kalam-Regular` (a script font from\nGoogle Fonts) at a chosen position on a PDF. It produces a signature that\nlooks handwritten. It does NOT add a cryptographic signature, certificate,\nor timestamp.\n\nFor qualified e-signature (the Polish \"podpis kwalifikowany\" required for\nRODO consents, employment contracts, court filings) the signer must use a\nprovider on the EU Trusted List (e.g. mSzafir, eGiełda, Asseco SCS) — those\nflows are out of scope for Janusz.\n\n## Mechanism\n\n`PyMuPDF` (`fitz`) — same library as `pdf_form_filling` and the post-render\nverification step in `pdf_creation`. We register `Kalam-Regular` as a\ncustom font on the target page, then `insert_text` at the signature\ncoordinates.\n\n## Workflow\n\n### Step 1 — Get the Kalam font\n\nIf not already on disk in the container, fetch from Google Fonts (CDN\nhosts the TTF directly):\n\n```bash\nmkdir -p ~/fonts\ncurl -L -o ~/fonts/Kalam-Regular.ttf \\\n  \"https://github.com/google/fonts/raw/main/ofl/kalam/Kalam-Regular.ttf\"\n```\n\nOr in Python:\n\n```python\nimport urllib.request\nurllib.request.urlretrieve(\n    \"https://github.com/google/fonts/raw/main/ofl/kalam/Kalam-Regular.ttf\",\n    \"/tmp/Kalam-Regular.ttf\",\n)\n```\n\n### Step 2 — Insert the signature\n\n```python\nimport fitz\n\ndoc = fitz.open(\"contract.pdf\")\npage = doc[-1]  # last page is the usual signing spot; user may specify another\n\npage.insert_font(fontname=\"Kalam\", fontfile=\"/tmp/Kalam-Regular.ttf\")\n\n# Coordinates: bottom-left of where the signature should start.\n# Origin is top-left in fitz, units are points.\npage.insert_text(\n    point=(120, 720),\n    text=\"Bartosz Wasilonek\",\n    fontname=\"Kalam\",\n    fontsize=18,\n    color=(0, 0, 0.5),  # subtle navy ink, looks more natural than pure black\n)\n\ndoc.save(\"contract_signed.pdf\")\ndoc.close()\n```\n\n### Step 3 — Verify and post\n\nRe-read the signed PDF to confirm the text landed, then upload via\n`upload_to_slack` into the thread.\n\n```python\ndoc2 = fitz.open(\"contract_signed.pdf\")\nlast = doc2[-1]\nextracted = last.get_text(\"text\")\nassert \"Bartosz Wasilonek\" in extracted, \"Signature did not embed\"\n```\n\n## Choosing the signature position\n\nMost contracts have a signature line (`______________________`) above the\nperson's name. To find it programmatically:\n\n```python\nimport fitz\n\ndoc = fitz.open(\"contract.pdf\")\nfor page_num, page in enumerate(doc):\n    # Find lines of underscores as signature placeholders\n    for block in page.get_text(\"blocks\"):\n        x0, y0, x1, y1, text, *_ = block\n        if text.strip().startswith(\"_\") and len(text.strip()) > 5:\n            # Signature line is here — place the glyph slightly above\n            print(f\"page {page_num+1}: signature line at y={y0:.0f}, x range {x0:.0f}-{x1:.0f}\")\n```\n\nTypical positioning: x ~ 10pt from the left edge of the underscore line,\ny ~ `y0 - 14` (just above the line so the glyph descends onto it).\n\n## Multiple signatures (counterparties)\n\nIf both parties sign, do them in separate `insert_text` calls — each can\nhave its own font / colour / position. To imitate two different handwritings\nyou can register a second script font (e.g. Caveat or Sacramento from\nGoogle Fonts) for the second signature.\n\n```python\npage.insert_font(fontname=\"Kalam\", fontfile=\"/tmp/Kalam-Regular.ttf\")\npage.insert_font(fontname=\"Caveat\", fontfile=\"/tmp/Caveat-Regular.ttf\")\n\n# Asecurio side\npage.insert_text((120, 720), \"Bartosz Wasilonek\", fontname=\"Kalam\", fontsize=18, color=(0, 0, 0.5))\n\n# Counterparty side\npage.insert_text((420, 720), \"Jan Kowalski\", fontname=\"Caveat\", fontsize=18, color=(0, 0, 0.4))\n```\n\n## Polish characters\n\n`Kalam-Regular` covers Latin Extended-A which includes Polish diacritics\n(ą, ć, ę, ł, ń, ó, ś, ź, ż). No additional registration needed.\n\n## Anti-patterns\n\n- Claiming this is a legally binding signature — it is not. Always tell the\n  user: \"Dodałem podpis wizualny — to nie jest podpis kwalifikowany\".\n- Forging someone else's signature — Janusz only signs as the requesting user\n  (e.g. Bartosz signing his own contracts). If a user asks Janusz to \"podpisz\n  za Pawła\", refuse and explain.\n- Using the default PDF Helvetica font for the signature — looks ridiculous,\n  also breaks Polish characters.\n\n## What this skill is NOT for\n\n- Qualified electronic signature (PAdES / XAdES / mSzafir)\n- Adding a certified timestamp (CAdES)\n- Verifying an existing signature on a PDF\n- Filling form fields — use `pdf_form_filling`\n"
          },
          {
            "label": "pptx_editing",
            "desc": "Edit existing `.pptx` PowerPoint files — replace text in a client deck, swap a date, change a logo, add a slide to a recurring report. Use when working with a `.pptx` already on disk or downloaded from Google Drive. For creating a presentation from scratch with full design control, use `pdf_creation` instead (HTML/CSS + WeasyPrint gives a better Asecurio-branded result than python-pptx). Pair with `asecurio_brand` for colour/font choices when adding new content.\n",
            "body": "---\nname: pptx_editing\ndescription: >\n  Edit existing `.pptx` PowerPoint files — replace text in a client deck,\n  swap a date, change a logo, add a slide to a recurring report.\n  Use when working with a `.pptx` already on disk or downloaded from\n  Google Drive. For creating a presentation from scratch with full design\n  control, use `pdf_creation` instead (HTML/CSS + WeasyPrint gives a\n  better Asecurio-branded result than python-pptx). Pair with\n  `asecurio_brand` for colour/font choices when adding new content.\n---\n\n# PPTX Editing — Asecurio\n\n## When to use\n\n- Edit an existing client deck (update text, logo, dates)\n- Add a slide to a monthly report deck\n- Replace placeholders in a sales template with real data\n- Fix typos / rebrand someone else's PowerPoint\n\n## When NOT to use\n\n- Create a deck from scratch with bespoke design — use `pdf_creation` (HTML+CSS+WeasyPrint).\n  python-pptx can create slides but the styling API is awkward; HTML/CSS produces a\n  more polished, on-brand result for the same effort.\n\n## Mechanism\n\n`python-pptx` reads and modifies `.pptx` files (which are zip archives of XML)\nwithout opening PowerPoint. The library is a thin wrapper around the OOXML\nspec — no rendering, no headless office. Operations are fast and deterministic.\n\n## Workflow\n\n### Step 1 — Read the structure first\n\nBefore editing anything, dump what's in the deck. A `.pptx` file usually\ncontains layouts (named slide masters) and shapes that look identical in\nthe editor but have different XML names — you need the names to target\nedits precisely.\n\n```python\nfrom pptx import Presentation\nfrom pptx.util import Inches, Pt\n\nprs = Presentation(\"client_deck.pptx\")\nfor i, slide in enumerate(prs.slides):\n    print(f\"\\n=== Slide {i+1} (layout: {slide.slide_layout.name}) ===\")\n    for shape in slide.shapes:\n        print(f\"  Shape: {shape.shape_type}, name={shape.name!r}\")\n        if shape.has_text_frame:\n            for para in shape.text_frame.paragraphs:\n                if para.text.strip():\n                    print(f\"    Text: {para.text!r}\")\n```\n\n### Step 2 — Edit at the run level, not the paragraph level\n\npython-pptx stores text in *runs* — fragments with identical formatting.\nReplacing a whole paragraph with `paragraph.text = \"…\"` collapses every\nrun into one, which destroys colour / weight / size variations the\noriginal deck used.\n\n```python\n# ❌ WRONG — destroys formatting\npara.text = \"new text\"\n\n# ✅ RIGHT — preserves formatting on each run\nfor run in para.runs:\n    if \"old\" in run.text:\n        run.text = run.text.replace(\"old\", \"new\")\n```\n\n### Step 3 — Adding a new slide: use a blank layout\n\n```python\n# ❌ WRONG — layout[0] usually has placeholders that will show\n# \"Click to add title\" if you don't fill them.\nslide = prs.slides.add_slide(prs.slide_layouts[0])\n\n# ✅ RIGHT — blank layout has no placeholders\nblank_layout = prs.slide_layouts[6]\nslide = prs.slides.add_slide(blank_layout)\n```\n\nIf the source deck only ships layouts with placeholders, remove empty\nplaceholders after adding the slide:\n\n```python\nfor shape in list(slide.placeholders):\n    if not shape.text.strip():\n        sp = shape._element\n        sp.getparent().remove(sp)\n```\n\n### Step 4 — Verify by re-reading\n\nAfter saving, reopen the file and assert the changes landed. PowerPoint\ncan be lenient on save-time errors that surface only when a user opens\nthe file later.\n\n```python\nprs2 = Presentation(\"client_deck.edited.pptx\")\nfor slide in prs2.slides:\n    for shape in slide.shapes:\n        if shape.has_text_frame:\n            assert \"old text\" not in shape.text_frame.text, shape.text_frame.text\n```\n\n## Applying Asecurio brand to new content\n\nWhen you add a textbox or slide, apply the brand (see [asecurio_brand](../asecurio_brand/SKILL.md)).\n\n```python\nfrom pptx.util import Pt\nfrom pptx.dml.color import RGBColor\n\nACCENT = RGBColor(0x27, 0x9A, 0xA8)\nDARK_BG = RGBColor(0x0F, 0x17, 0x2A)\nWHITE = RGBColor(0xFF, 0xFF, 0xFF)\nBLACK = RGBColor(0x00, 0x00, 0x00)\n\ntxBox = slide.shapes.add_textbox(Inches(0.8), Inches(0.6), Inches(8.4), Inches(1.2))\npara = txBox.text_frame.paragraphs[0]\nrun = para.add_run()\nrun.text = \"New heading\"\nrun.font.name = \"Montserrat\"\nrun.font.bold = True\nrun.font.size = Pt(32)\nrun.font.color.rgb = ACCENT  # heading in brand accent\n```\n\nFor dark-background section dividers:\n\n```python\nbackground = slide.background\nbackground.fill.solid()\nbackground.fill.fore_color.rgb = DARK_BG\n\n# Title in white Montserrat ExtraBold for high contrast on dark\nrun.font.color.rgb = WHITE\nrun.font.name = \"Montserrat\"\nrun.font.bold = True\n```\n\n## Common operations\n\n### Resize / reposition a shape\n\n```python\nfrom pptx.util import Inches\n\nshape.left   = Inches(1)\nshape.top    = Inches(2)\nshape.width  = Inches(8)\nshape.height = Inches(3)\n```\n\n### Add an image (e.g. logo replacement)\n\n```python\nslide.shapes.add_picture(\"logo_asecurio.png\", Inches(8), Inches(0.5), Inches(1.5))\n```\n\n### Iterate through tables in the deck\n\nA common bug: text inside tables is NOT in `slide.shapes` directly — it's\nnested in cell paragraphs. Targeting only `shape.text_frame` misses\neverything inside tables.\n\n```python\nfor slide in prs.slides:\n    for shape in slide.shapes:\n        if shape.has_table:\n            for row in shape.table.rows:\n                for cell in row.cells:\n                    for para in cell.text_frame.paragraphs:\n                        for run in para.runs:\n                            if \"PLACEHOLDER\" in run.text:\n                                run.text = run.text.replace(\"PLACEHOLDER\", real_value)\n```\n\n## .pptx on Google Drive\n\nEditing a `.pptx` that lives on Google Drive:\n\n1. Download via `gdrive_download(file_id, \"deck.pptx\")`\n2. Edit with python-pptx as above\n3. Upload back with `gdrive_update_file(file_id, \"deck.pptx\")` — this\n   replaces the file content in-place, preserving share permissions.\n\n`.pptx` files on Google Drive cannot be edited via the Slides API (it\nreturns \"The document must not be an Office file\"). The download → edit →\nupload round-trip is the standard workaround.\n\n## Anti-patterns\n\n- Setting `paragraph.text` destroys run-level formatting; always edit per run.\n- Using slide_layouts[0] when a blank layout (usually [6]) exists.\n- Forgetting tables — they are not in `slide.shapes` traversal by default.\n- Mixing font names — apply Montserrat for headings, Lato for body, nothing else.\n- Hex colours outside the brand palette. Asecurio uses `#279AA8`, `#0F172A`,\n  white, black — full stop.\n\n## What this skill is NOT for\n\n- Generating a deck from scratch (use `pdf_creation`)\n- PowerPoint OLE automation (Microsoft Office is not installed in the container)\n- Rendering slides to PNG / image previews (use `fitz` or LibreOffice headless if needed)\n"
          },
          {
            "label": "user_bartosz",
            "desc": "Preferences and context for Bartosz Wasilonek (CEO / COO Asecurio). Auto-loaded when DMing or @mentioned by him.",
            "body": ""
          },
          {
            "label": "writing_docs",
            "desc": "Asecurio documentation standard — based on ISO 24495-1:2023 (Plain Language). Read before drafting any Asecurio document.",
            "body": "---\nname: writing_docs\ndescription: Asecurio documentation standard — based on ISO 24495-1:2023 (Plain Language). Read before drafting any Asecurio document.\n---\n\n# Asecurio Writing Standard\n\nAsecurio docs follow **ISO 24495-1:2023 Plain Language**. The reader should be able to find what they need, understand it on first read, and use it.\n\n## Core rules\n\n1. **Reader first.** Write for the person who'll use it, not the person who wrote it.\n2. **Short sentences.** One idea per sentence. Aim for ≤20 words.\n3. **Plain words.** Replace jargon with everyday words. Spell out acronyms on first use.\n4. **Active voice.** \"Bartosz approves the contract\" — not \"the contract is approved by Bartosz\".\n5. **Concrete > abstract.** Examples beat generalities.\n6. **One section, one job.** If a section answers more than one question, split it.\n\n## Structure pattern (works for SOPs, policies, playbooks, PRDs)\n\n```\n# Page title (what this is, in one line)\n\n## What this is for (the reader's question / problem)\n\n## When to use it (preconditions, triggers)\n\n## Steps / process\n1. Step\n2. Step\n3. Step\n\n## Examples (concrete, real)\n\n## Owner & last review\n- Owner: <name>\n- Last reviewed: <date>\n```\n\n## Polish-language notes\n- Avoid Polish corporate fillers: \"w ramach\", \"w celu\", \"w związku z\" — replace with simpler equivalents.\n- \"*Klient*\" is fine; \"*Strona zamawiająca*\" is not (use the simple word).\n- Don't translate brand names (\"Notion\", \"Slack\", \"Gamma\" stay as is).\n\n## What to verify before publishing\n- [ ] Title says what the page is.\n- [ ] First paragraph answers \"why am I here?\"\n- [ ] Every list is parallel (same structure across items).\n- [ ] No undefined acronyms.\n- [ ] Owner and last-review date set.\n\n## Things to NEVER do\n- Don't paste raw client names into a published doc without checking confidentiality.\n- Don't quote prices/rates without confirming with Bartosz.\n- Don't use \"tbd\" without a date by which it'll be filled.\n"
          }
        ]
      }
    ],
    "control": [
      "Cost estimator",
      "Monthly spend",
      "Cost persistence",
      "Approvals",
      "Pending edits",
      "Thread claims",
      "Pricing watchdog",
      "Repo self-edit"
    ],
    "slash_commands": []
  },
  "operations": [],
  "versions": [],
  "meta": {
    "cost_tracking": "not_wired",
    "validation": [],
    "config_present": false
  }
}