plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md
Symmetric counterpart to lisa-jira-build-intake on the Linear side. Scans a Linear team for Issues carrying the configured `ready` build label, claims the first eligible Issue by relabeling to the configured `claimed` label, runs the implementation/build flow via the linear-agent workflow in-session (culminating in lisa-implement), relabels to the configured `done` label 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 label is skipped or safe-blocked with a lifecycle-repair comment, never claimed. The `ready` label is the human-flipped signal that an Issue is truly ready for development — mirroring how Notion PRDs work Draft → Ready → (us) In Review → Blocked|Ticketed.
npx skillsauth add codyswanngt/lisa lisa-linear-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.
$ARGUMENTS is one of:
ENG) — scans that team for ready Issues.linear — falls back to linear.teamKey from .lisa.config.json.Run one build-intake cycle. The first eligible ready Issue is claimed, built via the linear-agent workflow run in-session (Phase 3c, culminating in lisa-implement), relabeled to the configured done label on completion, then the cycle exits. Remaining ready Issues stay queued for later scheduler invocations.
This skill is the destination of the lisa-tracker-build-intake shim when tracker = "linear".
Build-queue label names are read from .lisa.config.json linear.labels.build.*, 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 ".linear.labels.build.${role} // empty" .lisa.config.local.json 2>/dev/null)
global_v=$(jq -r ".linear.labels.build.${role} // empty" .lisa.config.json 2>/dev/null)
echo "${local_v:-${global_v:-$default}}"
}
READY=$(read_role ready "status:ready")
CLAIMED=$(read_role claimed "status:in-progress")
REVIEW=$(read_role review "status:code-review")
For env-keyed done, resolve the env first, then look up done[<env>]:
target_env=staging) wins.deploy.branches (reverse lookup).done is a string in config, use it directly regardless of env.done is a map and env cannot be resolved, fail loudly — do not pick arbitrarily.TARGET_ENV="${target_env:-}"
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_TYPE=$(jq -r '.linear.labels.build.done | type' .lisa.config.json 2>/dev/null)
if [ "$DONE_TYPE" = "string" ]; then
DONE=$(jq -r '.linear.labels.build.done' .lisa.config.json)
elif [ "$DONE_TYPE" = "object" ]; then
[ -z "$TARGET_ENV" ] && { echo "ERROR: linear.labels.build.done is env-keyed but env not resolvable"; exit 1; }
DONE=$(jq -r --arg e "$TARGET_ENV" '.linear.labels.build.done[$e] // empty' .lisa.config.json)
[ -z "$DONE" ] && { echo "ERROR: linear.labels.build.done has no entry for env '$TARGET_ENV'"; exit 1; }
else
case "$TARGET_ENV" in
dev) DONE="status:on-dev" ;;
staging) DONE="status:on-stg" ;;
production) DONE="status:done" ;;
*) echo "ERROR: cannot resolve done label without env"; exit 1 ;;
esac
fi
In prose below, the role names refer to the resolved labels: e.g. "the ready label" means whatever linear.labels.build.ready resolves to (default: status:ready).
Linear's per-team workflow state names vary (Todo / Backlog / Up Next / etc.). Labels are workspace-scoped or team-scoped and stable across teams, so we drive the build queue off labels rather than chasing renamed native states. The native state field is informational until terminal completion; at the true terminal done value, the leaf-only-lifecycle rule requires native closure by moving the Issue to a configured Done / Completed workflow state when one exists.
Reads linear.workspace, linear.teamKey, and linear.labels.build.* from .lisa.config.json (with .local override).
Do NOT ask the caller whether to proceed. Once invoked with a team key, run the cycle to completion — claim and dispatch the first eligible Issue through the in-session lifecycle (Phase 3c), 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.
Specifically forbidden:
status:blocked by the pre-flight gate. The pre-flight status:blocked outcome is a valid terminal state of the per-Issue lifecycle.The only legitimate reasons to stop early:
ready label does not exist on the team's labels). Surface and exit with an Adoption hint."No Linear Issues labeled $READY. Nothing to do."Linear build queue uses these issue-level labels:
ready → claimed → review → done(env-keyed) (downstream)
(human/PM) (us claim) (us PR ready) (us build done)
(Defaults: status:ready / status:in-progress / status:code-review / status:on-dev/status:on-stg/status:done.)
This skill ONLY transitions $READY → $CLAIMED on claim, and $CLAIMED → $DONE on completion. It never touches status:done-as-terminal, $REVIEW (owned by the lifecycle / lisa-linear-evidence), or status:blocked (owned by the pre-flight gate).
Pre-flight check: at start of each cycle, confirm $READY, $CLAIMED, and the relevant $DONE variants exist on the team via lisa-linear-access operation: list-issue-labels. If $READY is missing, stop and report adoption needed. The other labels can be created on demand.
$ARGUMENTS:
linear → fall back to linear.teamKey from config.lisa-linear-access operation: list-teams({query: <teamKey>}).Query: lisa-linear-access operation: list-issues({team: <teamId>, label: "$READY"}).
Capture each Issue's: identifier, title, type label, priority, assignee, project, labels, description summary.
No query-time repo pre-filter here (by design). Unlike
lisa-jira-build-intake, which narrows its JQL withAND (labels = "repo:<current>" OR labels IS EMPTY)(the query-time arm ofrepo-scope-split), the Linearlist_issueslabel filter is an AND-of-labels and cannot express "current-repo or unlabeled" in one query. Addingrepo:<current>to this query would strand unlabeled Issues the determine + stamp path must see. So the Linear scanner keeps this query broad and relies on the per-candidate 3a.0 gate below for repo scoping.
If empty, report "No Linear Issues labeled $READY. Nothing to do." and exit. Common idle case.
A Linear team can oversee multiple repos (frontend / backend / infrastructure). This skill claims only Issues 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.repo:<current> label. Keep the Phase 2 scan broad so unlabeled Issues are still seen, determined, and stamped.repo-scope-split):
repo:<other> → skip (leave it ready for that repo's own intake); next candidate.repo:<name> via lisa-linear-access operation: save-issue (resolve/create the label via list_issue_labels/create_issue_label) so later cycles filter cheaply; re-apply with the now-known repo.repo-scope-split work-time procedure 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 Issues 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 label (e.g. status:ready applied before this rule existed, or hand-applied to a Project-grouped parent Issue) 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-linear-write-issue and the validate-time S15 gate in lisa-linear-validate-issue; all three cite the same rule so the classification never drifts. Never silently implement a container.
Run this gate before the claim relabel, starting with the oldest/highest-priority ready candidate. Do NOT relabel, comment "Claimed", or dispatch the lifecycle for an Issue that fails the gate.
Resolve container vs. leaf — structural first, then nominal. Per leaf-only-lifecycle the classification is structural: an Issue is a container if it has open child work, whatever its declared type; otherwise the type label decides. Resolve child work using the same hierarchy lisa-linear-read-issue uses — Linear's native parentage: an Issue groups sub-issues via parentId, and a Project (the Epic equivalent) groups Issues via projectId. Relations (save_issue_relation — blocks / is blocked by) express dependencies and are not parentage — do not count them as children.
Fetch the Issue's sub-issues via lisa-linear-access operation: get-issue (which returns the children) or lisa-linear-access operation: list-issues({parentId: <issueId>}), then count those still open (Linear state.type not in the completed/canceled/duplicate set):
# Children of <issueId>: native sub-issues via parentId.
# Count children whose Linear state.type is NOT terminal ("completed" / "canceled" / "duplicate").
# "duplicate" is a first-class Linear state.type (distinct from "canceled"): an Issue marked
# as a duplicate is closed and must NOT be counted as open, or the parent never rolls up.
# A parent whose children are all terminal is no longer holding open work and
# rolls up via leaf-only-lifecycle's rollup, not here.
OPEN_CHILDREN = count(list_issues({parentId: <issueId>})
where state.type not in {"completed", "canceled", "duplicate"})
For a Project-level parent (an Issue that itself anchors a projectId grouping rather than a parentId tree), resolve membership the same way lisa-linear-read-issue does and treat the parent as a container if any grouped Issue is still open. If sub-issue resolution is unavailable, fall back to the parentage lisa-linear-read-issue derives and treat the Issue as a container if any derived child is open. Note "sub-issues unavailable — parentage derived" so the operator knows how children were resolved.
Classify and act (first match wins). The type comes from the Issue's type: label (type:Epic, type:Story, type:Spike, type:Bug, type:Task, type:Sub-task, type: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 (a Linear Project) | Childless Epic/Project (pure rollup container) | Skip / safe-block — do NOT claim |
| no open children AND type ≠ Epic (Bug, Task, Sub-task, Improvement, Story, Spike, or no type: label) | 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 (a Linear Project) stays unclaimed — it 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 label in place (don't silently strip it — that hides the lifecycle error), post a single lifecycle-repair comment, record the Issue under "Skipped (container)" in the summary, and end the cycle. Do NOT relabel to $CLAIMED. Keep the comment idempotent — skip posting if an identical [claude-build-intake] lifecycle-repair comment already exists on the Issue, so a re-entrant cycle doesn't spam it.
Post via lisa-linear-access operation: save-comment with:
[claude-build-intake] Not claimed: this Issue carries the build-ready label ($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.
Rejection detection runs first — before the relabel below. Per the vendor-neutral rejection-detection rule (cite the slug; do not restate its classification table), classify this Issue at the top of 3b, BEFORE the $READY → $CLAIMED relabel — after the relabel the current-lane signal is gone. Read the Issue's history via lisa-linear-access operation: history id: <ISSUE-ID>, keyed on status:* label history (Linear build lanes are label-driven; resolve addedLabelIds/removedLabelIds against list-issue-labels), and classify it rejection-reclaim | forward-only | never-left-ready | unknown (a rejection-reclaim is the configured $READY label re-added after a later-lane label). Label names come from .lisa.config.json, never hardcoded. A failing/absent history yields unknown and the claim proceeds — detection never blocks the build. Issues carrying a learning marker ([lisa-learning-drop] / [lisa-learning-pr] / [lisa-learning-upstream-handoff]) or the learning:needs-triage label are never rejection triggers (no learning-about-learning). Carry the classification into the relabel and lifecycle below.
On rejection-reclaim, reflect before re-implementing (per rejection-detection): read the rejection evidence through the access layer — the Issue comments posted after the backward transition (the QA rejection comment) via lisa-linear-access operation: list-comments and the review threads on the rejected PR — assemble ONE candidate learning (rule, why, provenance linking the rejection comment + rejected PR, evidence links, scope hint, triggering issue, fingerprint sll4-sha1(rule\ntriggering_issue)[:12]), and route it to the lisa-persist-learning skill. If that skill is absent, record the candidate via lisa-linear-access operation: save-comment as a comment carrying a visible prose line plus the marker (a bare marker renders as an empty bubble) — Recorded a candidate learning from this rejection (queued for the judgment gate): <one-line candidate rule>. then <!-- [lisa-rejection-candidate] key=<issue>-<transition-ts> --> — and proceed. Dedupe on <issue>-<backward-transition-timestamp> — a second re-claim produces no duplicate. Unreadable/absent evidence → no candidate, still implement.
Claim-time archaeology runs second — after rejection detection, still before the relabel below. Classify this Issue per the vendor-neutral claim-archaeology rule, with the rejection classification above as its input. All shared semantics — ancestry signals, classification, learning-loop exclusion, cost budget, candidate derivation, marker dedupe, and the never-block degrade — live in that one slug; change them there, never here. Linear wiring only: the native relations are already in the read bundle; text-similarity searches run through lisa-linear-access operation: list-issues filtered to recently-closed Issues; the fallback candidate comment is posted via lisa-linear-access operation: save-comment.
Update labels via lisa-linear-access operation: save-issue: remove $READY, add $CLAIMED. Resolve label IDs via list_issue_labels (create $CLAIMED if missing).
Assign to the authenticated user when the Issue is unassigned. A claim must be attributable. If the Issue has no assignee, set its assigneeId to the authenticated viewer (resolve the viewer's id via the Linear MCP identity — e.g. get_user for the current actor) through lisa-linear-access operation: save-issue. Leave an already-assigned Issue's assignee untouched — never reassign work that already has an owner.
Post a [claude-build-intake] comment via save_comment: "Claimed by Claude. Starting build."
This is the idempotency lock — a re-entrant cycle's label: $READY filter will not see this Issue again.
If the relabel fails (permission, race), record under "Errors" and skip. Do not invoke the build flow on an Issue you didn't successfully claim.
After the claim succeeds, run the per-Issue lifecycle defined by the linear-agent workflow in the current session — never by spawning linear-agent (or any named worker) via the Agent tool. The lifecycle culminates in a team-first flow (lisa-implement), and that flow can only create its agent team from the lead session: a spawned teammate cannot add named teammates (Claude teams are flat), so dispatching the build into a subagent strands lisa-implement without its team and collapses the build into a single inline worker. Concretely:
linear-agent.md defines them and with all of its gating behaviors intact:
lisa-linear-read-issue — the full Issue graph (mandatory; never ad-hoc reads)lisa-linear-verify — pre-flight quality gate, including the draft-then-block procedure on FAILlisa-ticket-triage — analytical triage gate (a BLOCKED verdict stops the cycle with findings posted)lisa-implement <ISSUE-ID> for Build / Fix / Improve / Investigate-Only (or lisa-plan for an Epic-equivalent) — passing the full context bundle from the read step. When 3b classified this Issue rejection-reclaim, the context bundle passed to lisa-implement MUST include the rejection evidence summary (what was rejected, the defect the QA comment named, the approach named as wrong) — reuse the evidence already read in 3b, do not fetch it twice — so the plan can address it per rejection-detection; absence of evidence never blocks. lisa-implement's own orchestration preamble then creates the per-item agent team (input-resolver, Roster Decision, specialist fanout) exactly as a direct invocation would.lisa-linear-sync, lisa-linear-evidence) happen at the milestones the linear-agent workflow defines, within the dispatched flow.If you are somehow running this skill as a spawned teammate inside an existing team (nested misrouting — Intake keeps this chain in the lead session), do NOT run the lifecycle inline and do NOT spawn named peers. Return this payload to the lead so the lead session can run this Phase 3c in-session:
{
"type": "delegation-request",
"phase": "linear-build-intake 3c",
"workItem": "<ISSUE-ID>",
"context": {
"claimedLabel": "$CLAIMED",
"doneResolution": "Resolve $DONE from the PR base branch per this skill's Workflow resolution section"
},
"onSuccess": "Confirm the returned PR is merged, then apply Phase 3d and Phase 3d.1",
"onBlockedOrError": "Leave the Issue where the lifecycle left it and record the surfaced outcome"
}
The lifecycle run returns one of the following outcomes; resume this scanner with it:
done env status.status:blocked and assigns to creator. Let it stand. Record and move on.lisa-ticket-triage returned DUPLICATE_ALREADY_FIXED with a canonical Issue reference and empirical base-branch evidence. Post the triage finding, ensure the native duplicates <canonical> relationship exists when Linear exposes it (otherwise leave an explicit relation/comment reference), apply the terminal $DONE label, move the native Issue to the configured canceled-as-duplicate or completed terminal state, and do not open a PR. If the canonical fix is merged but not yet on the production branch, the close comment must say the production error can recur until the canonical Issue promotes and that recurrence is tracked by the canonical Issue; do not reopen this duplicate for that recurrence.$CLAIMED. Surface to human; do not auto-transition. Record under "Errors".$CLAIMED. Record with exception summary.Run this only when the returned triage verdict is exactly DUPLICATE_ALREADY_FIXED.
duplicates <canonical> relationship exists when Linear exposes one; if this workspace cannot create that relationship, leave an explicit relation/comment reference and record the limitation in the summary.$DONE value exactly as in Phase 3d. For env-keyed workflows, duplicate closeout uses the production/final done label, not an intermediate status:on-dev/status:on-stg waypoint.$CLAIMED and adding terminal $DONE, then move the native Linear state to the configured canceled-as-duplicate state. If no duplicate/canceled state is configured, use the configured terminal completed state only when that is the project's duplicate-close convention; otherwise record setup as an Error rather than inventing a state.If the canonical fix is merged but not yet present on the production branch, append the production-promotion caveat to the close comment: the production error can recur until the canonical Issue promotes, and recurrence is tracked by the canonical Issue rather than by reopening this duplicate.
This path is distinct from BLOCKED: ambiguity, open blockers, and duplicate-of-open findings remain held for human action and must not be auto-closed.
Run this at the end of 3c, after the lifecycle outcome is recorded and before 3d. It keeps the decay pass safe: a genuinely useful learning that keeps applying stays fresh, while dead weight ages out.
Identify which learnings demonstrably applied. Resolve the learnings surface with resolveProjectLearningsFile and parse it with parseLearningsFile from @codyswann/lisa/learnings (never hardcode the path; a missing file skips this step silently). An entry counts as applied ONLY when its rule was explicitly cited or observably followed in this claim's plan, diff, or review responses — the plan quotes the rule or its id, or the diff does specifically what the rule mandates where the default behavior would have differed. Presence in context is NOT application: entries reach a session only through the contract's bounded projection, so counting "it was projected" would confirm every projected entry on every claim and defeat decay entirely. A run that produced no plan or diff has nothing to confirm.
Bump each applied entry exactly once via the surgical writer:
node -e 'import("@codyswann/lisa/learnings").then(async m => { const r = await m.confirmLearningEntry(process.cwd(), process.argv[1], new Date().toISOString().slice(0, 10)); console.log(JSON.stringify(r)); }).catch(error => { console.log(JSON.stringify({ status: "error", id: process.argv[1], message: String(error) })); })' <entry-id> || true
The invocation is failure-safe by construction: a rejected import or write resolves to a structured error result instead of a crash, and the trailing || true absorbs any remaining non-zero exit (missing node, unresolvable package). Record an error or non-zero outcome in the cycle summary and continue — the bump must never abort the lifecycle.
confirmLearningEntry advances ONLY last_confirmed — rule text, why, provenance, first_learned, and confidence are untouched — and is idempotent within a claim: a repeat same-date bump returns unchanged, so an entry that applied repeatedly during one claim is bumped once, not once per application.
Never block on it. A failed bump, an unwritable file, or a not-found result (the entry was pruned) is recorded under the cycle summary and the claim proceeds — shipping the Issue always outranks confirming a learning about it.
A done env state (status:on-dev, status: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. Relabeling an Issue status:on-stg on an open PR makes it claim a deploy that never happened. Transition only after confirming the PR merged.
If the lifecycle run 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 relabeling; a still-running deploy is treated like an open PR (leave at $CLAIMED), a failed deploy is recorded as an Error.mergeStateStatus), leave it at $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 merged, applies this same env transition. Do not comment "Build complete" or change the native state.$CLAIMED.$DONE for this issue'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:
linear.labels.build.done is a string, that string is terminal.linear.labels.build.done is an object, only the production/final environment value is terminal (default: status:done). Intermediate env values such as status:on-dev and status:on-stg are not terminal and must keep the native Issue open.lisa-linear-access operation: save-issue: remove $CLAIMED (or $REVIEW if lisa-linear-evidence already moved it forward), add $DONE.$DONE is terminal, move the native Linear Issue state to the configured Done / Completed state. Resolve that state from project configuration if present; otherwise inspect the team workflow for a terminal state with state.type = "completed" and a name such as Done or Completed. If no terminal state can be resolved, record an Error and leave the labels as the source of truth — do not invent a state name. Conversely, if $DONE is an intermediate env yet Linear already natively completed the Issue (a magic-word / branch-linkage front-run — Linear auto-completes on merge to any branch, per the asymmetry documented in git-submit-pr), reconcile it back to active via lisa-linear-sync Phase 4b so the native state mirrors the non-terminal env instead of a premature closure.[claude-build-intake] comment: "Build complete. PR <URL> merged. Transitioned to $DONE." Include whether native closure was applied, already satisfied, skipped for an intermediate env, or unavailable for setup reasons.For any non-Success outcome, do NOT transition. The Issue sits where the lifecycle left it — humans take it from there.
Run this only after a successful $DONE transition in 3d. This is the forward arm of the leaf-only-lifecycle rule's "rollup is evaluated whenever a child transitions" requirement: a leaf reaching $DONE may complete its parent Issue, which may in turn complete its Project. Without this step a fully-built parent stays open until the recovery lisa-repair-intake cron happens to run.
lisa-linear-read-issue uses — native parentage: a sub-issue's parentId, and Project membership via projectId for the Epic-equivalent. If the Issue has no parent, skip — nothing to roll up.lisa-linear-sync <ANCESTOR-ID> --rollup. That skill derives the ancestor's status:* from its children per leaf-only-lifecycle, applies it via lisa-linear-access operation: save-issue only when it differs (never status:ready), and moves the native Linear state to the configured Done / Completed state when the derived env is the terminal $DONE. It is idempotent and safe-defaults (suggests, does not guess) when the rolled state is ambiguous.--rollup reports no change. Record each rolled-up ancestor and its derived state in the summary.This does not re-implement the state machine — it delegates to lisa-linear-sync --rollup, the single rollup implementation lisa-repair-intake also uses, so the forward and recovery paths can never drift. Children closed outside this flow are not observed here; lisa-repair-intake remains the recovery net for those.
Stop immediately after the first claimed, skipped, blocked, held, or errored Issue. Later scheduler invocations process the remaining ready Issues.
## linear-build-intake summary
Team: <teamKey>
Cycle started: <ISO timestamp>
Cycle completed: <ISO timestamp>
Issues processed: <n>
- $DONE (build complete, PR merged): <n>
- <ID> <title> → PR <URL>
- PR open — awaiting merge (left at $CLAIMED for repair-intake): <n>
- <ID> <title> → PR <URL> (mergeStateStatus: <state>)
- Skipped (container — leaf-only-lifecycle): <n>
- <ID> <title> — build-ready on a parent with open child work; lifecycle-repair comment posted
- Duplicate already fixed (closed as duplicate): <n>
- <ID> <title> — duplicate of <canonical>; no PR opened
- status:blocked (pre-flight verify failed): <n>
- <ID> <title> — see Issue comments
- Held (triage found ambiguities): <n>
- <ID> <title> — see Issue comments
- Errors: <n>
- <ID> <title> — <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 the lifecycle dispatch — no double-pickup.$READY, $CLAIMED, $DONE, plus terminal-only native state completion required by leaf-only-lifecycle. Every other label change (and non-terminal native state change) is owned by the per-Issue lifecycle or lisa-linear-evidence.DUPLICATE_ALREADY_FIXED is the only triage outcome that may close a claimed Issue without a PR from this cycle. It must include a canonical Issue reference and empirical base-branch evidence, and it closes through the configured duplicate/canceled terminal path rather than as completed build work.$DONE label is applied, move the Linear Issue to a native completed state only when $DONE is the true terminal done value; intermediate env labels stay open / active.status:* label is present. Two simultaneously breaks the build queue.$DONE. If done is a map and env is ambiguous, fail loudly.Before this skill can run against a Linear team, the team must adopt the build-queue label convention. Using the defaults:
status:ready, status:in-progress, status:code-review, status:on-dev, status:done, status:blocked on the team (or workspace). If your project overrides any linear.labels.build.* role name in config, substitute the actual label names you configured.$READY label to Issues that are ready for development.$CLAIMED, $REVIEW, $DONE for Lisa — humans should not set them manually except to recover from an error.If the team hasn't adopted these labels, the first run exits with an adoption hint.
leaf-only-lifecycle rule, never claim a container — an Issue with open child work, or a childless Epic — even if it carries the build-ready label. Skip or safe-block it (Phase 3a); never silently implement a container.$CLAIMED transition is the signature of cycle ownership.linear-agent workflow culminating in lisa-implement) owns it. And never spawn that lifecycle as a subagent; run it in-session per Phase 3c so lisa-implement can create its agent team.$DONE. Downstream labels are owned by QA / product / a future verification-intake skill.status:on-dev, status:on-stg, or configured equivalents). Native completion happens only at the terminal done value.status:blocked — don't try to fix the Issue from here.$DONE resolution. If done is a map and env is ambiguous, fail loudly.tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.