plugins/lisa-copilot/skills/jira-build-intake/SKILL.md
Symmetric counterpart to notion-prd-intake on the JIRA side. Scans a JIRA project (or JQL filter) for tickets in the configured `ready` status, claims the first eligible ticket by transitioning to the configured `claimed` status, runs the implementation/build flow via jira-agent, transitions to the configured `done` status on completion, then exits. Enforces the claim-time arm of the `leaf-only-lifecycle` rule: a parent/container with open child work (or a childless Epic) that still carries a stale build-ready status is skipped or safe-blocked with a lifecycle-repair comment, never claimed. The `ready` status is the human-flipped signal that a TODO ticket is truly ready for development — mirroring how Notion PRDs work product Draft → Ready → (us) In Review → Blocked|Ticketed.
npx skillsauth add codyswanngt/lisa jira-build-intakeInstall 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.
All Atlassian operations in this skill go through lisa:atlassian-access. Do not call MCP tools or acli directly.
$ARGUMENTS is one of:
SE) — scans that project for tickets in the configured ready status.project = SE AND component = "frontend" AND Status = Ready) — used as-is. The skill will not append a Status = <ready> clause if the JQL already names a status, so callers can intentionally widen.Run one build-intake cycle. The first eligible ready ticket is claimed, built via the lisa:jira-agent flow, transitioned to the configured done status (env-aware — see below), then the cycle exits. Remaining ready tickets stay queued for later scheduler invocations.
Status names are read from .lisa.config.json jira.workflow.*, falling back to defaults documented in the config-resolution rule. Bash pattern:
# Read role with default fallback. Local overrides global per-key.
read_role() {
local role="$1" default="$2"
local local_v global_v
local_v=$(jq -r ".jira.workflow.${role} // empty" .lisa.config.local.json 2>/dev/null)
global_v=$(jq -r ".jira.workflow.${role} // empty" .lisa.config.json 2>/dev/null)
echo "${local_v:-${global_v:-$default}}"
}
READY=$(read_role ready "Ready")
CLAIMED=$(read_role claimed "In Progress")
For env-keyed done, resolve the env first, then look up done[<env>]:
target_env=staging) wins.deploy.branches (reverse lookup: if base is staging, env is staging).done in config is a string (not a map), use it directly regardless of env.done is a map and env cannot be resolved, fail loudly — do not pick arbitrarily.# Resolve env, then DONE.
TARGET_ENV="${target_env:-}" # from caller args if supplied
if [ -z "$TARGET_ENV" ] && [ -n "$PR_BASE_BRANCH" ]; then
TARGET_ENV=$(jq -r --arg b "$PR_BASE_BRANCH" \
'.deploy.branches // {} | to_entries[] | select(.value == $b) | .key' \
.lisa.config.json 2>/dev/null | head -1)
fi
DONE_RAW=$(jq -r '.jira.workflow.done // empty' .lisa.config.json 2>/dev/null)
DONE_TYPE=$(jq -r '.jira.workflow.done | type' .lisa.config.json 2>/dev/null)
if [ "$DONE_TYPE" = "string" ]; then
DONE="$DONE_RAW"
elif [ "$DONE_TYPE" = "object" ]; then
[ -z "$TARGET_ENV" ] && { echo "ERROR: jira.workflow.done is env-keyed but env not resolvable"; exit 1; }
DONE=$(jq -r --arg e "$TARGET_ENV" '.jira.workflow.done[$e] // empty' .lisa.config.json)
[ -z "$DONE" ] && { echo "ERROR: jira.workflow.done has no entry for env '$TARGET_ENV'"; exit 1; }
else
# Default: env-keyed map matching legacy hardcoded names.
case "$TARGET_ENV" in
dev) DONE="On Dev" ;;
staging) DONE="On Stg" ;;
production) DONE="Done" ;;
*) echo "ERROR: cannot resolve done status without env"; exit 1 ;;
esac
fi
Run one build-intake cycle. The first eligible ticket in $READY is claimed by transitioning to $CLAIMED, built via the lisa:jira-agent flow, transitioned to $DONE on completion, then the cycle exits.
Do NOT ask the caller whether to proceed. Once invoked with a project key or JQL, run the cycle to completion — claim and dispatch the first eligible ticket through lisa:jira-agent, transition a successful build to $DONE, write the summary, and exit. The caller (a human or a cron) has already authorized the run by invoking the skill; re-prompting defeats the purpose of a background queue.
Specifically forbidden:
Blocked by lisa:jira-agent's pre-flight gate. The pre-flight Blocked outcome is a valid terminal state of the per-ticket lifecycle (owned by lisa:jira-agent), not a failure mode — surfacing those tickets to humans is success.The only legitimate reasons to stop early:
$CLAIMED or $DONE not reachable, or $READY status absent). Surface and exit."No tickets with Status=$READY. Nothing to do."The JIRA workflow has these statuses (configured per project — see Workflow resolution above for how role names map to actual workflow values):
TODO → ready → claimed → done(env-keyed) → On QA → archive
(PM/ (us claim) (us done; (downstream)
human) PR ready)
This skill ONLY transitions $READY → $CLAIMED on claim, and $CLAIMED → $DONE on completion. It never touches TODO, post-done statuses, or any blocked/closed states.
Pre-flight check: at start of each cycle, attempt the $CLAIMED and $DONE transitions against a sample ready ticket via lisa:atlassian-access operation: transition key: <K> to: "<status>" (in a probe / dry-run sense — or fetch transition metadata if the access skill exposes that). If the transitions are unreachable, stop and report the workflow misconfiguration to the caller — do not invent transitions.
$ARGUMENTS:
project = <KEY> AND Status = "$READY" ORDER BY priority DESC, created ASC.Status clause, append AND Status = "$READY".lisa:atlassian-access operation: list-sites (it enforces connection match against .lisa.config.json).Invoke lisa:atlassian-access operation: search-issues jql: "<JQL>". Capture each ticket's: key, summary, issue type, priority, assignee, parent (epic), labels, components.
If empty, report "No tickets with Status=$READY. Nothing to do." and exit. This is the common idle case.
A JIRA project can oversee multiple repos (frontend / backend / infrastructure). This skill claims only tickets for the repo it is running in. Run this gate before the leaf-only gate (3a) and the claim (3b), per the repo-scope-split rule's "Claim-time repo scoping" section (cite it by slug; do not restate its decision table).
config-resolution "Repo scoping" (.repo → .github.repo → git remote get-url origin basename). If unresolvable, stop and report — do not claim tickets you cannot scope.repo:<current> — a JIRA label, or a component equal to the repo name (accepted as an alias). Keep the Phase 2 scan broad (it must still see unlabeled tickets so they can be determined and stamped); this gate orders/filters the results.repo-scope-split):
repo:<other> (label or component) → skip (leave it ready for that repo's own intake); next candidate.repo:<name> via lisa:atlassian-access operation: write-ticket (add the label / set the component) so later cycles filter cheaply; re-apply with the now-known repo.repo-scope-split work-time procedure to break it into single-repo siblings, each created build-ready (build_ready: true) and stamped with its own repo:<name>; the current repo's sibling becomes a normal candidate."No ready tickets for repo <current>. Nothing to do.".Build intake claims only independently implementable leaf work units. This enforces the claim-time arm of the vendor-neutral leaf-only-lifecycle rule: a parent/container that still carries a stale build-ready status (e.g. Ready applied before this rule existed, or hand-applied to an Epic/Story) is never claimed — intake skips it or safe-blocks it with a clear lifecycle-repair message. It is the claim-time complement to the write-time labeling in lisa:jira-write-ticket and the validate-time S15 gate in lisa:jira-validate-ticket; all three cite the same rule so the classification never drifts. Never silently implement a container.
Run this gate before the claim transition, starting with the oldest/highest-priority ready candidate. Do NOT transition, comment "Claimed", or invoke lisa:jira-agent for a ticket that fails the gate.
Resolve container vs. leaf — structural first, then nominal. Per leaf-only-lifecycle the classification is structural: a ticket is a container if it has open child work, whatever its declared type; otherwise the issue type decides. Resolve child work using the same hierarchy lisa:jira-read-ticket uses — JIRA's native Epic → Story → Sub-task parentage (Epic link / parent field for Stories under an Epic, and the subtask relationship for Sub-tasks under a Story/Task). Issue links (blocks / is blocked by) express cross-item dependencies and are not parentage — do not count them as children.
Fetch the ticket's children via lisa:atlassian-access operation: search-issues with a JQL that resolves both subtasks and Epic-linked Stories, then count those still open (not in a resolved/Done status):
# Children of <TICKET>: native subtasks plus, for an Epic, its linked Stories.
# (parent = <TICKET>) covers Sub-tasks and child issues; ("Epic Link" = <TICKET>)
# covers Stories under an Epic on JIRA instances that expose the Epic Link field.
CHILDREN_JQL='(parent = "<TICKET>" OR "Epic Link" = "<TICKET>")'
# Count children whose status is NOT a resolved/terminal one. A parent whose
# children are all Done is no longer holding open work and rolls up via
# leaf-only-lifecycle's rollup, not here.
OPEN_CHILDREN_JQL="${CHILDREN_JQL} AND statusCategory != Done"
Invoke lisa:atlassian-access operation: search-issues jql: "<OPEN_CHILDREN_JQL>" and let OPEN_CHILDREN be the count of returned issues (0 if none). If the JQL cannot resolve the Epic Link field on this instance (older JIRA / team-managed projects expose parentage differently), fall back to the parentage lisa:jira-read-ticket derives and treat the ticket as a container if any derived child is open. Note "Epic Link unavailable — parentage derived" so the operator knows how children were resolved.
Classify and act (first match wins). The issue type comes from the ticket's issuetype field (Epic, Story, Spike, Bug, Task, Sub-task, Improvement):
| Condition | Class | Action |
|---|---|---|
| OPEN_CHILDREN > 0 (open child work, any type) | Container | Skip / safe-block — do NOT claim |
| no open children AND type = Epic | Childless Epic (pure rollup container) | Skip / safe-block — do NOT claim |
| no open children AND type ≠ Epic (Bug, Task, Sub-task, Improvement, Story, Spike, or no recognized type) | Leaf work unit | Proceed to 3b claim |
The childless-parent exception promotes every childless type except Epic to a claimable leaf: a childless Story is a directly shippable increment and a childless Spike is the investigation unit, so neither is stranded. Only a childless Epic stays unclaimed — an Epic is a pure rollup container by design, and a childless one is an incomplete decomposition or a mis-applied role, never an implementable unit.
Safe-block (default action for a flagged container). Leave the build-ready status in place (don't silently transition it away — that hides the lifecycle error), post a single lifecycle-repair comment, record the ticket under "Skipped (container)" in the summary, and end the cycle. Do NOT transition to $CLAIMED. Keep the comment idempotent — skip posting if an identical [claude-build-intake] lifecycle-repair comment already exists on the ticket, so a re-entrant cycle doesn't spam it.
Post via lisa:atlassian-access operation: comment key: <TICKET> body: "<message>" with:
[claude-build-intake] Not claimed: this ticket carries the build-ready status ($READY) but is a container with open child work (or a childless Epic), which violates the leaf-only-lifecycle rule. Build-ready (status:ready) is leaf-only per leaf-only-lifecycle — an agent claims and implements leaves, never a container. Repair: move $READY off this parent onto its leaf children (or, for a childless Epic, decompose it into leaf children or reclassify it to a leaf type). A parent's lifecycle state rolls up from its children and is never set to ready directly.
This gate never blocks a legitimate flat Task/Bug: those have no open children and a leaf type, so they fall straight through to the claim in 3b.
Transition the ticket from $READY to $CLAIMED by invoking lisa:atlassian-access operation: transition key: <TICKET> to: "$CLAIMED".
[claude-build-intake] comment via lisa:atlassian-access operation: comment key: <TICKET> body: "Claimed by Claude. Starting build."Status = $READY filter will not see this ticket again.If the transition fails (permission, missing transition, race), log under "Errors" in the cycle summary and skip this ticket. Do not invoke the build flow on a ticket you didn't successfully claim.
Invoke the lisa:jira-agent (existing per-ticket lifecycle agent) with the ticket key. lisa:jira-agent owns:
lisa:jira-read-ticket)lisa:jira-verify)lisa:ticket-triage)lisa:jira-synclisa:jira-evidenceWait for lisa:jira-agent to return. Capture its outcome:
done env status.lisa:jira-agent itself transitions the ticket to Blocked and reassigns to Reporter. This is correct and expected — let it stand. Record the outcome and move on.lisa:jira-agent posts findings and stops. The ticket stays in $CLAIMED. Surface to human; do not auto-transition. Record under "Errors" with reason "Triage found ambiguities — see comments on <ticket-key>".$CLAIMED for human investigation. Record under "Errors" with the exception summary.A done env status (On Dev, On Stg, or the terminal value) asserts that the code has actually reached that environment. Never set it for a PR that is merely open: auto-merge can be blocked indefinitely (a required rebase / BEHIND branch, failing checks, an unaddressed review), and the change may never land. Setting On Stg on an open PR makes a ticket claim a deploy that never happened. Transition only after confirming the PR merged.
If lisa:jira-agent returned Success:
gh pr view <pr> --json state,mergedAt,mergeStateStatus,url:
state == MERGED) → proceed to resolve and apply $DONE below. Where the env deploy is observable (a deploy workflow run / deployment status keyed to the merged-into branch via deploy.branches), confirm it did not fail before transitioning; a still-running deploy is treated like an open PR (leave in $CLAIMED for a later cycle), a failed deploy is recorded as an Error.mergeStateStatus), leave it in $CLAIMED, and stop. A later lisa:repair-intake cycle drives the open PR to merge — re-syncing a BEHIND branch so the already-enabled auto-merge can land, or surfacing a real blocker — and, once it is merged, applies this same env transition. Do not comment "Build complete" or file anything: the work is in-flight, not done.$CLAIMED for human investigation.$DONE for this ticket's PR base branch using the Workflow resolution algorithm above. If env can't be resolved and done is env-keyed, record an Error and skip this transition — never guess.$DONE is the true terminal done value per the leaf-only-lifecycle rule's Terminal native closure section:
jira.workflow.done is a string, that status is terminal.jira.workflow.done is an object, only the production/final environment value is terminal (default: Done). Intermediate env statuses such as On Dev and On Stg are not terminal and must remain unresolved / open.lisa:atlassian-access operation: transition key: <TICKET> to: "$DONE".$DONE is terminal, verify the resulting JIRA issue is natively closed/resolved: status category is Done, and resolution is set when the project's workflow requires one. If the transition screen requires an explicit resolution, use the configured default resolution if present; otherwise record an Error naming the missing workflow setup rather than silently landing in an unresolved Done-named status.[claude-build-intake] comment via lisa:atlassian-access operation: comment key: <TICKET> body: "Build complete. PR <URL> merged. Transitioned to $DONE." Include whether terminal native resolution was verified, already satisfied, skipped for an intermediate env, or blocked by workflow setup.For any non-Success outcome, do NOT transition. The ticket sits in $CLAIMED (or wherever lisa:jira-agent left it for the Blocked case) — the cycle's job is done; humans take it from there.
Stop immediately after the first claimed, skipped, blocked, held, or errored ticket. Later scheduler invocations process the remaining ready tickets.
## jira-build-intake summary
Query: <JQL or project key>
Cycle started: <ISO timestamp>
Cycle completed: <ISO timestamp>
Tickets processed: <n>
- $DONE (build complete, PR merged): <n>
- <ticket-key> <summary> → PR <URL>
- PR open — awaiting merge (left in $CLAIMED for repair-intake): <n>
- <ticket-key> <summary> → PR <URL> (mergeStateStatus: <state>)
- Skipped (container — leaf-only-lifecycle): <n>
- <ticket-key> <summary> — build-ready on a parent with open child work; lifecycle-repair comment posted
- Blocked (pre-flight verify failed): <n>
- <ticket-key> <summary> — see ticket comments
- Held (triage found ambiguities): <n>
- <ticket-key> <summary> — see ticket comments
- Errors: <n>
- <ticket-key> <summary> — <reason>
Total PRs opened: <n>
leaf-only-lifecycle rule's claim-time arm). The safe-block comment is idempotent — a re-entrant cycle does not re-post it.$CLAIMED set BEFORE lisa:jira-agent invocation — no double-pickup.$READY → $CLAIMED and $CLAIMED → $DONE, then verifies terminal native resolution when $DONE is the true terminal state per leaf-only-lifecycle. Every other status change is owned by lisa:jira-agent (which suggests transitions but only auto-transitions on the verify-FAIL path).$DONE, the resulting JIRA issue must be in a resolved / closed state (statusCategory = Done and resolution set when required). Intermediate env statuses stay unresolved / open.lisa:jira-build-intake cycles concurrently against overlapping queries — concurrent claims could race. The scheduling layer (when added) is responsible for serialization.$CLAIMED or $DONE aren't valid transitions in the project's workflow, stop and report rather than guessing alternative names.Reads atlassian.cloudId, jira.project, and jira.workflow.{ready,claimed,done} from .lisa.config.json (with .lisa.config.local.json overriding per key). The project key is also accepted as $ARGUMENTS for ad-hoc invocations.
Status role names default to:
ready → "Ready"claimed → "In Progress"done → env-keyed map { "dev": "On Dev", "staging": "On Stg", "production": "Done" }If a project uses different names (e.g. Open instead of TODO, In Development instead of In Progress, Code Review for terminal), override the relevant key in .lisa.config.json jira.workflow.*. The setup skills (/lisa:setup:jira) handle this interactively.
Per-invocation overrides via $ARGUMENTS (e.g. claim_status="In Development") are accepted as a secondary escape hatch but .lisa.config.json is the canonical source.
If a ready-equivalent status does not exist in the JIRA project's workflow, this skill cannot run. The remediation is to add it to the project workflow scheme — JIRA admin task, not something this skill can do.
| Field / variable | Default | Purpose |
|------------------|---------|---------|
| .lisa.config.json jira.project | (from $ARGUMENTS) | Project key for the default JQL |
| .lisa.config.json atlassian.cloudId | — | Atlassian Cloud site UUID (required) |
| .lisa.config.json jira.workflow.ready | Ready | The status that signals "human says this is buildable" |
| .lisa.config.json jira.workflow.claimed | In Progress | The intermediate status the agent sets on pickup |
| .lisa.config.json jira.workflow.done | env-keyed map (dev/staging/production) or string | The status set after a successful build; env-aware |
| .lisa.config.json deploy.branches | — | Reverse-lookup map for env inference from PR base branch |
leaf-only-lifecycle rule, never claim a container — a ticket with open child work, or a childless Epic — even if it carries the build-ready status. Skip or safe-block it (Phase 3a); never silently implement a container.$CLAIMED transition is the signature of cycle ownership.lisa:jira-agent to do build work directly. lisa:jira-agent owns the per-ticket lifecycle (read, verify, triage, route, sync, evidence). This skill is the dispatcher, not the builder.$DONE. Downstream statuses are owned by QA / product / a future verification-intake skill — not this one.On Dev, On Stg, or configured equivalents). Native resolution is terminal-only.lisa:jira-agent's pre-flight verify will catch it and transition to Blocked — don't try to fix the ticket from here. Pre-flight gating is lisa:jira-agent's job; running build work on a thin ticket produces broken work.lisa:jira-agent (status it doesn't claim, missing PR URL on success, etc.), record as Error and surface — never assume.$DONE resolution. If done is a map and env is ambiguous, fail loudly.development
Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally.
development
Guidelines for upgrading Expo SDK versions and fixing dependency issues
development
Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).
tools
`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app.