--- name: carta-workhub-build description: > Builds or rebuilds the Carta Workhub live artifact — a standalone Cowork view of the work a firm shares with its Carta fund admin team. Shows a request composer over the GP tasks it has a panel for, in tabs for Drafts, Needs Action (waiting on you), With Carta and Completed, grouped by category — requests, capital calls and distributions, financial reporting — with a thread view for each request, the capital call review and the reporting tracker. The artifact auto-detects the active firm from the Carta MCP context — no hardcoded firm name needed. Use this skill whenever the user asks to "build the carta workhub artifact", "rebuild carta workhub", "set up carta workhub", "deploy carta workhub", "show my Carta workhub", "rebuild carta tasks", "show my Carta task board", or "pin my Carta requests". model: sonnet allowed-tools: - Bash(uv run "/scripts/build_artifact.py" *) - Artifact --- carta-investors:6.71.0 # Carta Workhub — Build / Redeploy Deploys the `carta-workhub` Cowork live artifact. It is **assembled** from source parts in this skill's `resources/` directory by `scripts/build_artifact.py`, which also substitutes this session's Carta MCP server ID. You never need to read the assembled HTML. Carta Workhub is the work surface: a live queue belongs on something you pin and keep open, not inside a fund-data dashboard. ## What the artifact does - **Composer** — "Ask Carta to do something" opens a box with preset tiles from `resources/carta-workhub.config.js`; clicking one drops a labelled template into the textarea. Templates, not filled examples: the blanks tell the sender what the team needs, and a prefilled amount invites sending someone else's numbers. The tiles mirror the app's Quick actions grid but carry no "Ask my Carta fund admin team to…" preamble: that phrasing exists to route the chat picker, and here the text goes straight to the team, so it would only be noise in what they read. Then a second **Review summary** step shows the draft verbatim before anything reaches `fa:create:fund-admin-message`. Send lives only on that second step; **Back to edit** returns the text rather than an empty box. - **Beta badge** — on the page title. The subtitle is the only body copy above the composer. - **Open items** — review infers the request type from `detect` in `resources/carta-workhub.config.js` and checks each `requires` entry against the request, with label-only template lines stripped so a blank template reads as unspecified. Anything unmet is named in one line, with a single open box to add it. Nothing is forced: the box folds into the request on send, and sending with it empty is fine. This is pattern matching, not inference — the artifact has no model, so requirements are declared per preset. An unrecognised request gets no checklist rather than a wrong one. **A `requires` regex must match what a sender writes, not what the template says.** The capital-call check looked for `lps|investors|partners|class`, which the template's own "Split by LP class" line satisfied — so a request that named its call type instead ("Call Type: Pro-rata") was told it was missing information it had already given. That requirement is now `Call type`, matching the four types Carta acts on: pro-rata, subsequent close, bring investors in-line, hybrid. Change a template line and its `requires` entry together, or the check drifts back into testing for the old wording. - **Draft** — **Save as draft** holds a request without sending it. Drafts show in the **Drafts** tab, dashed, with **Review and send** or **Discard**. Sending a draft runs the same confirm step, and the draft is dropped only once the send succeeds. Carta has no unsent-draft state, so a draft lives in `localStorage` and nowhere else, and does not follow the user to another machine. Storage also throws on an opaque origin (an artifact served from a `data:` URL), so a probe at first use decides which of two truths every surface tells — saved on this computer, or kept for this session only. The composer states the scope **before** Save as draft is pressed, and the Drafts tab's description repeats it where drafts are listed. A durable, cross-device draft needs a server-side command that does not exist yet: carta-mcp#1. (The code calls these plans: `FAR_PLANS_KEY`, `farSavePlan`.) - **Sent state** — a confirmation panel: "Your Carta team is on it", the notification promise, and a line telling the sender they can reply or add detail in the thread while work is underway. It deliberately does **not** show the workflow id — that is Carta's internal handle, and quoting it at the sender implies it is how they follow up, when the thread is. - **Request type** — `fa:create:fund-admin-message` has no type field and the backend stamps `request_type: 'other'`, so a sent request would lose its category. The type is carried two ways: as the message's **first line**, which is durable server-side and is the first thing the team reads, and cached in `localStorage` against the workflow id. Otherwise the title is the row's name, which fund-admin builds from the subject line or the message's first line; a row still carrying a generic name (`FAR_GENERIC_TITLES`) reads its opening message instead (bounded to `FAR_HYDRATE_MAX`). **The backend wraps message bodies.** `content_text` comes back as `" Additional Info:\n "`. That preamble is Carta's own formatting, so `farUnwrap` strips it and the indent everywhere text is read or shown — without it every title read as "Additional Info:" and the thread showed the wrapper to the customer. - **Queue** — four tabs, each a row group: **Drafts** (`planned`), **Needs Action** (`todo`, waiting on the customer), **With Carta** (`progress`) and **Completed** (`done`). The first load opens on Needs Action, or on the next tab holding work when it is empty (`FAR_TAB_OPEN_ORDER`); after that only the viewer changes tabs. Each tab carries a count pill and a one-line description. The queue holds the workflows Workhub has a panel for, plus expense payments, management fee reviews, cash reconciliation and SOI reviews, listed in `TASK_TEMPLATES_WITH_TILES` in `resources/carta-workhub.config.js`: requests (`request-generic`), capital activities (`request-capital-activity`), financial packages (`publish-financial-package`), expense payments (`prepare-and-pay-expense`), management fee reviews (`review-management-fees`), cash reconciliation (`cash-reconciliation`, one workflow per bank transaction) and SOI reviews (`review-soi-v2`). No panel opens the last four, so each is a Carta link under Expense payments, Management fees, Cash reconciliation or SOI review, unless its needs-action task opens an MCP App (see Task MCP Apps). Every other GP task would only link out to Carta, so it is left out; add a template to that list once a panel opens its tasks. Completed reads the same list, so finished work never shows a kind of task the open tabs leave out. Every task of a listed workflow shows, including the ones no panel opens (a capital call With Carta or Completed), as a Carta link. Inside a tab, rows are grouped by category, in the order and with the icons `TASK_CATEGORIES` in `resources/carta-workhub.config.js` gives; a row joins the category listing its `workflow_template`, and a listed template no category lists lands in **Other**. A capital activity is a call or a distribution only by its name — fund-admin titles the task "Review capital call for…" or "Authorize distribution of…" from the activity type — so `named` on Distributions decides there. Filter chips under the description (All, then each category present, with counts) narrow the tab to one category; each tab keeps its own. A category previews 5 items, then **Show N more**. Sorted **Newest** or **Oldest** first, by when the work started, or for Completed by when it finished. There is deliberately no sort-by-status or category: the queue is partitioned by both, so it would be a no-op. Cards in a category that share a calendar day show the time as well, or a re-sort looks like nothing happened. The card/list toggle swaps the grid for one row per item. A card is the task's name (fund-admin's `display_name`; a request keeps its own title rules, above), the row's `entity_name` (omitted when blank — `fa:create:fund-admin-message` has no entity field, see `docs/plans/carta-workhub-entity-uuid.md`), and a date line: **As of** in Needs Action, **Started** in With Carta, **Completed** or **Canceled** in Completed, **Drafted** in Drafts. What it opens, in order: the capital call review panel, the reporting tracker, the request's thread, else the row's `_links.web_url` in a new tab, marked **Carta ↗** — an absolute http(s) link only. A task with none of these is shown but opens nothing. No assignee shows: neither list carries one yet. The page is light only, as the Workhub design is: `:root` sets `color-scheme: light`, so every `light-dark()` token resolves to its light value. **Two cursor-paged lists feed the queue**, read in parallel at 40 rows a page so no reply nears carta-mcp's 40k cap. On first load each tab paints once its reads land and counts `…` until then: Needs Action after its half of the task list, With Carta after both halves, Completed after both halves and its own list: - `fa:list:gp-workhub-active-task` — read as two halves, `pending_actor: customer` and `carta`. Every open task is kept when its `workflow_template` is in `TASK_TEMPLATES_WITH_TILES` (the command takes no template filter), grouped by `pending_actor` (`customer` → Needs Action, `carta` → With Carta), one card per workflow; the customer's task wins. A capital call review opens its panel only when `discover` answers for `fa:get:capital-activity-review-summary` — the panel's commands need `CARTA_MCP_CAPITAL_ACTIVITY_REVIEW`, which the list ignores — and is otherwise a Capital calls card linking to the review in Carta. A failed page, or pages left past 25, drops to the `localStorage` path rather than show a short list as complete. A task's `created_at` is the current task's, so the start date of a request or capital activity comes from `fa:list:firm-workflow` (`active`, those two templates), read alongside; other tasks use their own. - `fa:list:firm-workflow` (`TASK_TEMPLATES_WITH_TILES`, `complete`/`canceled`) — Completed, the last 90 days by `completed_date` (else `last_activity_at`), newest 3 pages only, keeping what was read; its count reads `120+` at that cap, and a plain `0` when nothing read is recent. The list is ordered by `created_at`, so it can miss old work finished recently. A failure leaves it empty. A workflow in both lists shows once, as open. - **Capital call review** — a `request-capital-activity` workflow carrying an open `review-capital-activity` (or `review-capital-activity-changes`) task opens the review panel instead of the thread. It is one page: a sidebar with the summary (the total, the notice and due dates, any adjustments, and for a distribution what it can pay out) and the payment accounts, beside the preparer's note and the **Allocations**, **Notice** and **Delivery** tabs, and **Wires** for an activity that pays out. The Notice tab shows the notice each investor receives, as the real PDF and as the rendered email. The footer carries **Request changes**, which opens a modal and keeps an unsent request as a draft on this computer, and **Approve and release**, behind its own confirm step. Read from `fa:get:capital-activity-review-summary`, `fa:list:capital-activity-review-row` (its rows carry each group's `wire_status` for the Wires tab), `fa:get:capital-activity-partner-email-preview` and `fa:get:capital-activity-notice-pdf-preview`; written with `fa:mutate:request-capital-activity-changes` and `fa:mutate:approve-capital-activity`. All of them ride the `fetch` and `mutate` tools already in the grant, so adding a command never changes the publish call. There is no build flag for this: the server decides which rows exist, so no review card means the environment does not serve them. `--ccr-fund-uuid` / `--ccr-activity-id` seed one card for a demo, and its panel still reads through the same commands. The seed card replaces that activity's live review card. **The summary's embedded rows are the table's first paint.** `rows.results` on the summary is the unfiltered first page; seeding from it means the table is populated the moment the summary lands, instead of reporting zero investors until the row walk returns. The walk then replaces the seed wholesale rather than emptying it first. A count that is not yet known reads `Totals`, never `0`. **Allocations reads buckets from `bucket_totals`, never from rows.** The summary's `bucket_totals` is the complete column set, in served order; a row's `amount_buckets` lists only the buckets that move money for that interest, so a bucket absent from a row is a zero cell and the column set must never be derived from rows. The summary view is Investor / Commitment / Net contribution / Called after; when more than one non-adjustment bucket or any adjustment composes the net, **Show breakdown** adds one column per bucket, adjustments last and signed by `impact_on_owed` (decrease reads as a reduction), with Investor and Partner class pinned left and Net contribution / Called after pinned right while the rest scrolls; the sidebar steps aside while the breakdown is open. Header labels are the full `display_name`, balanced over at most two lines and never abbreviated. Nothing reads `inside_commitment`: the post-call figures are served already computed. Totals come from `bucket_totals` and the summary, never by summing a page. The Participating and Non-participating counts above the table switch which investors it lists. When the row walk stops short the table says "Only N of M participating investors loaded", and after a complete walk a bucket whose total no loaded row carries is called out under the table rather than shown as a column of dashes. The review panel is `min(1160px, 96vw)` by `min(780px, 92vh)`. **The email preview follows `carta-home`'s `renderEmailPreview`** — the same command, the same envelope-plus-scriptless-iframe shape, the same `[/LINK_CARTA]` caveat. Recipients key on `addr_type` (`TO` / `CC` / `BCC`), not `type`. Keep the two behaviourally identical. **The Notice tab shows the real PDF, painted by a renderer the artifact carries.** Read with `fa:get:capital-activity-notice-pdf-preview`, which returns the document as a base64 `data:` URI; pdf.js, vendored under `resources/vendor/`, paints it to canvas. The notice is never redrawn from figures — a replica of a document drifts from the document. Three measured facts hold that shape in place, and none of them is obvious: - **The browser's own viewer is blocked here.** ``, `