plugins/lisa/skills/lisa-notion-write-prd/SKILL.md
Creates or idempotently updates a PRD as a page in the configured Notion PRD database, setting the lifecycle Status property to the draft value by default (or the ready value when initial_role is ready so lisa-notion-prd-intake auto-claims it). The Notion PRD-source writer behind lisa-prd-source-write. Dedupes by a stable marker embedded in the page (matched by marker, never by title). All Notion access goes through lisa-notion-access — never call the Notion API or MCP directly.
npx skillsauth add codyswanngt/lisa lisa-notion-write-prdInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Create (or update) a PRD page in the configured Notion PRD database. Invoked by
lisa-prd-source-write when source = notion; do not call directly from a vendor-neutral caller.
All Notion operations go through lisa-notion-access (the access chokepoint) — never curl the
Notion API or call a mcp__*notion* tool yourself.
$ARGUMENTS carries the lisa-prd-source-write spec: title, body (full PRD markdown),
initial_role (draft | ready, default draft), dedupe_key, marker, optional source_ref.
read_g() { local lv gv; lv=$(jq -r "$1 // empty" .lisa.config.local.json 2>/dev/null); gv=$(jq -r "$1 // empty" .lisa.config.json 2>/dev/null); echo "${lv:-${gv:-$2}}"; }
PRD_DB=$(read_g '.notion.prdDatabaseId' '')
[ -z "$PRD_DB" ] && { echo "Error: notion.prdDatabaseId not set in .lisa.config.json."; exit 1; }
STATUS_PROP=$(read_g '.notion.statusProperty' 'Status')
# Resolve the FULL PRD Status vocabulary from config (never hard-code) so the past-ready check is
# correct even when a project renamed any Status value.
DRAFT=$(read_g '.notion.values.draft' 'Draft')
READY=$(read_g '.notion.values.ready' 'Ready')
IN_REVIEW=$(read_g '.notion.values.in_review' 'In Review')
BLOCKED=$(read_g '.notion.values.blocked' 'Blocked')
TICKETED=$(read_g '.notion.values.ticketed' 'Ticketed')
SHIPPED=$(read_g '.notion.values.shipped' 'Shipped')
VERIFIED=$(read_g '.notion.values.verified' 'Verified')
# "Progressed past ready" set (never down-rank): the resolved in_review/blocked/ticketed/shipped/verified.
PROGRESSED=("$IN_REVIEW" "$BLOCKED" "$TICKETED" "$SHIPPED" "$VERIFIED")
Resolve the target Status value from initial_role: ready → $READY, otherwise $DRAFT.
The marker is embedded in the page (as the first body block). Find an existing PRD page in the DB
carrying it — match the marker, never the title:
lisa-notion-access operation: search query: "<marker>" (Notion indexes page content).$PRD_DB. If source_ref was passed, target that page
directly and skip the search.Markdown → Notion blocks (conversion boundary). Convert the PRD markdown to Notion block objects:
#/##/### → heading_1/2/3, paragraphs → paragraph, -/* → bulleted_list_item, 1. →
numbered_list_item, fenced code → code. The Notion API caps a single request at 100 blocks
and ~2000 characters of rich text per block: split long paragraphs across blocks, and if the PRD
exceeds 100 blocks, create the page with the first ≤100 blocks then add the remainder with batched
operation: append-blocks calls (≤100 each). When the MCP substrate is active, create-page may
accept the markdown content directly (it performs this conversion) — prefer that; the explicit block
conversion is the curl-substrate path.
Marker + usage-ledger preservation (both paths). The page must always carry exactly one
marker. On CREATE the marker is the first body block; on UPDATE never remove it. Never write a markerless body. Never write a markerless page. If the existing page content already contains the canonical managed ## Lisa Usage section, preserve that section when regenerating the page body unless the caller intentionally supplied an updated canonical section; use the shared lisa-usage-accounting serializer/merge path rather than freehand block edits to ledger rows.
CREATE:
<!-- $MARKER -->), then the converted PRD blocks.lisa-notion-access operation: create-page with:
{ "parent_database_id": "<PRD_DB>",
"properties": { "<title-prop>": { "title": [{ "text": { "content": "<TITLE>" } }] },
"<STATUS_PROP>": { "status": { "name": "<ROLE_VALUE>" } } },
"children": [ <marker block>, <PRD body blocks> ] }
Use the DB's actual title property name (read it via operation: read-database id: <PRD_DB> if
unknown) and the correct property type for $STATUS_PROP (status vs select).UPDATE (existing page or source_ref):
lisa-notion-access operation: write-page payload: { "id": "<page-id>", "properties": { "<STATUS_PROP>": { "status": { "name": "<ROLE_VALUE>" } } } } — unless the page's current Status is in the resolved ${PROGRESSED[@]} set (already past ready), in which case leave the Status and report reused (already past ready).operation: archive-page is page-level, so for blocks delete via the blocks API through
notion-access or, where block deletion isn't available, replace their text in place) and
operation: append-blocks the regenerated blocks. Do not duplicate the whole spec as a dated
note, and never drop the marker or an existing managed ## Lisa Usage section.ref: "<notion-page-id>"
url: "<page url>"
role: draft | ready # (or the page's current Status role when reused past ready)
marker: "<MARKER>"
outcome: created | reused
The PRD body's requirements MUST conform to prd-definition-of-ready: identified atoms (R1, R2, …), one behavior each in an EARS-pattern shape, each with a measurable fit criterion, plus the non-functional checklist. This governs factory-authored bodies; human-authored PRDs are validated at intake instead (*-to-tracker Phase 1.45).
All access via lisa-notion-access; never touch the Notion API/MCP directly.
Match dedupe by marker, never by title.
Preserve an existing canonical ## Lisa Usage section on update; never append a second usage
section or silently drop ledger rows.
Never down-rank a PRD whose Status is already past ready.
Resolve the Status vocabulary from config (notion.statusProperty, notion.values.*) — never
hardcode value names.
development
Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents (Claude Code, Codex, Cursor, OpenCode, Antigravity, Copilot) are already installed, asks which credential manager the machine uses (Bitwarden, 1Password, Doppler, Vault, AWS, or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent, headless by default, and emits a Dockerfile for a spin-up/spin-down environment. Run it on a new machine, in a container, or before cloning anything.
tools
Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv.
tools
Bring a developer's machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked. Same manifest, same pins, same installers as lisa-setup-remote-env; what differs is consent and that the pin is a floor rather than an equality. Run it on a fresh checkout, after a manifest change, or when a tool fails at the moment of use.
tools
Route one unit of work to a remote execution surface. Reads the executionEnv parameter (local by default, codex-cloud or claude-web today), verifies the environment is provisioned and bound to this repository, submits a thin skill invocation, records the task identifier to .lisa/remote-dispatch.json, and exits without polling. Routing only — the remote runs the identical skill from the identical repository. Composable and inline: other skills invoke it via the Skill tool rather than users calling it directly.