# Carta Issuance

> Issue securities on a Carta cap table. Use when the user asks to issue certificates, stock certificates, option grants (ISO, NSO, EMI, CSOP, Unapproved, Startup Concessions, Non-Concessional, ZEPO), profits interest units (PIUs), to draft shares, grants or units, or to resume issuing from a draft set. USE WHEN the user says "issue", "grant", "draft", "award", "give equity", "give shares", "give stock", "create a certificate", "create a grant", "issue a profits interest", "issue PIUs", "grant profits interest units", "set up an option grant", "issue equity to a named person", or names any specific security type above. Also USE WHEN the user points at a spreadsheet, CSV, Carta import template, or a grant/award document as the source of the issuance ("issue the grants in this file", "here's our import template").

- Skill: `carta/carta-issuance` (Agent Skill, multi-file: 17 files)
- Install (CLI): `npx skillmds@latest add carta/carta-issuance`
- Raw SKILL.md: https://api.skillmd.com/api/skills/carta/carta-issuance/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: carta (https://skillmd.com/u/carta)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/carta/carta-issuance

---


<!-- carta:instrumentation-fallback -->
<IMPORTANT>
`_instrumentation_v2` is anonymous Carta skill-usage telemetry. ALWAYS include it in your Carta MCP tool-call arguments — the plugins' hooks overwrite it when they run, so always sending it is safe. Shape:
`_instrumentation_v2: {"skills": ["<plugin>:<skill>", ...], "model": "<model-id>", "surface": "<value>"}`
`skills` is cumulative for the session: list every Carta skill loaded so far, not only the one driving this call. Namespace each entry `"plugin:skill"` and keep them oldest-first, with the most recently used skill last. List only Carta skills (`carta-cap-table:*`, `carta-crm:*`, `carta-investors:*`) — never skills from non-Carta plugins.
`surface` is the Claude surface you are running in: `"chat"` (claude.ai or the Claude app, i.e. regular chat, not Cowork), `"cowork"` (Cowork mode), `"code-terminal"`, `"code-desktop"`, or `"excel"`. Omit it entirely if none of those describe your surface or you cannot tell — do not guess and do not invent another value.
</IMPORTANT>

# Issue Securities

Walk an admin from raw input to issued certificates, option grants, or profits interest
units on a Carta cap table. Those three are the **only** security types this skill issues.

| Flow | Example prompts |
|---|---|
| Certificates | "Issue 1 cert for Jane Doe, 1000 Series A at $1.50." · "Draft 5 founder certs on Acme." |
| Option grants | "Issue 1000 ISOs to Jane at $1.50 on the 2024 Plan." · "Draft 10 ISOs for new hires." · "Issue an EMI grant — 2000 options at £0.50." |
| Profits interest units | "Issue 5,000 CC units to Jane with a $2.00 per-unit threshold." · "Grant a profits interest — 500 units, overall threshold $1M." · "Draft 5 PIUs on the 2024 Plan." |
| Spreadsheet / file | "Issue the grants in ~/Downloads/Q3-hires.xlsx." · "Here's our import template — issue these certs." · "Draft the grant in this signed award agreement." |
| Resume | "Resume draft set 472." · "Continue the 'Q2 hires' draft set." |

To **fix** an already-issued certificate, grant or unit, that's `carta-modify-issuables`,
not this skill.

**Out of scope** — stop and route to the Drafts UI in the Carta app for RSUs, SARs, CBUs,
warrants, convertibles, SAFEs, convertible debt, and for custom legends, vesting,
acceleration, or exercise periods:

> *"This skill issues certificates, option grants and profits interest units today. For \<thing\>, use the Drafts UI in the Carta app."*

**Never substitute the certificate flow for a PIU.** Server-side a PIU *is* a certificate row
with `type="PIU"`, so that path looks like a fallback if a PIU call is refused — it isn't. It
issues a plain unit certificate with no threshold value, a different security.

Carta's own import template has sheets for several of those, so an uploaded workbook routinely
contains rows this skill can't issue. [Phase 0.25](#phase-025--ingest-an-uploaded-file) skips
them and reports the count — never reshape an RSU row into an option grant to make it fit.

## Architecture — one engine, two surfaces

The transaction is identical everywhere; only the surface that collects and reviews it varies.

- **The engine** is this file: resolve `security_type` → fetch reference data → assemble rows →
  `save_drafts` / `validate_drafts` / `issue_securities` → recovery. It never knows which
  surface is in play.
- **The adapter** implements exactly three capabilities. Nothing else may branch on
  environment.

| Adapter | Selected when | `collectConfig` (0.5) | `showReview` (2) | `confirm` (2→3) |
|---|---|---|---|---|
| **Cowork** (primary, ~95% of usage) | `preview_start` **absent** | one `show_widget` form | chat markdown | one `AskUserQuestion` |
| **Code** | `preview_start` **present** | `render-panel` config panel | `render-panel` review panel | the panel's **Confirm & Issue** button |

**This file documents the Cowork path**, since that is nearly all real usage. If [Phase 0 Step
1](#step-1--detect-the-environment-from-the-tool-surface) selects the Code adapter, read
[code-adapter.md](references/code-adapter.md) — its **§0** lists every point where that adapter
diverges, and anything §0 does not mention behaves exactly as described here.

Read these two **in parallel with the first Carta fetch**, not before it —
[Phase 0.5](#phase-05--configure-the-issuance)'s `issuance_init` has no dependency on them, so
reading first only delays it:

- **[references/cowork-adapter.md](references/cowork-adapter.md)** — the form, the chat review,
  the confirm, and the authoritative per-block field list. Skip only if Step 1 selected Code.
- **[references/payload-reference.md](references/payload-reference.md)** — the authoritative
  field contract: types, formats, picklists, autofills, date quirks.

**The type-specific row file waits for `security_type`.** Read
[option-grant-fields.md](references/option-grant-fields.md), *or*
[certificate-fields.md](references/certificate-fields.md), *or*
[piu-fields.md](references/piu-fields.md) once that resolves — exactly one, and never
before. Loading the wrong one is pure cost, and a grant run has no use for Rule 144.

Both paths end the same way: the `issue_securities` mutate ([Phase 3](#phase-3--on-confirmation-run-the-mutate)).
The SDK's HITL prompt on that mutate is the final, irreversible gate — never the review gate.

---

## Hard rules

1. **The field contract lives in [payload-reference.md](references/payload-reference.md).**
   Read it before constructing any payload. No invented keys.
2. **Never mix two security types' fields in one mutate.** Run the skill once per type for a
   mixed request.
3. **One confirmation gate per mutate attempt** — never zero, never two stacked. On Cowork the
   gate is the `AskUserQuestion` in
   [cowork-adapter.md §3](references/cowork-adapter.md#3-confirm--one-askuserquestion); on Code
   it is the panel button, and stacking a question on an open panel suspends its submit watcher
   so the click never lands. Recovery questions after a server short-circuit are unrestricted
   on both. The SDK's HITL prompt is not this gate.
4. **Retry contract — reuse identity from the FIRST response.** `draft_set_id` from the first
   mutate goes on every subsequent `issue_securities`, `save_drafts`, `load_drafts`,
   `validate_drafts`, `resolve_duplicate_stakeholder`; omitting it makes the server auto-create
   a *second* draft set with the same incomplete rows. Each row's `draft_pk` from its first
   save goes on every retry row alongside *every* required field; omitting `draft_pk` inserts a
   new row instead of updating.
   **A timeout is not an error** — the call may have already succeeded server-side, so retrying
   with the wrong params risks a duplicate draft set or a double-issue. Read
   [payload-reference.md § Timeouts & retries](references/payload-reference.md#timeouts--retries)
   before retrying any mutate that timed out.
5. **The server is the source of truth.** Don't mirror its validation; surface its messages
   verbatim.
6. **Never delegate to a background agent.** The gates require interactive HITL.
7. **Templates only — no custom payloads** for legends, vesting, acceleration, or exercise
   periods: *"Custom \<thing\> isn't supported here. Save as draft and finish in the Drafts UI."*
8. **No id sniffing.** Required values come from user input, `cap_table:get:stakeholders` with
   `detail=full`, or a documented default — never scraped from another grant or certificate. If
   none of the three applies, ask via `AskUserQuestion`.
9. **Pre-save assertion.** Before *any* `save_drafts` or `issue_securities` call, walk every row
   and confirm each `always` field (per [Row templates](#row-templates)) holds a non-null value.
   If one is missing, recover **before** the call, in order: (a) the row template's documented
   default; (b) re-run the stakeholder lookup (`detail=full`) and re-stamp
   `issue_date_relationship` / `email` / `stakeholder_kind`; (c) `AskUserQuestion`.
   This is load-bearing because both failure modes are **silent**: `save_drafts` accepts
   incomplete rows without complaint, and at issue time a row with `stakeholder_id=null` slips
   duplicate detection, creates zero securities, and still returns success. That the server
   accepts a row is therefore never a reason to send one — **never offer to save past a missing
   `always` field, and never describe the server's tolerance to the user as an option**
   ([save-validate-flow.md § The assertion is not advisory](references/save-validate-flow.md#the-assertion-is-not-advisory)).
10. **Never ask who the grantees are before opening the collection surface.** A missing
    recipient is an empty field on the surface, never a chat question — this is the single most
    common way this skill goes wrong. Two sub-rules follow from it:
    - **A bare "N \<securities\>" is a quantity, not a headcount.** *"100 option grants"*,
      *"100 certificates"* — the server's `quantity` field counts shares/options for **one**
      recipient. With no named people and no plural-**person** language, open **one** blank
      block with `quantity` pre-filled to N (`knowns.rows = [{"quantity": "100"}]`).
    - **Only pre-render multiple blank blocks when the language counts people** — *"100
      employees"*, *"grants for 100 new hires"*, or an explicit list of names. Then build
      `knowns.rows` as that many empty dicts so the surface opens pre-sized.
    - Genuinely ambiguous (rare) → `AskUserQuestion` which one. That is a real fork, not the
      forbidden "who are the grantees" question.
    - **"Which file did you mean?" is also a real fork**, not this forbidden question — but only
      when the prompt referenced a file and [Phase 0.25](#phase-025--ingest-an-uploaded-file)'s
      search found zero or several candidates, or the workbook has more than one importable
      sheet. Never use it to ask *who* is in the file, and never in place of parsing a path the
      prompt already gave.
11. **Issue what was validated, not a copy of it.** When a draft set already holds the rows the
    user approved, [Phase 3](#first-do-you-need-a-payload-at-all) issues with `draft_set_id` and
    **no `drafts` key**. Re-sending rows makes the issued payload merely *probably* identical to
    the reviewed one — a transposed digit anywhere in it issues terms nobody approved, and no
    later gate compares the two. Send rows again only to change them, and then with each
    `draft_pk` attached.

12. **Never build a collection surface you cannot prove you have the rules for.** See
    [Confirm this skill actually loaded](#confirm-this-skill-actually-loaded) — run that check
    before the surface, and stop rather than building one from partial context.

The incidents behind these rules — including the ones that look redundant — are in
[references/incidents.md](references/incidents.md). Read it before weakening any of them.

## Confirm this skill actually loaded

A `Skill` invocation can return *"Launching skill: carta-issuance"* and inject **no content**.
That has happened: the run continued for a full turn on a reference file it had read directly,
built a config form from it, and shipped a form whose option-type control was hardcoded to
ISO/NSO instead of gated to the corp's jurisdiction — a UK or AU corp would have been offered
the wrong tax treatments. The form looked entirely plausible. Nothing failed.

A partial load fails *confidently*, so check for it rather than waiting to notice. **Before
building any collection surface, confirm you can answer all three from loaded content — not
from memory, and not from a reference file you happened to open:**

| # | Question | Where the answer lives |
|---|---|---|
| 1 | How many numbered items are in [Hard rules](#hard-rules), and what does the **last** one say? | this file — the answer is **12**, and it is this check |
| 2 | Which **three** `so_type`s does the corp's resolved jurisdiction allow, and what does a wrong `"US"` default show a UK company? | [Phase 0.5](#option-grant-resolve-the-fmv-and-the-jurisdiction-before-building-the-surface) |
| 3 | Which script builds the surface on this adapter, which `--security-type` values does it take, and what must you never do instead? | [Phase 0.5](#phase-05--configure-the-issuance) |

Question 1 is self-verifying: rule 12 *is* this check, so a run that cannot name it is a run
that never loaded this file. Answering "11" means the content is stale or partial — stop.

**If any answer is missing, stop and say so.** Do not build the surface, and do not
reconstruct the rules from a reference file:

> *"The carta-issuance skill didn't load fully, so I don't have the issuance rules in front of
> me. Re-invoke it (or start a fresh message) and I'll pick this up from the top."*

Re-invoking is cheap. A form built on missing rules issues real securities on the wrong tax
treatment, and neither the review nor the server catches it.

---

## Voice & defaults

<!-- No [PATTERN carta-writing-style] block: the rules below are what this skill's
     user-facing text actually needs (tagged defaults, no raw ids, jargon explained
     on first use), and the shared pattern does not cover them. -->

- **Explain anything the skill chose.** Tag `(default)`, `(from existing record)`, or
  `(autofill — <so_type> rule)` with a one-line explanation under the review. The review is the
  user's only chance to reject a default, so an unshown default is one they never got to see.
- **Silent defaults are computable; prompted fields aren't.** If the skill can stamp it
  (today's date, the plan's grant term, an autofill rule), stamp it and surface it tagged.
  Never ask twice.
- **Show the full text of legally binding values** (e.g. the legend body), not just the
  template name.
- **Dates display as `MM/DD/YYYY`** everywhere the user sees them. Payload formats follow
  [payload-reference.md](references/payload-reference.md).
- **Explain jargon on first use** (Rule 144 date, Section 4(a)(2), INDIVIDUAL, legend).
- **No raw ids or payload field names in customer-facing text — ever.** Not in headers, status
  lines, prompts, confirmations, or error renderings. Never write the word "ID"
  (✅ *"looking up Jane"* / ❌ *"pulling stakeholder id 12345"*), and never render `(<number>)`
  after a name. Translate payload keys before surfacing them, including when echoing server
  `banner_errors` back: humanize mechanically (`_` → space, Title Case), with the exceptions
  listed in [references/labels.md](references/labels.md).

---

## Resolve `security_type`

Resolve once, at the top. Pass on every draft-set tool call.

| Cue | `security_type` |
|---|---|
| "cert", "certificate", "shares", "Series A", "common", "membership units" | `certificate` (default) |
| "option", "ISO", "NSO", "grant" (with plan), "EMI", "CSOP", "Unapproved", "Startup Concessions", "ESS", "Non-Concessional", "ZEPO" | `option_grant` |
| "PIU", "PIUs", "profits interest", "profits interest unit", "incentive units", "threshold", "hurdle" | `piu` |
| Ambiguous ("equity") | Ask with `AskUserQuestion` |
| Mixed in one prompt | Ask which to run first; run the others in follow-ups |
| Out-of-scope security | Route to the Drafts UI; stop |

**"units" and "membership units" are not PIU cues.** On an LLC, Carta's equity language
renames a *certificate* to a membership unit, so bare "units" lands at `certificate` at least
as often as at `piu`. Read it as `piu` only alongside a real PIU signal — "profits",
"incentive", a threshold or hurdle amount, or a named equity plan. Without one it is a real
fork → `AskUserQuestion`, never a silent pick.

---

## Phase 0 — Preflight

Four steps, in order, **all before any user interaction and before gathering any input.** Step
1 is free. Steps 2–4 are the only round trips this preflight may spend: one `ToolSearch`, one
connectivity check, one `list_accounts`. Phase 0.5 then spends exactly **one** more —
`issuance_init`, which carries the stakeholder lookup with it.

### Step 1 — Detect the environment from the tool surface

**Look at your own available tools.** `preview_start` present → **Code** adapter;
`preview_start` absent → **Cowork** adapter. That is the whole test: free, synchronous, and
correct in both environments, because `render-panel` cannot work without `preview_start`.

Do **not** run a Bash probe, check the filesystem, or reason about what the environment "looks
like" — the tool list is ground truth. Record the selection once and reuse it for Phases 0.5
and 2. Never re-detect per surface, never drop from Code to Cowork because a panel seems slow,
and never attempt `render-panel` when `preview_start` is absent.

### Step 2 — Load every tool in ONE ToolSearch call

```
ToolSearch: "select:mcp__carta__fetch,mcp__carta__mutate,mcp__carta__welcome,mcp__carta__list_accounts"
```

`mcp__carta__` is the placeholder prefix
([Step 2a](#step-2a--carta-command-names-hardcoded-never-discovered)) — when the session's
Carta tools carry a different prefix, substitute it into the `select:` string; the names after
the prefix never change. Zero matches on the literal `mcp__carta__` names means the wrong
prefix, not a disconnected server — re-check the session's tool list before treating it as the
Step 3 stop.

One call, four tools, the complete set for the run. **`mutate` is loaded here, up front**, so
Phase 2 never has to load it after the user confirms — that would be serial latency at the
worst possible moment. On the Cowork path, add `mcp__visualize__show_widget` to the same
`select:` list if it isn't already loaded.

**Never call `discover` or `search_tools` in the hot path.** Every command name is hardcoded
below; looking up a name you already know is a pure round trip. `discover` is a debugging aid.

### Step 2a — Carta command names (hardcoded, never discovered)

Reads go through `fetch`, writes through `mutate`. The argument key is **`params`**, not
`arguments`, and command names are **colon-separated**:

```
mcp__carta__fetch({"command": "cap_table:get:<noun>",     "params": {…}})
mcp__carta__mutate({"command": "cap_table:mutate:<noun>", "params": {…}})
```

**`mcp__carta__` is a placeholder** — here, in every code block below, and in every reference
file. The real prefix is environment-dependent (`mcp__carta-test__fetch`, plugin-scoped and
UUID-suffixed connector forms all occur). Resolve it from the session's tool list and
substitute it everywhere; only the prefix varies — tool and command names never do. The one
exception: the frontmatter `allowed-tools` entries are literal grant patterns — never
substitute there.

| Purpose | Command | Tool |
|---|---|---|
| Reference data for the collection surface, **plus named stakeholders** via `stakeholder_names` | `cap_table:get:issuance_init` | `fetch` |
| Stakeholder lookup for a roster **miss** — pass `names=` for several, `search=` for exactly one | `cap_table:get:stakeholders` | `fetch` |
| Load an existing set's rows | `cap_table:get:load_drafts` | `fetch` |
| List draft sets (resume by name) | `cap_table:list:draft_sets` | `fetch` |
| Cap-table totals for context math — authorized, outstanding, fully diluted, ownership % | `cap_table:get:cap_table_by_share_class` | `fetch` |
| Save rows, no validation | `cap_table:mutate:save_drafts` | `mutate` |
| Validate a saved set | `cap_table:mutate:validate_drafts` | `mutate` |
| Save + validate + dedupe + issue | `cap_table:mutate:issue_securities` | `mutate` |
| Resolve flagged duplicates | `cap_table:mutate:resolve_duplicate_stakeholder` | `mutate` |

> **The totals source has a breakdown-sounding name.** `cap_table:get:cap_table_by_share_class`
> — `corporation_id` alone — returns authorized, outstanding, fully diluted, and ownership %.
> Context math only (e.g. percent-of-fully-diluted for a grant), never a payload source. There
> is no `cap_table:get:cap_table_summary` — guessing it returns *"Unknown command"* — and the
> similar-sounding `cap_table_summary_report` is a different plugin's report command, not a
> name here; the row above is this skill's totals source.

**Go through `fetch`/`mutate`, not `call_tool`.** The runtime's tool descriptions deprecate
`fetch` in favour of `call_tool` and `discover` in favour of `search_tools`. That notice is
known and deliberately not followed — do not "fix" the contradiction. `fetch` stays because
the per-command tools `call_tool` would target are excluded from `tools/list` (the mechanics
below); the `discover` half is moot here because every command name is hardcoded in the table
above, so discovery never runs (full story:
[incidents.md § Round-trips](references/incidents.md#round-trips-that-bought-nothing)).
Both `fetch` and `mutate` are *pinned gateway* tools: always
present in `tools/list`, reachable in one hop, with scope and staff checks enforced inside the
command executor. The double-underscore form (`cap_table__mutate__issue_securities`) is not a
typo for a command name — carta-mcp also generates one hidden tool per command by swapping `:`
for `__` — but those are excluded from `tools/list` and reachable only via a
`search_tools` → `call_tool` round trip, which is the cost this skill's hot path exists to
avoid. When such a tool isn't visible to the session, that route returns *"Unknown tool"*
instead.

**Never call `set_context` for a corporation-scoped command.** Every command above takes
`corporation_id` as a direct param — pass it.

### Step 3 — Confirm Carta MCP connectivity

The whole flow depends on the Carta MCP server. When it doesn't answer, **classify the failure
before reporting it** — "not connected" and "Carta is briefly down" need opposite responses from
the user, and telling someone to reconnect a connection that was fine is its own failure.

| Signal | Meaning | Do |
|---|---|---|
| No Carta MCP tool in the tool list at all | Genuinely not connected | Stop with the message below |
| A call fails with HTTP 5xx / 502 / 503 / a gateway or HTML error body / a timeout | Transient upstream — the server is connected and briefly unhealthy | **Retry once**, then stop with the *temporary problem* message |

**Genuinely not connected** — stop before gathering any input:

> *"I can't reach Carta — the Carta MCP server isn't connected. Connect the Carta MCP server and try again."*

**Transient upstream** — retry the failed call exactly **once**. If the retry succeeds, continue
the run normally and say nothing about it. If it fails again, stop:

> *"Carta is having a temporary problem on its end — the connection is fine. Give it a minute and try again."*

**The retry cap is one, and it is a hard cap.** A second failure means waiting, not another
attempt: re-running the same call against a 502 cannot succeed, and repeated attempts are the
inner-loop thrash this skill's budgets exist to prevent. Do not vary the call to make a retry
look novel, do not fall back to a different tool or a `discover`/`search_tools` probe, and do not
treat an HTML error body as a data payload to parse — an HTML response to a JSON call is an
outage signal, never content.

### Step 4 — Resolve the corporation by name

If the prompt named a company and you don't already have its `corporation_id`, call
`list_accounts(search="<name>")` — **never** an unfiltered `list_accounts()`, which returns a
truncated alphabetical page that may never reach the name you want. `search` is the tool's own
name lookup; don't substitute a `discover` guess for it. Only ask the user via
`AskUserQuestion` if `search` returns zero or several ambiguous matches.

---

## Phase 0.25 — Ingest an uploaded file

**Skip this phase entirely unless the prompt references a file.** When it does, the file
replaces the prompt as the source of the rows — everything downstream is unchanged. It does not
add a path around any gate: Phase 1 still resolves, Phase 1.5 still saves and validates, Phase 2
still reviews, Phase 3 is still the only mutate.

Supported: `.xlsx` `.xlsm` `.csv` `.tsv` (deterministic) and `.pdf` `.docx` (text extraction —
see [Documents](#documents-pdf--docx)). The sub-skill
[issuance-import/SKILL.md](issuance-import/SKILL.md) owns the mechanics;
[issuance-import/references/column-map.md](issuance-import/references/column-map.md) is the
header vocabulary. **Never hand-read a workbook** — a column read by eye is how a quantity lands
in an exercise-price field.

### Step 0 — Confirm you can actually run the parser

The parser is a local script, so this phase needs `Bash(uv run *)`. **Check your own tool
surface for Bash before promising an import** — it is present on the Code adapter and is not
guaranteed on Cowork.

No Bash → **do not hand-read the file.** Reading a workbook by eye is the failure this whole
phase exists to prevent, and offering it as a fallback would make the parser's guarantees
optional. Say so and route to the feature built for this:

> *"I can't read spreadsheets in this session. Two options: import it directly in Carta's Drafts
> UI, which takes this same template — or paste the rows here as text and I'll set them up."*

Pasted-as-text rows are fine: they arrive in the prompt, so the ordinary prompt-driven flow
handles them with the user's own values in plain sight. That is different in kind from silently
parsing a binary nobody can see.

### Step 1 — Locate the file

Take the path straight from the prompt; a pasted `~/Downloads/…` path is the norm. Only if the
user said "the attached file" with no path, list the likely directories (`ls -t ~/Downloads`,
`~/Desktop`, the cwd) and look for a supported extension recently modified. Zero or several
plausible matches → `AskUserQuestion` which one (allowed by Hard rule 10's file carve-out).
`Bash(find *)` is deliberately not granted to this skill — use `ls`.

### Step 2 — Parse, before spending any round trip

```bash
uv run "${CLAUDE_PLUGIN_ROOT}/skills/carta-issuance/issuance-import/scripts/parse_upload.py" \
  --file "<path>" --out-dir "$OUT_DIR"
```

This first run is deliberately **without** `--reference`: it costs nothing, and its output tells
you the two things Phase 0.5's fetches need — the `security_type` and the names in the file. It
prints `SECURITY_TYPE=`, `ROW_COUNT=`, and the paths it wrote.

- **Exit 2 with `AMBIGUOUS:` + `CANDIDATES=[…]`** — the workbook has more than one importable
  sheet. `AskUserQuestion` which, then re-run with `--sheet "<name>"`. **Never merge two
  sheets into one batch** (Hard rule 2) and never pick for the user.
- **Exit 2 with `ERROR:`** — nothing usable. Surface the message verbatim and fall back to the
  ordinary prompt-driven flow; do not guess at rows.

**Reconcile `security_type` with the prompt.** File and prompt disagreeing is a real fork →
`AskUserQuestion`. The file wins only when the prompt never said.

### Step 3 — Fetch reference data (Phase 0.5's fetches, informed by the file)

Run [Phase 0.5](#phase-05--configure-the-issuance)'s fetches exactly as documented, with two
inputs now supplied by the file: `issuance_init`'s `security_type`, and its `stakeholder_names`
covering **the names the file contains** rather than the names the prompt named. That keeps the
lookup bounded by the file's row count, never by roster size.

`stakeholder_names` takes the whole list at once, which is the only correct shape here — a
40-row sheet resolved through a concatenated `search=` matches **nobody** and would create 40
duplicate stakeholders on a real cap table. Pass the names as a list; never join them into a
`search` string.

**The [account-setup gate](#account-setup-gate-option-grant-and-piu) still applies.** Having a
parsed file in hand is not a reason to push past it: a corp with no option-grant document set
cannot issue one, whether the rows came from a spreadsheet or from the prompt. Stop where the
gate says to stop — the parsed rows cost nothing and the file is still there afterwards.

### Step 4 — Re-run the parser to resolve names to ids

```bash
uv run "…/parse_upload.py" --file "<path>" [--sheet "<name>"] \
  --reference "$OUT_DIR/_data.json" --out-dir "$OUT_DIR"
```

`--reference` is the same `_data.json` you just built for `build_config.py`. The parser matches
the file's free text against it — vesting schedule, acceleration terms, share class (by name
**or** prefix), legend (by code or name), document set, equity plan, and the roster — and
writes `_import_knowns.json`.

**Unresolved is blank, never guessed.** A cell matching nothing leaves its field **unset** with
an `import_notes` entry. There is no fuzzy matching, and do not add any: an almost-match on a
vesting schedule or share class issues genuinely wrong terms, and unlike a bad quantity the
server cannot catch it.

### Step 5 — Merge into `knowns` and open the surface

`_import_knowns.json` holds `{security_type, rows, equity_plan_id?, batch_errors?}`. Its `rows`
**are** your `knowns.rows` — merge them in and continue into Phase 0.5 unchanged. The row count
comes from the file, so Hard rule 10's quantity-vs-headcount heuristic doesn't apply here (a
40-row sheet is unambiguously 40 blocks). Carry `batch_errors` through to the surface's
panel-level banner, and hold `equity_plan_id` for the first mutate only.

Each row may carry `import_notes` — `[{field, raw_value, reason}]`, **display-only**. Two
obligations, both load-bearing:

1. **Show every note on the surface**, against the field it's about. On Code, `build_config.py`
   renders them as amber markers automatically. On Cowork, render them the same way in the
   `show_widget` form — see
   [cowork-adapter.md §1](references/cowork-adapter.md#import-markers-uploaded-file-rows).
2. **Render a noted field with nothing selected**, so the surface's own readiness check blocks
   submission until the admin picks. `build_config.py` does this for you. A marker alone is
   ignorable; the blocked button is what actually prevents a silent wrong issuance.

**Strip `import_notes` before any mutate** — same discipline as the review-only fields
([Build the mutate payload](#build-the-mutate-payload-from-your-phase-1-resolved-rows)).
The server rejects unknown keys.

### Step 6 — Say what happened, in one line, before the surface opens

Read `_import_report.json` and report totals — never silently drop a column or a row. A dropped
`Exercise Price` column is a wrong-priced grant the user has no way to notice.

> *"Read 38 rows from Q3-grants.xlsx. State of Residency and Employee ID aren't fields this
> flow sets, and 3 values I couldn't match are flagged in the form — everything else is filled
> in. Review and submit when ready."*

Rows the file carried but this skill can't issue (RSUs, SARs, CBUs, warrants, RSAs,
convertibles) are skipped by the parser with a reason. **Name them and their count** — they need
the Drafts UI, and an admin who thinks a 40-row sheet issued 40 securities when it issued 37 has
been misled.

**Name the unmapped columns too, not just how many.** `unmapped_columns` in the report holds the
header labels this skill has no field for — several
([cowork-adapter.md](references/cowork-adapter.md#fields) lists them: state of residency, state
exemption, employee id, cost center, job title, salary, convertible note) are dropped by design,
and a file supplying one is an admin who expects it to land. *"2 columns I couldn't map"* leaves
them believing it did. Say which: *"State of Residency and Employee ID aren't fields this flow
sets — add them on the stakeholder record in Carta."* Naming a column costs a few words; a
silently dropped value the admin deliberately filled in is data loss they find out about later.

### Documents (`.pdf` / `.docx`)

The parser extracts text to `_import_text.txt` and stops — it writes no rows, because prose has
no fixed layout and a script guessing at it would guess silently.

Read the text, build the rows yourself in the parser's own row schema
([issuance-import/SKILL.md § Row schema](issuance-import/SKILL.md#row-schema)), and give every
field you filled this way an `import_notes` entry with `"confidence": "low"` so it renders as
needs-confirmation. Then continue from Step 3.

Take only what the document states. A grant agreement rarely names a vesting template by the
company's own template name, so leave `vesting_template_id` unset rather than inferring it from
prose like "vests monthly over four years" — that is the fuzzy match this phase forbids, done by
hand. Empty text means a scanned image: the parser exits 2 saying so; route the admin to OCR it
or type the values in, and never infer values from a filename.

---

## Phase 0.5 — Configure the issuance

Collect everything on **one** surface — every field, per stakeholder — so the user submits once
instead of answering a chain of questions, and so a single batch can carry genuinely different
terms for different people. This is the engine's `collectConfig`; the adapter from Step 1
decides what the surface is.

**Fetch the reference data first, and issue every call below in ONE assistant turn.** They have
no dependencies on each other; serial fetches here are pure latency.

- **Stakeholder lookup** — pass the people the prompt named as `stakeholder_names` on the
  **same** `issuance_init` call below. The server resolves them alongside the reference data in
  one round trip, and the result comes back as that payload's `stakeholders` section with the
  same shape as the standalone `cap_table:get:stakeholders` command. There is no separate
  stakeholder fetch here.
  **If the prompt named nobody, pass no names at all**: there is nobody to resolve yet, and
  [Phase 1](#phase-1--resolve-each-row--reconcile-share-classes) resolves whatever names the
  user types into the form.

  > **Never put two people in one `search=`.** `search` AND-s its whitespace-separated terms, so
  > it matches **one person only** — `search="Jane Doe"` works, `search="Jane Doe Bob Smith"`
  > asks for a single human matching all four terms and returns an empty list with a perfectly
  > healthy `200`. Commas don't help; they're stripped before the terms are AND-ed. Use
  > `stakeholder_names` (here) or `names=` (Phase 1) for several people — never a concatenated
  > `search`.
- **Reference data** — `cap_table:get:issuance_init` with the active `security_type`, plus
  `stakeholder_names` when the prompt named people. **One call** returns every section the
  surface and Phase 1 need, each with the same `{count, results}` shape as its standalone
  command:
  - *Option grant* — `vesting_templates`, `acceleration_templates`, `document_sets`,
    `valuations_409a`, `international_valuations`, `option_plans`.
  - *Certificate* — `certificate_share_classes`, `legends`, `vesting_templates`,
    `acceleration_templates` (cert vesting is opt-in but needs the same two lists once opted in).
  - *PIU* — `certificate_share_classes` (**read as the unit classes**: same endpoint and
    shape, so the section keeps that name and its fallback command), `option_plans`,
    `vesting_templates`, `acceleration_templates`, `document_sets`, `draft_set_init`. **No
    `legends`, no valuations** — a PIU has no legend and no exercise price. Threshold value
    types are not a section: they are the fixed pair `Unit` / `Overall`. `draft_set_init`
    carries the issuer's `thresholdNoun` (`"hurdle"` on the growth-shares preset) and `isLLC`.
  - *Both, only when `stakeholder_names` was passed* — `stakeholders`, already at `detail=full`,
    carrying `id`, `full_name`, `email`, `event_relationship`, and `kind` per person.

  Every section is fetched server-side in parallel, so adding `stakeholder_names` costs no extra
  wall-clock — it removes a round trip rather than adding one.

  **Partial failure is non-fatal.** A section that failed comes back `null` and is named in the
  top-level `errors` array (`[{section, message}]`); fall back to that section's individual
  `cap_table:get:<section>` command. An empty `errors` means full success — use the payload
  directly. This is the only fallback path; the rest of this file just says "from the
  `issuance_init` payload".

  **Read each section under its own name.** Never let one section's `count: 0` stand in for
  another's. Exactly one count may stop the flow — the [Account-setup
  gate](#account-setup-gate-option-grant-and-piu) below, on `document_sets.count` read under that
  name and no other. Every other count, zero included, never gates: the surface is built and
  opened regardless (Hard rule 10).

### Account-setup gate (option grant and PIU)

Runs once, immediately after the `issuance_init` payload is read — before FMV, jurisdiction,
plan, or any surface work — and skipped entirely for `certificate`. Both adapters run it.

**Read the count from its section under that exact name.** A real run aborted a valid
issuance by reading `acceleration_templates`' zero as `document_sets`'
([incidents.md § Reading server data wrong](references/incidents.md#reading-server-data-wrong)).
`count >= 1` always passes, and no other section's zero ever gates.

| `security_type` | Section | On `count == 0` |
|---|---|---|
| `option_grant` | `document_sets` | **Hard stop** — `document_set_id` is an `always` field on every grant row |
| `piu` | `certificate_share_classes` | **Hard stop** — `prefix` is an `always` field, so the batch could never issue |
| `piu` | `document_sets` | **Soft** — a PIU needs one only when the issuer's own properties demand it, and no MCP command exposes those. Build the surface *without* the Documents row and let `validate_drafts` decide (Hard rule 5) |
| `certificate` | — | skipped |

Hard stop, before building any surface:

> *"Your corporation doesn't have any option-grant document templates set up yet. Create one in the Carta app, then come back."*
> *"Your corporation doesn't have any unit classes set up yet. Create one in the Carta app, then come back."*

Soft, one line alongside the surface:

> *"This company has no profits-interest document templates. Carta will reject the issuance if your company requires a grant agreement — set one up in the Carta app if it does."*

**A section that failed to fetch** — `null`, absent, or not the documented `{count, results}`
shape — is a failed fetch, **not** `count: 0`. Run that section's own fallback command and
gate on its count; if the fallback errors too, surface its message verbatim and stop as a
fetch failure, never with a no-templates message.

**Why stopping here doesn't break Hard rule 10.** Rule 10 forbids asking for *collectible
fields* before the surface opens. These fields pick **among existing records** and cannot
create one, so for an `always` field zero records makes it unfillable from any surface and the
batch can never issue — an **account-setup blocker**, the same category as an unreachable Carta
MCP (Phase 0 Step 3), resolved in the Carta app rather than on this surface. For a
*conditional* field (PIU document sets) the blocker isn't certain, which is why that one is
soft.

**The gate reads only the sections in the table above.** It is not a "stop on any empty
section" rule and must not be read as one.

### PIU: check issuer eligibility (before building the surface)

Profits interests belong to LLCs and partnerships. **Nothing server-side rejects a PIU on a
C-corp** — not the draft-set views, not the validators — so this check exists only here. Read
`draft_set_init.isLLC` from the `issuance_init` payload:

- **`true`** → continue.
- **`false`** → warn once with `AskUserQuestion` before collecting anything: *"\<Company\> isn't
  set up as an LLC or partnership on Carta, and profits interests are normally issued by one.
  Continue anyway, or switch to certificates?"* Continuing is the admin's call; the server will
  accept it either way.
- **absent or `null`** (the section failed, or the field is missing) → treat as **unknown, not
  as `false`**. Continue without the warning — never block on a failed fetch.

### Option grant: resolve the FMV and the jurisdiction (before building the surface)

**Skipped entirely for `certificate` and `piu`** — neither has an exercise

…(truncated)
