Imported from CodySwannGT/lisa (
plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md). Install upstream withnpx skills add CodySwannGT/lisa --skill lisa-repair-intake. Copyright stays with the author.
Repair Intake: $ARGUMENTS
Run one batch-repair cycle against the queue identified by $ARGUMENTS, or by merged GitHub
config when the queue is omitted and a GitHub source/tracker default is resolvable. Where lisa-intake
scans the ready role and moves work forward, repair-intake scans the stuck and
close-out roles and moves work unstuck or fully closed:
- Stalled in-progress — an item left in an in-progress role (build
claimed, PRDin_review) whose processing cycle died. It is technically "being worked" but nothing is happening, so it sits ignored forever. (The vendor PRD intakes explicitly leave an errored PRD inin_review"for the human to investigate from there" — that orphan is exactly what this skill recovers.) For a stalled build, repair-intake first diagnoses why it stalled by inspecting its PRs and deploys. A PR that already merged is recovered by applying the env transition build-intake never got to (its merge gate left the itemclaimedwhen the merge landed after its agent returned) — no re-dispatch. A PR that is merely behind its base (BEHIND, no conflict) is re-synced in place withgh pr update-branchso the already-enabled auto-merge can finally land — a clean rebase needs no human, and leaving it stranded is the exact gap that lets an auto-merge PR sit unmerged forever. A true merge conflict is first given one bounded in-place re-dispatch to the build agent — whosedrive-pr-to-mergefix-mode loop resolves conflicts — because a conflict, unlike a failing external check, is fixable by re-running the build; only a conflict that survives that single attempt (or that the agent says needs design input) becomes a fix ticket. The genuinely non-resolvable blockers — failing checks / unaddressed CodeRabbit orCHANGES_REQUESTEDreview / a failed deploy — get a build-ready leaf fix ticket with the item moved toblocked(blocked by that ticket) instead of blindly re-dispatching the agent, which would just churn against them. - Recoverable blocked — an item in
blockedwhose blocker may now be gone. The blocker is one of three classes, and repair re-checks all of them, not just dependencies: (a) anis blocked bydependency has since closed; (b) a validation / quality-gate self-block — the item was bounced toblockedby its own pre-flightverify/validategate (missing Validation Journey, Sign-in Required, Acceptance Criteria, etc.) with no dependency at all, and a human has since edited the item to add what the gate demanded; or (c) clarifying questions answered / an ambiguity research can now settle. A self-block (b) is the common one missed by dependency-only re-checks: nothing else is blocking it, so re-running the same gate against its current content is the only way to know it is now passable. - Terminal-open drift — an item already carrying its true terminal lifecycle role (for
example GitHub
status:done) but still open/active in the provider's native state. The inverse also drifts on Linear: a leaf whose nativestatewas auto-completed by a magic-word / branch-linkage merge into a non-terminal env (Linear completes on merge to any branch, unlike GitHub's default-branch-scoped close) while its derivedstatus:*label is still intermediate — reconcile it back to active vialisa-linear-syncPhase 4b perleaf-only-lifecycle. - Rollup drift — a parent/container item (Epic, Story, PRD, Linear Project, or equivalent)
whose own lifecycle state does not match the roll-up of its children's states per
leaf-only-lifecycle. This covers the completed case (all children terminal → close the parent out) and the intermediate-env case (all children shipped to an env likeOn Stg, but the parent never advanced — including a parent left stranded in a status it should never carry). - Stale-
readycontainer — a parent/container (open child work, or a childless Epic) wrongly carrying the build-ready role. This is a leaf-only-invariant violation the build-intake claim gate deliberately leaves for a human; repair-intake reconciles it by rolling the parent up from its children (with an audit note), so a container never sits inreadyindefinitely. - Missing official ready-label drift — a GitHub issue that is missing every configured Lisa
lifecycle label. repair-intake classifies it as a PRD or build ticket and adds the configured
readylabel (prd-readyfor a PRD, buildstatus:readyfor a ticket) so normal intake can see it; if the later intake/implement gate finds the item incomplete, it moves the item toblocked. - Missing native child link drift — a GitHub parent (a
ticketed/other open non-product-owned PRD, or a build Epic/Story container) whose children are discoverable — from the generated-work section/comment for a PRD, or from body parentage (Parent: #<n>/Parent Epic: #<n>) for a build container resolved via the documented hierarchy fallback — but whose native sub-issue list is missing one or more of those children. This is the common shape when children were created by an external generator (e.g. Codex) or an older write path that recorded parentage only in prose and never calledaddSubIssue. repair-intake replays theprd-backlink/github-write-issuenative-linking contract and attaches the missing same-repo children idempotently, so rollup and the GitHub UI can rely on the native graph again.
This skill is the symmetric counterpart to lisa-intake. It reuses the same queue-detection,
the same agent-team orchestration, the same "don't ask, just run" confirmation policy, and the
same per-item surfaces the vendor intakes use (lisa:<source>-to-tracker dry-run for PRDs;
lisa:<tracker>-agent + the scanner's lifecycle transitions for build) — it differs in which
roles it scans and, for stalled/blocked work, that it skips the claim step (the item is already
claimed/blocked). Close-out candidates do not dispatch agents; they only reconcile terminal
lifecycle state with provider-native closure and rollup state.
Public contract
/lisa:repair-intake [<queue>] [intake_mode=prd|build|both] [build_queue=owner/repo] [stale_after=2h] [max_candidates=100] [force=true]
| Token | Meaning | Default |
|---|---|---|
<queue> |
Same queue identifier lisa-intake accepts (see Source dispatch). Optional only when merged config resolves a GitHub source/tracker and identity; otherwise required. |
merged GitHub default when resolvable |
intake_mode |
prd | build | both. Only meaningful for a GitHub org/repo (or bare github) that hosts both PRD and build label namespaces. both is unique to repair — a repair sweep usefully covers both lifecycles in one schedule. Absent → both when both namespaces exist, else whichever lifecycle exists. |
both for dual GitHub queues; otherwise infer |
build_queue |
GitHub build scan target used with literal github, especially intake_mode=both. Explicit owner/repo or URL for a build-only invocation wins over this token; otherwise this wins over github.queueRepo. It never redirects the PRD scan. |
github.queueRepo, then github.org/github.repo |
stale_after |
How long since the last state-changing transition into the in-progress role, or since the last human / PR-side forward-progress activity, before an in-progress item counts as stalled. Automation self-comments do not reset this clock. Accepts 24h, 90m, 2d, or 0 (treat any in-progress item as stalled — manual recovery, also the only way to resume work on a provider that exposes no reliable timestamp). Overrides config. |
2h |
max_candidates |
Cap on how many stuck/close-out candidates to enumerate and evaluate. Repair every materially actionable candidate within this bounded set, then stop. Overrides config. | 100 |
force |
true bypasses the loop-prevention backoff window (so a manual re-run re-attempts items even if their fingerprint is unchanged). It does not change the staleness rule — use stale_after=0 for that. |
false |
Confirmation policy
Do NOT ask the caller whether to proceed. Once invoked, resolve the explicit or config-backed queue and run the cycle to completion. The caller (a human at the CLI or a scheduled cron) has already authorized the run by invoking the skill; re-prompting defeats the purpose of a background repair sweep.
Specifically forbidden:
- Previewing projected scope (number of stuck items, projected re-dispatch count, write counts) and asking whether to continue.
- Offering A/B/C-style choices like "repair / skip / report-only" — the documented behavior IS the default.
- Pausing because many items are stuck, an item looks complex, or a repair is likely to land
the item back in
blocked. Returning an item toblockedwith a current, accurate note is a valid outcome of the repair lifecycle, not a failure. - Pausing because a re-dispatch looks expensive. The cost of one cycle is bounded by
max_candidatesand the actionable subset inside that cap; the cost of stalling a scheduled cron waiting on a human is unbounded.
The only legitimate reasons to stop early:
- Missing required input (no queue argument and no resolvable GitHub default, or missing project configuration). Surface the missing value and exit.
- The queue itself is misconfigured (Status property missing expected values, JIRA workflow can't reach required transitions). Surface and exit.
- No stuck/close-out candidates, or none actionable this cycle. Exit cleanly with the idle-case summary.
Orchestration: thin dispatcher (no team of its own)
Repair-intake follows the same orchestration contract as lisa-intake: it creates NO agent team and spawns NO named teammates. It is a bounded scanner/dispatcher — resolve the queue, evaluate staleness, pick the actionable candidates, and run each repair in the current session.
The reasoning is the same as Intake's: a repair that resumes stalled build work may culminate in a team-first lifecycle skill (lisa-implement, lisa-plan), and those skills can only create their agent team from the lead session — a spawned teammate cannot add named teammates (Claude teams are flat), so pushing the repair into a subagent strands the lifecycle skill without its team and collapses it into a single inline worker. Therefore:
- Scanning, staleness evaluation, and per-item repairs run inline in this session (Bash / MCP / vendor skills via the Skill tool —
lisa-<source>-to-trackerfor a PRD, the<tracker>-agentworkflow's gate skills for a build item). - Any repair that dispatches a lifecycle flow does so via the Skill tool in this same session — never an
Agentspawn — so the lifecycle skill's team-first preamble fires exactly as a direct invocation would. - The only permissible
Agentuse is a bounded anonymous helper (Agentwithnameomitted) for scan-side legwork whose result returns directly to this session.
Codex: the same contract applies — run the scan inline in the root session and invoke any lifecycle skill there so it can multi_agent_v1.spawn_agent its own team; do not spawn_agent the lifecycle flow itself. Other runtimes: apply the same rule through their equivalent delegation surface; if the runtime has no team/subagent tooling at all, the lifecycle skill's own no-team fallback handles it.
If a teammate inside an existing team somehow invokes this skill (it is a session entry point, not a nested flow), do not scan or repair from there: return a structured delegation-request to the team lead asking it to run the repair cycle in the lead session, and surface the misrouting.
Source dispatch
Detect the queue type from $ARGUMENTS using the exact same detection and disambiguation
rules as lisa-intake — read that skill's "Source dispatch" section for the authoritative
table; the detection is identical and only the per-item action changes (repair instead of
claim-and-advance). The essentials, inlined here so this skill is self-complete:
If $ARGUMENTS is... |
Queue / lifecycle | Source/tracker key | Candidates repaired |
|---|---|---|---|
| Notion database URL/ID | PRD (Notion) | source=notion | in_review, blocked, terminal/open PRDs, all-terminal generated-work rollups |
| Confluence space URL/key | PRD (Confluence) | source=confluence | in_review, blocked, terminal/open PRDs, all-terminal generated-work rollups |
| Confluence parent page URL/ID | PRD (Confluence, narrowed) | source=confluence | in_review, blocked, terminal/open PRDs, all-terminal generated-work rollups |
Linear workspace URL, team URL/key, or literal linear |
PRD (Linear) | source=linear | in_review, blocked, terminal/open PRDs, all-terminal generated-work rollups |
GitHub repo URL / org/repo (PRD namespace) |
PRD (GitHub) | source=github | in_review, blocked, terminal/open PRDs, missing PRD child links, all-terminal generated-work rollups |
GitHub repo URL / org/repo with tracker = github (build namespace) |
Build (GitHub) | tracker=github | claimed, blocked, terminal/open issues, parent rollups (intermediate-env + all-terminal), stale-ready containers |
GitHub repo URL / org/repo with an open issue missing configured lifecycle labels |
GitHub label normalization | per classified lifecycle | add configured prd.ready or build ready |
Literal github or omitted GitHub repo when merged config resolves a GitHub source/tracker and identity |
GitHub; PRD scans identity, build scans build_queue → merged github.queueRepo → identity; both runs those two lane-specific targets |
per lifecycle | per lifecycle above, plus GitHub ready-label normalization |
| JIRA project key or full JQL | Build (JIRA) | tracker=jira | claimed, blocked, terminal/closure verification, parent rollups (intermediate-env + all-terminal), stale-ready containers |
Disambiguation (same as lisa-intake): a notion.so/notion.site URL → Notion; an Atlassian
/wiki/spaces/<KEY> URL → Confluence (with /pages/<id> → parent-page narrowing); a
linear.app workspace/team URL or literal linear → Linear; a github.com URL / <org>/<repo>
token / literal github → GitHub; a bare token matching the JIRA project-key regex → JIRA
(else try Confluence space, then Linear team); a string with JQL operators → JQL. A single-item
URL is out of scope — this skill is batch-only; repair one item by hand via lisa-implement
(build) or by re-running lisa:<source>-to-tracker (PRD).
For GitHub intake_mode=build (a build-only invocation), an explicit URL or owner/repo wins;
otherwise resolve build_queue, then local/global github.queueRepo, falling back to
github.org/github.repo. For intake_mode=both, the primary <queue> is the PRD identity/source
repo and build_queue controls the build lane, falling back through github.queueRepo to identity.
A short queueRepo is normalized to github.org. Thus both is a bounded split scan: PRD candidates
at the primary identity queue, build candidates at the resolved build queue, combined into one
summary. Never apply both lifecycle namespaces to the umbrella repo merely because
github.queueRepo is set. Repository identity remains distinct and continues to drive
repo:<current> filtering, writes, and automation names. If neither an explicit queue nor a
resolvable merged GitHub default exists, stop with the missing-input error.
Role names for every vendor are resolved from .lisa.config.json per the config-resolution
rule — never hardcode status/label strings. The relevant repair roles:
| Lifecycle | Vendor | In-progress role key | Blocked role key | Terminal / rollup role key |
|---|---|---|---|---|
| Build | JIRA | jira.workflow.claimed (In Progress) |
jira.workflow.blocked (Blocked) |
env-resolved jira.workflow.done |
| Build | GitHub | github.labels.build.claimed (status:in-progress) |
github.labels.build.blocked (status:blocked) |
env-resolved github.labels.build.done (status:done) |
| Build | Linear | linear.workflow.claimed (In Progress) |
linear.workflow.blocked (Blocked) |
env-resolved linear.workflow.done (Done) |
| PRD | Notion | notion.values.in_review (In Review) |
notion.values.blocked (Blocked) |
notion.values.shipped (Shipped) |
| PRD | GitHub | github.labels.prd.in_review (prd-in-review) |
github.labels.prd.blocked (prd-blocked) |
github.labels.prd.shipped (prd-shipped) |
| PRD | Linear | linear.labels.prd.in_review (prd-in-review) |
linear.labels.prd.blocked (prd-blocked) |
linear.labels.prd.shipped (prd-shipped) |
| PRD | Confluence | confluence.parents.in_review (page id) |
confluence.parents.blocked (page id) |
confluence.parents.shipped (page id) |
In addition to the lifecycle roles above, the build lifecycle defines the human_needed marker — an additive label (jira.labels.human_needed / github.labels.build.human_needed / linear.labels.build.human_needed, default Human Needed / human-needed) that rides alongside blocked when the block needs human-only input no agent or retry can supply (see config-resolution "Build markers"). repair-intake's interaction with the marker is asymmetric and is the whole point of the distinction below:
- The blocks repair-intake itself writes are the auto-recoverable kind — it files a build-ready fix ticket and moves the item
blockedblocked by that ticket, expecting the next cycle to self-heal. Those are nothuman_needed; if such an item arrives carrying ahuman_neededmarker this skill applied on an earlier cycle, repair-intake clears it (the block is no longer waiting on a human). - Never remove a
human_neededmarker this skill did not apply. "Stale" is a judgment about the block's kind, not about who applied the marker or when — so without this rule an operator's deliberate hold, applied after correcting a wrong transition, is indistinguishable from a leftover the sweep is designed to clear, and gets swept. Establish provenance from the label event's actor (rejection-detectionAutomation-reversal memory reads the same surfaces); if provenance is not readable, leave the marker in place. Removing a human's hold is unrecoverable within the loop; leaving a stale one costs a cycle and is visible. - The marker is consulted before any repair transition, not only before Class C. Class C's hard stop is the strictest reading of it, but a marker that is honoured on one classification path and ignored on the other three is not a guard — and Class A, dependency clearing, is exactly the path an operator reverting a wrongly-cleared blocker is trying to protect. Match it robustly (hyphen/underscore, case-insensitive, label set and note prose) wherever it is read.
- The blocks the vendor agent writes when repair-intake re-dispatches it (its pre-flight gate) carry
human_neededalready — the agent owns that marker. repair-intake leaves it in place.
Resolve with the standard role-read pattern (local overrides global, default fallback):
read_role() {
local path="$1" default="$2"
local local_v global_v
local_v=$(jq -r "${path} // empty" .lisa.config.local.json 2>/dev/null)
global_v=$(jq -r "${path} // empty" .lisa.config.json 2>/dev/null)
echo "${local_v:-${global_v:-$default}}"
}
# e.g. build/github:
CLAIMED=$(read_role '.github.labels.build.claimed' 'status:in-progress')
BLOCKED=$(read_role '.github.labels.build.blocked' 'status:blocked')
Access layer (which surface does each write)
repair-intake stays vendor-neutral; concrete reads/writes go through the same layers the vendor
intakes use. Never call Atlassian MCP or acli directly — go through lisa-atlassian-access.
| Vendor | Reads (scan / comments / links) | Writes (transition / comment / close-out) | Re-dispatch / re-validate |
|---|---|---|---|
| JIRA (build) | lisa-atlassian-access search-issues / lisa-jira-read-ticket |
lisa-atlassian-access transition / comment |
lisa-jira-agent |
| GitHub (build) | gh issue list / gh issue view --json / gh pr list / GraphQL sub-issues |
gh issue edit (labels) / gh issue comment / gh issue close --reason completed |
lisa-github-agent |
| Linear (build) | Linear MCP list_issues / get_issue / list_comments |
Linear MCP save_issue (labels) / save_comment |
lisa-linear-agent |
| Notion (PRD) | lisa-notion-access (query, page comments) |
lisa-notion-access write-page (status) / page comment |
lisa-notion-to-tracker (dry-run) |
| GitHub (PRD) | gh issue list/view (PRD labels) / GraphQL sub-issues / generated-work section |
gh issue edit / gh issue comment / gh issue close --reason completed |
lisa-github-to-tracker (dry-run) |
| Linear (PRD) | Linear MCP list_projects / get_project (+ sentinel feedback issue) |
Linear MCP save_project (labels) / save_comment |
lisa-linear-to-tracker (dry-run) |
| Confluence (PRD) | lisa-atlassian-access CQL |
lisa-atlassian-access page parentId update / comment |
lisa-confluence-to-tracker (dry-run) |
Staleness model
An in-progress item (build claimed, PRD in_review) is stalled when the last
state-changing transition into the in-progress role, or the last human / PR-side forward-progress
activity after that transition, is older than the stale_after threshold. blocked items are NOT
gated on staleness — their repairability is judged on current blocker/answer state, not elapsed
time.
Automation self-comments are not forward progress and must not reset the staleness clock. Status
comments like [claude-build-intake] PR remains open..., [codex-build-intake] Follow-up pushed...,
or [lisa-repair-intake] ... may be useful audit notes, but they cannot make a claimed item fresh
forever. If a provider exposes a changelog/history surface, prefer the timestamp of the last
transition into the claimed/In-Progress role over the item's generic updated timestamp. When the
history surface is unavailable, ignore comments whose author/marker clearly belongs to Lisa or its
automation agents, and use the newest human comment/edit or PR-side progress event instead.
A build claimed leaf whose linked PR has already merged (state == MERGED) is likewise NOT
gated on staleness. A merged PR is a settled terminal state, not in-flight work: the only thing
missing is the env transition build-intake never applied (its merge gate left the item claimed
because the merge landed after its agent returned). The recovery is judged on PR merge state, not
elapsed time — and crucially post-merge activity does not defer it. A freshly-merged PR keeps
producing activity that the signal below would otherwise read as keep-alive (a queued/in-progress
release or deploy check-run, a post-merge CodeRabbit summary comment), so gating merged-PR recovery
on staleness strands a completed leaf in claimed for as long as that activity keeps the clock
warm — exactly the failure that leaves a shipped Sub-task showing status:in-progress for a day
while its parents roll up against it. Recover it regardless of recent activity (Build claimed
decision tree step 0, and the dedicated high-confidence ordering bucket).
Threshold resolution
$ARGUMENTSstale_after=<dur>(one-off override) — always wins. ParseNh/Nm/Nd/0into hours..lisa.config.jsonintake.repair.staleAfterHours(durable project default).- Built-in default: 2 hours.
stale_after=0 means "treat any in-progress item as stalled" — a manual full-recovery lever,
and the only way to resume work on a provider that exposes no reliable activity timestamp.
Activity signal (state-change first, portable across vendors)
Compute the item's newest eligible activity timestamp from the highest-priority signal the vendor
exposes, and compare it to now - stale_after:
- Provider-native status/label transition time into the in-progress role, when the provider
exposes it cleanly (JIRA changelog transition to
claimed/ In Progress, GitHub label event, Linear state/label history, Notion/Confluence page move/status history). - Latest human lifecycle/progress comment or edit on the item (and, for Linear PRDs, the
sentinel feedback issue). Exclude automation self-comments and Lisa audit markers such as
[claude-build-intake],[codex-build-intake],[lisa-build-intake], and[lisa-repair-intake]. - For build items, latest PR-side forward-progress activity on the linked PR: newest commit, review, check-run, or PR comment.
- Provider-native item
updatedAt/last_edited_time/updatedonly when the provider cannot expose transition/comment authorship and the timestamp is not known to be driven by automation self-comments.
If ANY of these is newer than the threshold, the item is active → record it as active and
skip it (read-only). For build claimed, an open PR with recent commits/checks is active. For
PRD in_review, a recent comment or page edit is active.
Count only forward-progress signals as keep-alive: new commits, a review that was just
requested or posted, an in-progress/queued check run, a fresh progress comment. A settled
blocker state — a failing/errored check run, CONFLICTING mergeability, a CHANGES_REQUESTED
review, an unaddressed CodeRabbit/reviewer change request, or a failed deployment — is NOT
keep-alive activity: it does not reset the staleness clock. The clock runs from the last genuine
progress event, so a PR that has been sitting failed/conflicted/awaiting-changes for longer than
stale_after counts as stalled and is diagnosed below.
A merged linked PR is the same kind of non-keep-alive signal, in the other direction: the work
is settled and complete, so its post-merge check-runs and summary comments must NOT count as
keep-alive either. A build claimed leaf with a merged PR is recovered regardless of the staleness
clock (see the Staleness model note above and the dedicated ordering bucket); it is never recorded
active and skipped on the strength of post-merge activity.
If a provider cannot expose any reliable timestamp, do not auto-resume its in-progress
items unless the caller passed stale_after=0. (Dependency-cleared blocked repair still
proceeds — it is judged on blocker state, not time.)
Repair decision tree
Apply per candidate. Continue through the ordered list until every candidate inside the
max_candidates cap has been evaluated. Each candidate may trigger a write (lifecycle transition,
native close/archive/complete, re-dispatch, or refreshed note), be recorded read-only, or be
recorded under Errors. Do not stop after the first write; the cap is the batch boundary.
Before any lifecycle write on a candidate, classify it for automation-reversal per
rejection-detection Automation-reversal memory — a role change this skill made that a human
then moved back. This runs once per candidate at the top of the walk, ahead of the branches below,
because it wraps every repair path rather than belonging to one: a reversed rollup reconciliation
involves no dependency link at all, and a reversed Class-B re-check is the same shape as a reversed
Class-A clearing. Detection is a pure read and unknown never blocks the sweep — an item whose
history or authorship cannot be read is judged by the normal rules below, treated as neither a
reversal nor a clearance. A candidate classified automation-reversal skips the branches below and
is handled per Loop prevention.
Build claimed (stalled in-progress) → diagnose blocker, else resume in place
First check for an already-merged PR — this check is NOT gated on staleness. Read the item's
linked PR state before applying the staleness gate (see "Stuck-cause diagnosis" step 1–2 for
discovery). If state == MERGED, recover it immediately via step 0's merged-PR arm regardless of
elapsed time or recent post-merge activity (per the Staleness model's merged-PR exemption): a merged
PR is a completed leaf, and deferring it behind the staleness clock is what strands shipped work in
claimed.
Only if the PR is not merged does the staleness gate apply. Once it passes, diagnose why it stalled by inspecting the item's PRs and deploys (see "Stuck-cause diagnosis" below). A stalled build usually stalled for a concrete external reason, and re-dispatching the agent at it will not fix a PR that cannot merge or a deploy that failed — it just churns.
- Diagnose PR & deploy state. Run "Stuck-cause diagnosis" below. It resolves, in order:
- PR already merged (checked first, staleness-exempt) → the build effectively completed; the
vendor build-intake's merge gate left the item
claimedbecause the merge landed after its agent returned. Do not re-dispatch or file anything — apply the scanner's post-agent env-resolvedclaimed → donetransition directly (step 2 below, env-resolved), and record it. This is the recovery arm for build-intake leaving merged-but-unadvanced items inclaimed. - PR only behind its base (a needed rebase) → mechanically resolvable, not a human blocker.
Re-sync the branch in place so the already-enabled auto-merge can land (see diagnosis step 3).
Keep the item
claimed; a later cycle confirms the merge and transitions. Do not file a fix ticket for a clean rebase. - A true merge conflict → not an immediate fix ticket. A conflict is fixable by re-running
the build, so attempt one in-place re-dispatch first (the resume sequence below; the vendor
agent re-enters
drive-pr-to-mergefix mode, which resolves conflicts). Only a conflict that survives that single attempt — the same conflicting head stillCONFLICTINGon a later cycle — or that the agent reports needs design input, falls through to the fix-ticket path (diagnosis step 5). - A real external blocker re-running the build cannot fix (failing checks /
CHANGES_REQUESTED/ unaddressed CodeRabbit; or a failed deploy) → do not dispatch the agent. File a build-ready leaf fix ticket for the blocker, move this itemclaimed → blockedwith anis blocked bylink to that ticket, and record it. The existing "Buildblocked→ unblock if cleared" path resumes this item on a later cycle once the fix ticket is terminal — a self-healing loop. Skip the resume steps below.
- PR already merged (checked first, staleness-exempt) → the build effectively completed; the
vendor build-intake's merge gate left the item
If the PR is healthy in-flight and no blocker is found, the work simply died mid-flight — run the same per-item sequence
the vendor build-intake runs, skipping the claim transition (the item is already claimed):
- Dispatch the item to the vendor agent —
lisa-jira-agent/lisa-github-agent/lisa-linear-agent(matching the queue's tracker) — with the item ref. If repair-intake is running as a teammate rather than the lead/root agent, return a structureddelegation-requestto the lead instead of spawning that named peer yourself; only the lead can add named teammates in Claude's flat roster. This resumes the work in place, preserving its existing branch/PR and prior comments. - On agent success, apply the scanner's post-agent transition yourself:
claimed → done, wheredoneis env-resolved exactly aslisa:<tracker>-build-intakeresolves it (perconfig-resolutionenv-keyeddone: explicittarget_envarg wins; else reverse-lookup the env from the resulting PR's base branch viadeploy.branches; then cap the resolved env by the promotion-completeness gate — see "Promotion completeness" below; ifdoneis a map and env is unresolvable, fail loudly — never guess). repair-intake owns this transition because it is standing in for the scanner that never got to finish it. - On a surfaced blocker (agent reports it cannot proceed), leave/move the item to
blockedwith a[lisa-repair-intake]note (see Loop prevention). When the surfaced blocker is something only a human can supply (credentials, access/permissions, a product or scoping decision), the item also carries thehuman_neededmarker — the vendor agent's pre-flight gate applies it; if repair-intake makes the block transition itself for such a reason, it adds the marker too. (A block that another tracked ticket or retry will clear is not human-needed — that is the auto-recoverable fix-ticket path above.)
Do not reset stalled in-progress items to
ready. Reset throws away state, makes a partially-built item look freshly human-approved to the nextlisa-intakeclaim, and forces a two-cycle recovery. Resume in place.
Stuck-cause diagnosis: PR & deploy blockers
Run this for every stalled claimed build item before considering an agent re-dispatch. The
goal is to distinguish "work died mid-flight, just resume it" from "work is blocked on a concrete
external state that resuming the agent cannot fix."
1. Find the associated PR(s) and deploy(s). From the item's linked PRs (GitHub: prefer the
native dev-link surface — gh issue view <n> --json closedByPullRequestsReferences — which lists
merged PRs that closed the issue, then gh pr list --search <issue-ref> --state all; JIRA:
dev-status / remote links; Linear: attachments and git-branch links) and the deploy(s) for the
resulting merge (the env-keyed deploy.branches mapping from config-resolution). The --state all
is load-bearing: gh pr list --search defaults to --state open, so a merged (closed) PR is
invisible on that surface — the exact state this recovery path exists to catch. A merged PR linked
only via search (no Closes # / native dev link) would otherwise never be discovered, and the leaf
would never recover. Read each PR with the vendor's native state, e.g. GitHub
gh pr view <n> --json state,mergedAt,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,comments,reviews.
For reviews, derive the effective set from the latest non-dismissed review
per reviewer, ordered by submitted_at then id. Never count the raw history:
an older approval does not satisfy a later CHANGES_REQUESTED review by the
same reviewer. Use the same paginated REST reduction documented by
lisa-drive-pr-to-merge when the gh pr view projection is incomplete.
2. PR already merged → recover, don't re-dispatch. If state == MERGED, the build is effectively
complete and the only thing missing is the env transition the build-intake never applied (its merge
gate left the item claimed because the merge landed after its agent returned). Do not re-dispatch
or file anything: apply the scanner's post-agent env-resolved claimed → done transition (the
resume-sequence step 2, env-resolved from the merged PR's base branch and capped by the
promotion-completeness gate below) and record it as a repair write.
Run the gate before the write, never after. The blocker path (step 4) assumes the item is still
claimed, so a deploy failure discovered after a lifecycle write has nowhere to go. Resolve every
rung first: if the rung's deploy concluded anything but success, write nothing, leave the item
claimed, and fall through to the blocker path; if it has not concluded, write nothing and leave it
claimed for a later cycle. Only a fully resolved gate result is ever written.
3. PR only behind its base → re-sync in place (mechanical, not a blocker). If the PR is clean but
behind its base — mergeStateStatus == BEHIND while mergeable != CONFLICTING and no required check
is failing — it does not need a human. This is exactly the case that strands a PR forever: GitHub
auto-merge will not advance a BEHIND branch on its own, so a PR opened with --auto sits unmerged
until something rebases it. Delegate this mechanical nudge + classification to the
drive-pr-to-merge skill in report mode — the single source of truth for the
"ensure auto-merge + re-sync a clean BEHIND branch" primitive — so this scanner does not
re-implement it:
drive-pr-to-merge pr=<n> on_blocker=report
In report mode it ensures auto-merge is enabled and runs gh pr update-branch <n> only when the
PR is BEHIND-but-clean and the base branch's ruleset or classic branch protection requires
strict up-to-date status checks (strict_required_status_checks_policy / required_status_checks.strict).
If strict checks are off, it does not update the branch solely because the base moved; that avoids
CI cancellation storms in repos where updating the PR head restarts and cancels in-flight runs. It
never edits code, resolves threads, or dismisses reviews. It returns a classification (merged /
will-merge-after-resync / blocked:<reason>). On a merged / will-merge-after-resync result,
record this as a repair write (resynced), keep the item claimed, and move on — a later cycle sees
the now-CLEAN (or merged) PR and either lets auto-merge finish or applies the merged-PR recovery in
step 2. Only if gh pr update-branch itself reports a conflict it cannot apply does the PR become a
true conflict (step 4). Honor the backoff window so repeated cycles don't re-issue update-branch on
an unchanged head (Loop prevention). For JIRA/Linear items the PR is still the GitHub PR backing the
branch — operate on it the same way.
4. Classify as a blocker. Treat any of these as a real external blocker:
- True merge conflict —
mergeable = CONFLICTINGormergeStateStatus = DIRTY(overlapping changes a plain rebase cannot resolve), orgh pr update-branch(step 3) reported a conflict. A merelyBEHINDbranch is not here — it was re-synced in step 3. Unlike the other classes below, a conflict is resolvable by re-running the build, so step 5 gives it one in-place re-dispatch before filing — see its conflict-first rule. - Failing required checks —
statusCheckRolluphas aFAILURE/ERROR/TIMED_OUTconclusion, ormergeStateStatus = UNSTABLE/BLOCKEDdue to checks. - Change requests outstanding —
reviewDecision = CHANGES_REQUESTED, or unresolved CodeRabbit (or other reviewer) comments that request changes and have not been addressed by a newer commit. - Branch-protection / approvals blocked —
mergeStateStatus = BLOCKEDfor a reason other than a transient check still running. - Failed deploy — the deployment for the item's merge/branch reports a failed/errored status (failed deploy workflow run, failed deployment status, or the project's deploy check is red).
A check that is still queued/in progress, or a CLEAN/HAS_HOOKS mergeable PR with no
outstanding change request, is not a blocker — that is normal in-flight state. (Such a PR with
recent check/commit activity would already have been caught as active by the staleness gate.)
5. On a blocker found → file a leaf fix ticket + block the item.
Conflict-first exception (try to resolve before filing). A true merge conflict — and only a
conflict, not failing checks, change requests, or a failed deploy — is fixable by re-running the build:
the vendor agent's drive-pr-to-merge fix-mode loop resolves conflicts. So before filing a fix ticket
for a conflict, give the item one in-place re-dispatch: run the resume-in-place sequence (steps 1–3
of the parent path above), which re-enters drive-pr-to-merge in fix mode against the existing PR.
Bound it to a single attempt per conflicting head — when you re-dispatch, post a [lisa-repair-intake] conflict-resolve-attempt: <item-ref>@<head-sha> marker keyed on the PR head SHA. On a later cycle, if
that marker already exists for the same head SHA and the PR is still CONFLICTING, the attempt
failed: stop retrying and file the fix ticket below. File immediately (skip the attempt) if the agent
reports the conflict needs design input. Every other blocker class files the fix ticket with no
re-dispatch. Honor the backoff window / state fingerprint (Loop prevention) so the re-dispatch is never
re-issued against an unchanged conflicting head.
- File one build-ready leaf fix ticket per distinct blocker via
lisa-tracker-write(the vendor-neutral leaf writer + validation gate; never a vendor*-write-*skill directly),issue_type: Bugfor a failing-check/conflict/failed-deploy,Taskfor review-feedback follow-up,build_ready: trueso it auto-builds. The ticket MUST name: the blocked item + its PR/deploy URL, the exact blocker (conflict / which checks failed with their logs link / which change requests / which deploy run), three-audience description, and Gherkin acceptance criteria for "PR is mergeable / deploy is green." Every created blocker fix ticket MUST end with therejection-detectionoperator footer as a visible prose line:To stop this from being raised again, close it as **Not planned**. Close it as **Completed** if it was fixed — a later recurrence may be re-filed as a regression. - Transition the stalled item
claimed → blockedand add anis blocked bylink to the new fix ticket (vendor-native: JIRA issue linkis blocked by; GitHub/LinearBlocked by:line- label). Post a
[lisa-repair-intake]note naming what it is blocked by and why. This block is auto-recoverable — the fix ticket will build and close on its own — so do not add thehuman_neededmarker, and if the item carries ahuman_neededlabel this skill applied on an earlier cycle, remove it here (it is no longer waiting on a human). Thehuman_neededmarker is reserved for blocks a human must clear; this one a later cycle clears automatically. A marker this skill did not apply is never removed here — see the marker's provenance rule above. The block's kind says the block self-heals; it says nothing about why a human put a hold on the item, and an operator's hold applied after correcting a wrong transition looks identical to a leftover unless provenance is checked.
- label). Post a
- Record it as a repair write. Do not dispatch the vendor agent for this item this cycle.
The item now sits in blocked; once the fix ticket reaches a terminal state, the Build
blocked → unblock if cleared path (next section) detects the cleared is blocked by
dependency and resumes the original in place — a self-healing loop.
Idempotency. Before filing, check for a fix ticket already carrying the marker
[lisa-repair-intake] blocker:<item-ref>/<blocker-key> (blocker-key is a stable slug of the
blocker, e.g. pr-1234/merge-conflict or pr-1234/checks-failing). Per the rejection-detection
rule's Proposal rejection memory section, that marker search MUST cover open AND closed
tickets (body-enumeration fallback on search-index lag): an open match → reference it and ensure
the is blocked by link is present rather than creating a duplicate; a match closed as not
planned (GitHub stateReason == "not_planned"; the config-resolved equivalent on JIRA/Linear) is
a human decline of that fix ticket — do not re-file it unless evidence postdates the
decline, and the re-filed ticket MUST carry BOTH the machine token (declined <date>; recurred <date> in <ref>) and the human acknowledgment sentence (You declined this on <date>. It has recurred (<date>, <ref>), so we're raising it once more for your review.); a match closed as
completed is a regression path, not a decline. Honor the backoff window and state fingerprint
(Loop prevention) so re-runs over the same unchanged blocker are no-ops.
Promotion completeness: the base branch is not the delivery
A merged PR's base branch names the environment the change entered, not the environments it has
reached. A hotfix merged straight to main enters production out of order: the reverse-lookup
resolves production, production is terminal, and the item closes while staging has none of the
fix and cannot deploy. The evidence is real — merged PR, merge-commit ancestry in main, a green
production deploy — and the inference from it is still wrong.
So before writing any env-resolved done, walk deploy.order from its lowest rung and keep
rungs while they are reached, stopping at the first that is not. A rung is reached when both
hold for its deploy.branches branch:
- Ancestry —
git fetch origin <branch> && git merge-base --is-ancestor <merge-sha> origin/<branch>, asserted for every env branch at or below the resolved env — never only the PR's base. - Deploy health — that branch's most recent deploy concluded
success. Read theconclusion, never thestatus: an in-flight deploy has a null conclusion and is indistinguishable from a pass on the status field alone. Onlysuccesspromotes — a null conclusion and every other conclusion (failure,cancelled,timed_out,neutral,skipped,stale,action_required) leave the rung unreached. An unconcluded deploy is unknown, not green — do not write that rung this cycle; a later cycle reads a concluded run. Where a project exposes no deploy surface for a branch at all, ancestry alone decides that rung.
Write the highest contiguously reached rung at or below the resolved env — never a higher rung,
even when the higher rung is reached and a lower one is not. A merge present in dev and main but
absent from staging writes dev (On Dev), which is intermediate: the item stays natively open
and the promotion gap is what holds it open. When no rung is reached, write nothing and leave the
item in its current role.
The refusal is named, never silent. When the gate caps the env below the resolved one, the
[lisa-repair-intake] note MUST carry all three fields —
<first unreached env> (<its branch>) — <condition> — where the condition is exactly one of
missing ancestry, deploy unknown: <run URL, or "no concluded run"> (an in-flight run, or a
branch with a deploy surface but nothing concluded), or
deploy concluded <conclusion>: <run URL>. A failing run named without its environment and branch,
or an environment named without its condition, is an incomplete note.
Do not classify the gap away. An open back-fill PR against a skipped environment branch is outstanding delivery, not branch hygiene: it is the evidence that a rung was skipped, so it holds the item open. Recording "the fix reached dev and production but skipped staging, so that PR is a branch back-fill, not outstanding delivery" and then closing anyway is this defect exactly — the condition observed, filed under the wrong heading, and overridden.
Build blocked → re-evaluate, unblock if cleared
- Read the block reason and classify the blocker (see Blocker classification & clearing). An item
may be held by a dependency, by a validation / quality-gate self-block, by a
deployed / runtime verification failure, by an ambiguity, or by more than one at once.
Re-check every class present — do not stop at "no
is blocked bylinks, therefore nothing to do." A self-block has zero dependencies by definition, yet is fully re-checkable. - Dependency cleared — if every parsed
is blocked bydependency is cleared → moveblocked → claimed, then run the same agent-dispatch + post-agentclaimed → donesequence as the stalled-claimedpath above (one-cycle recovery). If the agent re-blocks, move back toblocked— a valid outcome. - Validation / quality-gate self-block re-check — if the block reason is a pre-flight
verify/validategate failure (its[lisa-*]block note carries a gate marker + a "Missing requirements" list and there is no openis blocked bydependency), re-run the same gate against the item's current content vialisa-tracker-validate(read-only; the vendor-neutral gatelisa:<tracker>-build-intakeandlisa:<tracker>-write-*already use). This is the build mirror of the PRDblocked → re-validatepath below.- PASS now (the human added what was missing) → move
blocked → claimedand resume exactly as in (2): agent-dispatch + post-agentclaimed → done. - Still FAIL → stay
blocked, but refresh the note with the current (usually smaller) missing-requirement set so the human sees what remains. Because the fingerprint includes the gate verdict + missing-requirement set (Loop prevention), a partial human fix changes the fingerprint and re-checks next cycle, while a truly-unchanged gate result stays in backoff.
- PASS now (the human added what was missing) → move
- Deployed / runtime verification blocker re-check — if the block reason is a failed
deployed or runtime verification (a smoke/E2E/health check or manual probe against a live
environment that errored — e.g. an authenticated endpoint returning 500, a deploy health check
red, a seeded-data assertion failing), re-check by reproducing the original failing check with
the same context it used: the same auth identity/credentials, the same target environment, the
same route, and the same scope/parameters. A probe that does not exercise the failed path is
not evidence the blocker cleared — an anonymous request to an auth-gated resource, a
request against a different environment, or a narrower scope can all return a healthy status
while the originally-failing path is still broken. (This exact false-negative — an unauthenticated
GETto an auth-gated resource returning 200 without touching the failing table — wrongly unblocked a build item whose authenticated path was still 500ing, sending itready → claimed → blockedagain.)- Reproduces clean now (the same check that failed now passes) → move
blocked → claimedand resume as in (2). - Still failing → stay
blocked; refresh the note with the current observed result. Because the root cause is external (a deployed defect, not item content), prefer filing/keeping a build-ready fix ticket and anis blocked bylink to it (the "real external blocker" path), so a later cycle self-heals when that ticket goes terminal. - Cannot reproduce this cycle (the agent lacks the credentials/env access to run the original
check) → stay
blocked; do not unblock on the absence of a reproduction. If the missing access is human-only, apply thehuman_neededmarker. Never unblock a deployed-verification blocker on a weaker signal than the one that set it.
- Reproduces clean now (the same check that failed now passes) → move
- If the block was an ambiguity research can settle and no dependency remains → run the
research needed (
lisa-codebase-research/lisa-product-walkthrough); if resolved, proceed as in (2). - Else → still blocked. Refresh the note with the current reason (Loop prevention) and leave it
blocked.
Build terminal-open → native close / complete / resolve
For each build item that already carries the env-resolved true terminal done role but is still
native-open / active / unresolved:
-
Verify the item is a leaf or a rollup parent whose all required children are terminal. If it is a parent with incomplete children, do not close it; refresh a
[lisa-repair-intake]note naming the incomplete child set. -
Verify the terminal
donerole is the true final value perleaf-only-lifecycleandconfig-resolutionenv-keyeddone. Intermediate env labels (for examplestatus:on-devorstatus:on-stg) are not terminal and must stay open.Verify too that the terminal role was earned, not merely worn: re-run the promotion-completeness gate (above) against the item's merge commit, and close only when the gate reports the terminal rung itself as reached. On a branch with a deploy surface that means
conclusion == success; an in-flight run, or no concluded run at all, does not reach the rung and does not authorize closure. Where a branch exposes no deploy surface, ancestry alone decides it, exactly as the gate documents. If any env branch at or below the terminal one lacks the commit or fails its deploy-health test, do not close: demote the item to the highest contiguously reached rung, keep it open, and refresh a[lisa-repair-intake]note carrying the full refusal reason (env, branch, condition). A terminal label applied before the gate existed is exactly how an out-of-order hotfix reaches native closure — so the label alone is never sufficient evidence to close. -
Perform the provider-native terminal action idempotently:
- GitHub:
gh issue close <number> --repo <org>/<repo> --reason completed. - Linear: move the issue to the configured Done / Completed native workflow state if available; otherwise record the missing native state as a setup error.
- JIRA: verify it is resolved / closed (
statusCategory = Done, resolution set if required); if not, transition through the configured terminal workflow path or report the missing setup.
- GitHub:
-
Post a compact
[lisa-repair-intake]note only when the native close-out changed state or when an actionable setup error must be surfaced. Do not spam already-closed terminal items.
Lifecycle label contradicts native state — walk BOTH directions
A lifecycle label can lie in two ways, and historically only one of them had an owner:
| label | native state | owned by |
|---|---|---|
terminal done role |
still open / active | the Build terminal-open section above |
non-terminal role (ready, claimed, blocked, or any other status: member) |
closed / completed | nothing, until now |
A pass that walks one direction leaves the other accumulating silently. Measured on the Linear TUN
board: TUN-556 and TUN-503 both carry a terminal status:done while still natively open in
On Dev — invisible to intake and indistinguishable from handled, exactly like a bot-applied
status:in-progress on GitHub.
Resolve both directions in one pass with the shipped detector, which always reports
directionsWalked covering both, so a "clean" verdict is an assertion that both were examined.
Build the classifier input inside this cycle. Never consume a temp file another skill wrote.
repair-intake runs as its own cycle, so borrowing lisa-github-build-intake's scratch file fails
two ways: absent, the redirect fails and set -e aborts the whole cycle; stale from an earlier run
on a different item, the resolver classifies that other item and this section reports drifts: []
while directionsWalked still claims both directions were walked. That is precisely the
silent-clean verdict this section promises cannot happen.
TRUST_DIR=$(mktemp -d)
trap 'rm -rf "$TRUST_DIR"' EXIT
gh api "repos/<org>/<repo>/issues/<n>" > "$TRUST_DIR/issue.json"
# --paginate emits ONE ARRAY PER PAGE; --slurp + `add` flattens all of them so a
# label event on page two is not silently dropped.
gh api "repos/<org>/<repo>/issues/<n>/timeline?per_page=100" --paginate --slurp \
> "$TRUST_DIR/pages.json"
jq 'add // []' "$TRUST_DIR/pages.json" > "$TRUST_DIR/timeline.json"
# Same resolution `read_role` uses: local overrides global, both optional.
# Resolving a stale terminal `done` here would make the true terminal label look
# like `open-label-closed-state` — and that direction WRITES.
jq -s '(.[0] // {}) * (.[1] // {})' \
<(cat .lisa.config.json 2>/dev/null || echo '{}') \
<(cat .lisa.config.local.json 2>/dev/null || echo '{}') \
> "$TRUST_DIR/config.json"
jq -n --slurpfile i "$TRUST_DIR/issue.json" \
--slurpfile t "$TRUST_DIR/timeline.json" \
--slurpfile c "$TRUST_DIR/config.json" \
'{issue: $i[0], timeline: $t[0], config: $c[0]}' \
> "$TRUST_DIR/input.json"
node "${CLAUDE_PLUGIN_ROOT}/scripts/lifecycle-label-trust.mjs" < "$TRUST_DIR/input.json"
Untrusted labels are excluded from drifts before any repair runs — the classifier does this
itself, so repair must never act on a label the classifier refused to believe. Without it, a
bot-applied label on a natively closed item would be advanced to the terminal done role by the
open-label-closed-state repair, laundering the exact input the guard rejected into a real write.
The excluded array reports what was held back; surface it, do not repair it.
terminal-label-open-state→ hand to theBuild terminal-open → native closesection above. Do not restate its rules; the terminality test lives there (intermediate env rungs likestatus:on-dev/status:on-stgare not terminal).open-label-closed-state→ the previously unowned direction. The item is natively closed or completed while still wearing a non-terminal lifecycle role. Advance the label to the env-resolved terminaldonerole and post one idempotent[lisa-repair-intake]note. This is a write on Lisa's own lifecycle surface, not a contest with another writer. Apply the leaf/container check from the Lifecycle ownership guard section before this branch: repair-intake owns container repair, so a natively-closedreadyleaf is skipped here and left to the build lane rather than claimed by this pass.- Vendor caveat — Linear. On Linear the lifecycle surface is the native workflow state, not a
status:*label (seeconfig-resolution). Astatus:*label there is leftover cruft that no repair direction reads, which is precisely why TUN-556 and TUN-503 rotted. Treat Linear's native state as authoritative and remove the contradicting stale label; never move the state to match a label.
Membership in the lifecycle namespace is decided by the status: prefix, never a pinned member
list — the live family has drifted 7 → 6 members, and a literal set fails silently by simply not
matching the member it stopped covering.
Bot-authored lifecycle label → distrust, never revert (read-only)
status:* labels written by a bot actor within the plausibility window of the item's creation are
not lifecycle signals — see lisa-github-build-intake Phase 2b for the measurement and the
classifier. Report them here so an operator can see how much of the board is wearing a label nobody
chose:
- list each item alongside the untrusted label, the bot login, and the latency, from the classifier's
untrustedarray; - list
unknownProvenancelabels separately — trusted, but unattributable; - do not unlabel and do not relabel. The bot re-applies on each subsequent review event, so a reverting reconciler produces a label-flap loop that is worse than the defect. Repair happens by intake declining to believe the label, which is idempotent and writes nothing.
This class is deliberately read-only and never counts toward the repair cap's actionable work.
Build parent rollup reconciliation (intermediate-env or terminal close-out)
For each parent/container item (an Epic, a Linear Project, or any item — of any type — with open child work), reconcile its lifecycle state with the roll-up of its children — **including the intermediat
Truncated - read the full file at https://github.com/CodySwannGT/lisa/blob/8f4d3401603dae4777e0f52771f15145a4fb2c30/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md.