---
name: send-to-tickitoff
version: 2.0.0
description: Convert a markdown task list into a shareable tickitoff.app checklist link. Use when the user wants to send, share, or turn a to-do list, checklist, or set of tasks into a tickitoff link.
compatibility: Requires Node.js 18 or newer (the scripts use built-in fetch and AbortSignal.timeout). The scripts are dependency-free — no npm install. Network access is used for live-link validation and staleness checks; pass --no-live to skip both. Environments without Node or without network have documented fallbacks (see "If you cannot run these scripts").
allowed-tools: Bash(node *)
metadata:
  source: https://tickitoff.app/.well-known/agent-skills/send-to-tickitoff/SKILL.md
  discovery: https://tickitoff.app/.well-known/agent-skills/index.json
---

# Send to Tickitoff

Convert a markdown task list into a shareable `https://tickitoff.app/l?l=...`
link. The recipient opens it as a checklist they can tick off — no account and
no install required.

No Node, or a sandbox that cannot reach tickitoff.app? Skip the scripts: one
`GET https://tickitoff.app/api/encode?md=<url-encoded markdown>` mints the
link server-side — see "If you cannot run these scripts" below before doing
anything else.

## Getting this skill

If you are reading this over HTTP, the complete skill lives at
`https://tickitoff.app/.well-known/agent-skills/`:

- **One fetch:** `send-to-tickitoff.tar.gz` contains every file below at the
  archive root; its sha256 digest is in `index.json`.
- **Loose files** (served under `send-to-tickitoff/`): `SKILL.md`,
  `scripts/encode-list.cjs`, `scripts/content-lint.cjs`, `scripts/lz-string.cjs`,
  `scripts/size-bands.cjs`, `scripts/validate-link.cjs`,
  `references/format-contract.md`, `references/contract-fixture.json`. The
  encoder and validator require their `scripts/` siblings; fetch all of
  `scripts/` if you are not taking the archive.
- **Per-file integrity:** `send-to-tickitoff.manifest.json` carries a sha256
  digest for every loose file — verify downloads against it before executing
  anything.

## Updating this skill

Your copy of this skill can go stale: the app keeps shipping, and a stale copy
eventually encodes payloads the app rejects, or gives guidance the reader no
longer matches. The validator checks for this on every live run and tells you
which case you are in:

- `Skill schema version is accepted by the app` **FAIL** — your links are
  rejected. Stop and update before re-encoding.
- `Skill schema version is current` **WARN** — your links still work, but the
  skill is behind the app's current wire version.
- `Skill copy freshness` **WARN** — your files differ from what the app
  currently publishes (this catches doc and behavior changes that do not bump
  the schema).

To update:

1. Fetch `https://tickitoff.app/.well-known/agent-skills/index.json` and read
   the `send-to-tickitoff` entry (its `version` field says what is current).
2. Download the `send-to-tickitoff.tar.gz` archive it points at and verify the
   sha256 digest from the index **before executing anything**.
3. Extract to a fresh directory (the files sit at the archive root) and re-run
   the encode and validate steps from there.
4. If you run a vendored copy (skill directory checked into a repo), tell the
   user it is stale so they can update the vendored source — do not silently
   patch it.

## If you cannot run these scripts

Work down this ladder. Never compute the payload compression by hand — mint
the link server-side or fall back to markdown.

1. **You can fetch URLs** (every hosted assistant's chat-level URL tool works
   even when the code sandbox has no network):

   ```
   GET https://tickitoff.app/api/encode?md=<url-encoded markdown checklist>
   ```

   Or POST the raw markdown as the request body, which fits a checklist too
   long for a URL — send `content-type: text/markdown` (`application/json`
   also works; the body is still raw markdown, not JSON). Form content-types
   and `text/plain` are rejected before the request reaches the endpoint,
   with `Cross-site POST form submissions are forbidden` — that error means
   the header, not the markdown.

   The JSON response carries `url` (the share link), `markdownUrl` (its
   `/l.md` twin, for verification), and `band` (the share-reliability verdict
   from the same classifier these scripts use). The endpoint applies the
   **Mapping rules** below: one `#` H1 becomes the checklist title, `##`
   headings become sections, `- ` markers become items, and descriptions and
   notes map exactly as they would if you hand-mapped the document to JSON —
   so minting server-side and hand-mapping land on the same checklist. The
   one thing it cannot do is derive a title for a document that has no H1;
   supply one. It stores nothing. Verify by fetching `markdownUrl` and
   comparing against your input.

2. **You can run code but not fetch URLs**: `npm install lz-string@1.5.0` —
   the vendored `scripts/lz-string.cjs` is that exact npm package file, so
   the registry copy encodes identically. Build the v4 envelope per
   `references/format-contract.md` — object key order matters, and the `k`
   checksum is mandatory: a v4 payload without a matching one is refused by
   `/l.md` and `/api/oembed` and flagged to readers. Then:

   ```js
   const token = LZString.compressToEncodedURIComponent(JSON.stringify(envelope));
   const url = 'https://tickitoff.app/l?l=' + encodeURIComponent(token);
   ```

3. **Neither**: do not guess or invent a link. Emit the checklist as plain
   markdown instead — one `#` H1 as the checklist title, `##` headings for
   sections, `- ` list markers for items — and tell the user to paste it
   into any item title at `https://tickitoff.app/new` (pasting markdown into
   an item imports the whole checklist), then press Share.

## Input

Use the task list passed as the argument. If none was given, use the most
recent task list in the conversation. If no list is available, ask the user for
one.

The recommended markdown shape:

```markdown
# Weekend trip prep

Prose under a section heading becomes that section's description.

Quick things before the structured prep:

- Book the rental car

## Documents

Bring the essentials.

- Pack chargers — phone, laptop, and camera batteries
- Check passport expiry
  Must be valid for 6 months past the return date.

## Lodging

- Confirm the hotel reservation

---

Anything after the final rule becomes checklist notes.
```

Users will not always follow this exactly. Accept any reasonable checklist and map
it with the rules below.

## Mapping rules

Build a JSON object with exactly this shape:

```json
{
  "title": "string",
  "items": [{ "title": "string", "description": "string" }],
  "sections": [
    {
      "title": "string",
      "description": "string",
      "items": [{ "title": "string", "description": "string" }]
    }
  ],
  "notes": "string"
}
```

- **title** — if the document's only H1 is the leading line, use that H1 as the
  checklist title. If there is no title heading, or if multiple H1s are section
  headings, derive a short title from the content or ask the user.
- **items** — ungrouped tasks before the first section heading. Keep them before
  every section; this ordering is part of the wire contract.
- **sections** — optional. Any heading level (`#`, `##`, `###`, and so on)
  starts a section unless it is the sole leading H1 used as the checklist title.
  Multiple H1s with no separate title line are sections, not competing
  checklist titles.
- **section description** — prose between a section heading and that section's
  first checkbox or task line. Use `""` if there is none. A parent heading
  immediately followed by a child heading still becomes a titled section with no
  items; keep it.
- **leading prose before sections** — in a document that also has sections,
  prose between the checklist title and the first task line maps to a titleless
  leading section: `{ "title": "", "description": "...", "items": [...] }`.
  Put the leading tasks in that same section, not top-level `"items"`, so the
  intro renders as a headerless block. Those tasks are no longer ungrouped on
  the wire; ungrouped-on-the-wire only exists before the first section.
- **notes** — prose after the final `---`. That prose is notes, not the last
  section's description. Use `""` if there is none. For a sectionless checklist,
  prose under the sole title before the first task can also be notes.
- **items inside sections** — one entry per top-level bullet (`-`, `*`, `+`) or
  numbered item under that heading. The bullet text is the task `title`.
- **description** — a task's optional detail. Take it from text after a
  ` — `, ` – `, or `: ` separator on the bullet line, or from indented lines
  beneath the bullet. Use `""` when there is none.
- Strip `- [ ]` / `- [x]` checkbox markers — shared checklists always start
  unchecked.
- Fold nested sub-bullets into the parent task's `description`; Tickitoff has
  no sub-tasks. Join the folded points with `; ` or ` · ` — inside a title or
  description a line break collapses to a space (see Formatting).
- **Keep the user's inline formatting.** All four fields render an inline
  markdown subset (see Formatting), so preserve `**bold**`, `` `code` ``,
  links, and the like rather than stripping them. Block markdown (headings,
  list syntax, blockquotes, tables) is not supported — flatten it to inline
  text or plain prose.
- Drop any task whose title is empty.
- Do not invent `id` fields — the encoder assigns them.
- You may omit `"sections"` for a flat checklist. The encoder then emits a v4
  payload with `"sections": []`.

## Formatting (inline markdown)

The reader renders a small **inline** markdown subset in every text field —
the checklist title, notes, section titles, section descriptions, item titles,
and item descriptions.
Preserve the user's inline formatting; it renders. Everything not in the table
below is shown as literal text.

| Syntax | Renders as |
| --- | --- |
| `**bold**` or `__bold__` | bold |
| `*italic*` or `_italic_` | italic |
| `~~struck~~` | strikethrough |
| `` `code` `` | inline monospace code |
| `[label](https://example.com)` | a link |

Rules and gotchas:

- **Links** accept only `http://`, `https://`, and `mailto:` URLs. Any other
  scheme — or a malformed URL — renders as its literal source text instead of a
  link. A bare URL is **not** auto-linked; wrap it: `[text](https://…)`. The
  reader appends the destination host in muted parentheses unless the link text
  already shows it, and opens links in a new tab.
- **`_` only formats at word boundaries**, so `file_name` and `snake_case` stay
  literal. `*` formats mid-word too (`un*believ*able`).
- **Inline only.** Once text is inside a field, bullet/numbered syntax,
  blockquotes (`>`), tables, images, and fenced code blocks are not supported —
  those markers render literally. This is why sub-bullets are folded into
  description text, not left as markdown bullets.
- **No raw HTML** — tags render as literal text, never as markup.
- **Escaping.** To show one of `` \ ` * _ ~ [ ] ( ) `` literally, prefix it with
  a backslash (`\*not italic\*`, `2 \* 3 = 6`). Only those characters are
  special; `#`, `-`, `>`, and `.` need no escaping.
- **Line breaks survive only in notes.** Inside a title or description a newline
  collapses to a space, so join folded sub-points with a separator, not a line
  break.

You do not encode markdown specially — it is just text in the fields. These
rules tell you what will render and what to escape so the result reads as the
user intends.

## Garbled text

A checklist can reach a reader as fluent prose with a scatter of impossible
words in it — `unoeaock`, `ceaoud`, `feaow` — and links whose hosts do not
exist. Two different hops produce that, and both look clean from the inside:

- **Before the encoder** — text mis-spelled at the source, a lossy
  transcription, a bad paste. It compresses, decodes, and round-trips
  perfectly, because it is exactly what you handed over.
- **After the encoder** — a URL that was retyped or reconstructed instead of
  copied. lz-string does not fail loudly on a damaged payload: change a few
  characters mid-token and it still decompresses to a structurally valid
  checklist of nearly the right length, with localized letter damage that reads
  as typos. Worse, LZ back-references reuse the damage, so one wrong character
  can misspell the same word every time it appears. Since schema v4 the payload
  carries a checksum over its own content, so this case is caught outright:
  the validator's `Payload integrity` check fails, `/l.md` and `/api/oembed`
  refuse the link, and readers see a warning banner. Never hand over a link
  whose integrity check failed — re-encode from the source instead.

Assume you are the last line of defence, especially for text you wrote yourself
rather than took from the user, and for any URL that did not travel as bytes
(see Delivering the link).

Two checks approximate this for you, both advisory:

- The encoder and the validator flag **words that look garbled** — three vowels
  in a row is rare in English — and **two link hosts that are one typo apart**,
  which is what a mangled hostname looks like next to its intact twin.
- The validator (network only) resolves every link host and reports
  `Link destinations resolve`. A host that does not exist in DNS is a broken
  link, whatever the prose around it says.

Neither check fails the link and neither is exhaustive. When one fires,
proofread the flagged field against your source and re-encode; do not hand over
a checklist whose links you have not read.

## Size and share reliability

The 2048-character payload size is a **recommendation, not a hard limit.** The
encoder always returns a working link, and the reader opens a checklist of any
size — a large payload only makes the *URL* harder to send through some
channels. The encoder (on stderr) and the validator classify the encoded length
into one of five bands:

| Encoded length | Band | Verdict | Likely to break |
| --- | --- | --- | --- |
| under 1024 | `compact` | Shares everywhere | nothing |
| 1024–2048 | `heavy-but-fits` | Shares almost everywhere | SMS, dense QR codes |
| 2049–4096 | `beyond-safe` | Some apps will break this link | SMS/QR, plain-text email, WhatsApp previews |
| 4097–8192 | `address-bar-fragile` | Risky, paste-and-click only | the address bar, many link previews/embeds |
| over 8192 | `risky` | Most apps will break this link | nearly everything but a clicked link |

How to act on it:

- The encoder **never fails on size** — it prints the link plus the band
  verdict.
- The validator reports size as a **warning, never a failure** for anything past
  `compact`; a large link still validates as long as it round-trips and the live
  server accepts it.
- When the band is past `compact`, relay the verdict in plain language as you
  hand over the link, so the user can choose their channel. Offer to shorten
  descriptions, split the checklist, or front it with a shortlink if they need a
  more robust channel (SMS, a QR code). Do not withhold the link.

(These bands mirror `src/lib/limits.ts`; the skill's copy lives in
`scripts/size-bands.cjs`.)

## Steps

Script paths below are relative to this skill's directory — `$SKILL_DIR` stands
for wherever this SKILL.md lives. Substitute what your environment provides:
some harnesses expose it as a variable (Claude Code sets `$CLAUDE_SKILL_DIR`),
otherwise use the path you installed or downloaded the skill into.

1. Map the user's checklist into the JSON object above.
2. Write that JSON to a temp file, e.g. `/tmp/tickitoff-list.json`.
3. Run the encoder:

   ```bash
   node "$SKILL_DIR/scripts/encode-list.cjs" /tmp/tickitoff-list.json
   ```

   It prints the full share URL on stdout. On stderr it prints a content
   summary (task count, description coverage, sections, notes), the size band
   with any share-reliability warning, and `Warning:` lines for anything that
   will not survive the trip: unknown JSON keys (their values are NOT encoded),
   dropped items, field text that renders dead in the reader (bare URLs,
   block markdown, unclosed markers — see Formatting), and text that looks
   garbled (see Garbled text). **Read the stderr output.** A warning means the
   checklist will open but will not read as intended; fix the JSON and
   re-encode. A misnamed field is reported by name, with the valid keys for
   that position. It never fails on size.
4. **Validate that URL before showing it to anyone.** Never hand over a link
   that has not passed validation.

   ```bash
   node "$SKILL_DIR/scripts/validate-link.cjs" "<url from step 3>"
   ```

   The validator runs every structural, payload, content, live-server, and
   staleness check it can and prints a per-check report ending in
   `RESULT: VALID` or `RESULT: INVALID`. It exits 0 when valid, 1 when broken.
   Size shows up as a `Share reliability` warning, not a failure — a large link
   still validates. Pass `--json` for a machine-readable report (same checks,
   same exit codes) if you parse output rather than read it.
5. If the result is `INVALID`, read the per-check feedback — it names the exact
   cause — then fix it and repeat steps 3–4:
   - A payload-shape or item check failed → correct the JSON in the temp file.
   - `URL is free of markdown wrapping` or `The "l" value has no literal "+"`
     failed → the URL was damaged after generation, not mis-encoded; resend the
     encoder output verbatim (see Delivering the link).
   - `Schema version`, `Live production round-trip`, or
     `Skill schema version is accepted by the app` failed → the skill and the
     deployed app have drifted; follow **Updating this skill** before retrying.
   - `Payload integrity` failed → the URL you validated is not the one the
     encoder produced. Do not repair it by hand and do not send it: re-encode
     from the source JSON, and move the result as bytes (see **Delivering the
     link**). If it fails again on a freshly encoded link, the skill's encoder
     and the app have drifted — follow **Updating this skill**.

   A `Share reliability` warning is **not** a failure — never withhold the link
   over size (see Size and share reliability). A `Content rendering`,
   `Link destinations resolve`, or `Descriptions present` warning means the link
   works but will not read as intended — fix the flagged fields and re-encode.
   `Link destinations resolve` names hosts that do not exist in DNS; treat every
   one of them as a typo in your own text until you have checked it against the
   source (see Garbled text). A staleness warning means
   update the skill when convenient (see Updating this skill). Stop after 3
   genuine-failure attempts and tell the user the skill needs maintenance,
   quoting the validator output. Do not hand out a link that failed a non-size
   check.
6. Once the result is `VALID` (warnings are fine), check that the validator's
   `URL fingerprint` line matches the `Fingerprint:` line from step 3 — if they
   differ you validated a different string than you minted — then give the user
   that URL per **Delivering the link** below, fingerprint included. If the size band is past `compact`, relay its
   plain-language verdict so they can pick a channel. Note that the whole
   checklist is encoded in the URL itself — there is no server copy, so anyone
   with the link can read it.

## Delivering the link

Every character of the URL is load-bearing — the payload is compressed and the
`%2B` escapes are part of it. The two delivery mistakes that actually corrupt
links are markdown wrapping and hand transcription, so:

- **Machine path only.** Move the URL by pipe, file, or clipboard from the
  encoder's stdout to its destination. Never retype it, never "clean up" its
  escaping — converting `%2B` to `+` corrupts the payload (a `+` in a query
  string decodes as a space).
- **If you redirected the encoder's stdout, you do not have the link.** A URL
  you saw truncated in a report, or saw in an earlier draft, or reconstructed
  from either, is not the link — and a reconstruction is not obviously wrong,
  it is a working URL for a *different* checklist that reads as typos (see
  Garbled text). When the complete URL never entered your context, hand over
  the file path and let the user open it; do not compose the URL yourself. The
  same rule covers every long high-entropy string: hashes, base64, tokens.
- **Relay the fingerprint.** The encoder prints
  `Fingerprint: <n> chars, sha256:<8 hex>` on stderr and the validator prints
  the same line for whatever URL it was handed. Quote it beside the link you
  deliver. It is the only thing that binds the link you validated to the link
  you shipped — every other check in this skill assumes they are the same
  string, and a retyped URL fails on the character count before anyone reads
  the hash.
- **No adjacent markdown punctuation.** Do not wrap the URL in `**bold**`,
  `[brackets]`, backticks-with-adjacent-text, or end a sentence with it —
  renderers glue punctuation onto the URL and break the copy. Put it on its own
  line, bare.
- **Fragile channels:** if the destination mangles long URLs (chat renderers,
  email clients), deliver it as a real hyperlink instead — an HTML page or
  artifact with an `<a href>` whose value is inserted programmatically from the
  encoder output.
- If a recipient reports "This link couldn't be opened", suspect transit damage
  first: run the validator on the URL exactly as the recipient received it; the
  transport checks name what happened to it.

## Errors

`encode-list.cjs` never fails on size — an oversized checklist still produces a
working link, with a band warning on stderr (see Size and share reliability). It
exits non-zero only for:

- **Invalid input** (exit 2) — e.g. missing title, no tasks, or an item using
  the removed v1 key `explanation` (renamed to `description` in v3; the error
  says exactly what to rename). Fix the JSON and retry.
- **Self-check failed** (exit 3) — the encoder could not round-trip its own
  output. Do not hand out the URL; report that the skill needs maintenance.

`validate-link.cjs` exits 0 (valid — possibly with warnings, including size,
content-rendering, and staleness warnings), 1 (broken link — read the report
and correct), or 2 (usage error — no URL was passed). It needs network for the
live round-trip, the schema-version negotiation, the link-host lookups, and the
skill-freshness check;
with no network it reports those as warnings/skips and still validates
everything offline. Pass `--no-live` to skip the network checks deliberately.

## Wire format

`references/format-contract.md` documents the exact URL encoding and how it
stays in sync with the Tickitoff app. Read it only if the encoder or validator
misbehaves, or the format needs to change.
