Validate GitHub Issue: $ARGUMENTS
Run all organizational quality gates against an issue spec OR an existing issue. This skill is read-only — it never writes to GitHub. The output is a structured report consumed by callers (lisa-github-write-issue for pre-write gating, lisa-github-to-tracker for PRD dry-run, lisa-github-verify for post-write checks).
Input
$ARGUMENTS is one of:
- An existing issue ref (
org/repo#<number>orhttps://github.com/<org>/<repo>/issues/<number>): fetch it and validate the live state. Use this for post-write checks. - A proposed issue spec (YAML block, see schema below): validate as-is without touching GitHub. Use this for pre-write and dry-run checks.
Standalone entry point — validating an item written by another path
Input form 1 is a supported entry point in its own right, not only an internal step of a caller flow. Point this skill at any existing item — however it was written, including by a bespoke script, a direct API or GraphQL call, or the vendor's own web UI — and it fetches the live state and runs the full gate set against it.
Copy-pasteable, via the Skill tool:
Skill(skill: "lisa-github-validate-issue", args: "CodySwannGT/lisa#3663")
where the argument is an issue ref such as CodySwannGT/lisa#3663, or the full issue URL. The report it returns is the same structured PASS/FAIL
report the write path consumes, so a bespoke write path can discharge both the pre-write
validate and the post-write verify obligation with it (see the bespoke-path section of
lisa-github-write-issue).
This skill is plugin-resident. It is invoked through the Skill tool and is not expected to
appear in any repository's scripts/ directory; not finding a shell script by this name is
not evidence that the capability is absent.
Spec schema
Specs are passed as a fenced YAML block. Required keys depend on issue_type.
issue_type: Story # Story | Task | Bug | Epic | Spike | Sub-task | Improvement
org: my-org
repo: my-repo
summary: "[CU-1.2] Upload contract PDF from settings"
priority: medium
parent_ref: "my-org/my-repo#1234" # Parent Epic for ordinary children; PRD for an Epic only in the same-repo GitHub self-host shape; Story/etc. for Sub-task
body: |
## Context / Business Value
...
## Technical Approach
...
## Acceptance Criteria
```gherkin
Scenario: <name>
Given <precondition>
When <action>
Then <observable outcome>
Out of Scope
...
Target Backend Environment
dev
Sign-in Required
Account: ...
Repository
backend-api
Validation Journey
...
Behavioral flags — caller asserts these so the validator can pick the right gates
(on a LIVE item runtime_behavior_change is DERIVED from the stored declaration, not taken on assertion — S8)
runtime_behavior_change: true # → requires Target Backend Environment + Validation Journey; LIVE items derive it from the stored declaration instead (S8) authenticated_surface: true # → requires Sign-in Required artifacts_attached: true # → requires Source Precedence section links: [{ ref: "my-org/my-repo#99", type: "is blocked by" }] # known issue links (may be empty) remote_links: [{ url: "https://github.com/.../pull/42", title: "PR #42" }] journey_followup: auto # auto | none — see S11 build_ready: true # caller asserts the configured build-ready role (github.labels.build.ready, default status:ready) is/would be applied — see S15 child_refs: ["my-org/my-repo#601", "my-org/my-repo#602"] # known child work (sub-issues / task-list / "Blocked by" parentage) — see S15 prd_source: "https://notion.so/..." # set when the issue was generated from a PRD — requires the Source Requirement section, see S16
If the caller passes only an issue ref, fetch via `gh issue view <number> --repo <org>/<repo> --json number,title,body,labels,state,milestone,assignees`, parse the body sections, derive the spec fields — including `runtime_behavior_change`, derived from the `## Target Backend Environment` declaration per `derived-branch-plan` and authoritative over any caller assertion — then run gates. The parser lives in `lisa-github-read-issue` (composition).
## Standalone use, against an item that already exists
This is a supported entry point, not only an internal step of a caller flow.
Point it at a live issue and it fetches and validates the stored state:
```text
Skill(lisa-github-validate-issue) with owner/repo#1234
Use it whenever the issue reached the tracker by some path other than
lisa-github-write-issue — a team's own script, a workflow step, a cron, anything holding the
credentials but no skill runtime. Those paths get neither the pre-write nor the
post-write gate, and their own read-back substitutes for neither: a read-back
proves the tracker stored what was sent, never that what was sent was any good.
The gate definitions live here on purpose, so every caller picks up a change automatically. Running this skill by hand is the same gate the write path runs, not an approximation of it.
Gates
Gates are grouped into Specification (spec-only checks, no GitHub lookups) and Feasibility (requires GitHub lookups). The dry-run path may opt to run Specification gates only via --spec-only; the write path runs both.
Each gate is tagged with a fixed category and a product_relevant boolean. Categories are the same fixed set used by lisa-jira-validate-ticket so downstream PRD-intake comment-formatting policy is shared across vendors.
Per-type content requirements are defined once in the vendor-neutral work-item-definition-of-ready rule (eager + reference); gates S4–S6 and S17 enforce them, and S18 enforces the stateless-pickup property directly.
| Gate | Category | Product-relevant |
|---|---|---|
| S1 Required core fields | structural |
false |
| S2 Summary format | structural |
false |
| S3 Description three audiences | product-clarity |
true |
| S4 Acceptance criteria in Gherkin | acceptance-criteria |
true |
| S5 Bug-specific content | product-clarity |
true |
| S6 Spike-specific content | scope |
true |
| S7 Parent sub-issue declared | structural |
false |
| S8 Target Backend Environment | technical |
false |
| S9 Sign-in Required | technical |
false |
| S10 Single-repo scope | scope |
false |
| S11 Validation Journey | acceptance-criteria |
true |
| S12 Source Precedence | design-ux |
true |
| S13 Relationship Search | dependency |
true |
| S14 Evidence manifest binding (leaf work units) | acceptance-criteria |
true |
| S15 Leaf-only build-ready | structural |
false |
| S16 Source Requirement traceability | product-clarity |
true |
| S17 Improvement measurability | acceptance-criteria |
true |
| S18 Stateless-pickup dry-run | product-clarity |
true |
| S19 Branch Plan derivation | technical |
false |
| S20 Named-control reachability | acceptance-criteria |
true |
| F1 Issue type label exists in repo | structural |
false |
| F2 Parent sub-issue exists and is the right type | structural |
false |
| F3 Linked issues exist | structural |
false |
| F4 Required labels populated | structural |
false |
| F5 Required external access provable | technical |
true |
Category values are the same fixed set as lisa-jira-validate-ticket:
product-clarity,acceptance-criteria,design-ux,scope,dependency,data,technical,structural.
Specification Gates
S1 — Required core fields
org, repo, issue_type, summary, priority, body must all be present and non-empty.
S2 — Summary format
- Single line, ≤ 100 characters.
- Imperative voice ("Add X", "Fix Y", not "Adding X" or "X is broken").
- Bug / Task / Sub-task summaries SHOULD start with a
[repo-name]prefix when the project convention uses one.
S3 — Description has all three audiences
Body must include all of these sections (case-insensitive ## headings):
Context / Business Value— stakeholder-facingTechnical Approach— developer-facingAcceptance Criteria— coding-assistant-facingOut of Scope— explicit non-coverage list
Missing any → FAIL with name of missing section.
S4 — Acceptance criteria in Gherkin
Applies when issue_type ∈ {Story, Task, Bug, Sub-task, Improvement}.
The ## Acceptance Criteria section must contain at least one Scenario: block with Given / When / Then form, ideally inside a ```gherkin code fence. Reject prose-only criteria, "should work" language, or numbered lists without Given/When/Then verbs.
S5 — Bug-specific content
When issue_type = Bug, body must additionally include the full bug anatomy from the work-item-definition-of-ready rule:
- Agent-executable reproduction — numbered steps a stateless agent can run mechanically: exact entry point, named account/role (consistent with Sign-in Required when authenticated), concrete data state, exact actions. Human-followable-only prose ("click around until it breaks") FAILs. A linked failing test satisfies this outright and is the preferred form.
- Expected vs. actual behavior, naming or linking the source of "expected" (spec, PRD requirement, prior release behavior) — a fix target is a fact, not an opinion.
- Environment + version — where reproduced and the build/commit observed; last-known-good when known (
unknownmust be stated, not omitted). - Reproducibility rate — always, or intermittent with observed frequency; intermittent invalidates run-once verification.
- Occurrence evidence — at least one link/attachment: error-tracker issue, log excerpt, stack trace, screenshot/recording.
A Bug's terminal state is its reproduction: the same steps (or test) fail before the fix and pass after — capture both as evidence: under the S14 manifest when runtime_behavior_change = true, or attached directly to the item for non-runtime Bugs (doc/config fixes), where S14 is N/A.
S6 — Spike-specific content
When issue_type = Spike, body must include:
- The question being answered
- The decision the answer enables, and the options being weighed
- A timebox
- Deliverable format and location — decision doc / prototype / findings page, and where it will live, so the terminal state — a deliverable at that location that actually answers the question, with findings, decision and options — is checkable
Gherkin AC is intentionally N/A for Spikes (S4).
S7 — Parent sub-issue declared
When issue_type ∉ {Bug, Epic}, parent_ref must be set — except for a build-ready leaf work unit, which may stand alone. A flat Task / Improvement, or a childless Story / Spike (no open child work) with build_ready = true, is an independently claimable leaf per leaf-only-lifecycle, which states such leaves "must not be stranded"; for these a missing parent is N/A, not a FAIL. This mirrors the leaf carve-out already in S10/S15. A Sub-task is exempt from this exception — it always requires a parent. When a parent IS declared, the native sub-issue link is set by lisa-github-write-issue Phase 6 step 3. (Validity of a declared parent is checked in feasibility gate F2.)
S8 — Target Backend Environment
Every leaf work unit carries ## Target Backend Environment in the body. The section is where runtime_behavior_change is persisted, so it is rendered for exempt work too — see the declaration grammar at the end of this gate. Read accepted environments from the exact configured keys of .lisa.config.json deploy.branches, never from a hardcoded list. Accept a human-confirmed bare exact configured key or Confirmed: <env>, automated Inferred: <env> — evidence: <title|body|reproduction|hostname>, automated Assumption: <env> — remote default branch <branch> for a unique reverse-map, or Assumption: remote default branch <branch> when no unique reverse-map exists. Human confirmation replaces an automated annotation with the bare key or Confirmed: <env>. For legacy bare values, use managed draft markers and current ticket content only; provider edit history is not required. A marker proves automation and requires re-annotation; otherwise unknown provenance plus conflicting evidence fails for confirmation. Validate the annotation shape/source and remote-default branch; validate <env> as an exact configured key whenever present. A valid branch-only assumption must not fail solely because its reverse-map is absent or ambiguous. Normalize built-in prod ↔ production only when exactly one of those keys is configured. No other aliases are valid.
The section persists runtime_behavior_change, so it is never simply skipped. Work that changes no runtime behavior declares the exemption in place of an environment: None — no runtime behavior change: doc-only (or config-only / type-only). A container (an Epic, or any issue holding child work) declares None — container: state rolls up from children. The None — prefix is the machine discriminator; the words after it are for the human reading the issue.
Derive the flag from the stored content, never from a caller's word about a live issue: an exact configured environment key means true, a None — declaration means false, and an absent section means underivable — never false. On a live issue the stored declaration is authoritative, so a caller asserting a runtime_behavior_change that contradicts it FAILs this gate naming both values rather than silently preferring either; one of the two is wrong, and picking one quietly is how an unauditable gate gets built. A proposed spec with no section FAILs — lisa-github-write-issue must render it before the write. A live legacy issue with no section is N/A with a repair note routed to claim time: the same asymmetry S19 uses, for the same reason, because failing every existing issue would turn a legacy queue red for a section no human had a way to add.
S9 — Sign-in Required
When authenticated_surface = true, body must contain ## Sign-in Required naming the account/role and credential source.
If the spec doesn't set authenticated_surface, infer it: scan the body and AC for sign-in / login / "as a {role} user" / authenticated route signals. If signals present and no Sign-in Required section: FAIL.
S10 — Repository section, single-repo scope
When issue_type ∈ {Bug, Task, Sub-task, Improvement} — or a build-ready childless Story/Spike (a claimable leaf per leaf-only-lifecycle) — body must contain ## Repository naming exactly one repo. Multiple repos OR cross-repo references in AC: FAIL with recommendation "Split into per-repo work units under a shared parent Story (see lisa-task-decomposition step 1.5)".
(GitHub Issues live in one repo by definition, so the ## Repository section is technically redundant — keep it for parity with the JIRA path so downstream tooling sees the same shape. Cross-repo references in AC are still possible and still fail this gate.)
An Epic, or a Story/Spike that still holds child work (or is not build-ready): skipped (may span repos — coordination containers, not claimable leaf work units).
This gate is product_relevant: false because cross-repo work units are not a product question — they are a decomposition error. Callers (lisa-github-to-tracker, lisa-notion-to-tracker, etc.) MUST pre-split cross-repo work into per-repo work units during the decomposition phase per lisa-task-decomposition step 1.5; an S10 failure here indicates the agent skipped that step and must auto-split + revalidate before writing, not surface a clarifying comment to product.
S11 — Validation Journey present
When runtime_behavior_change = true, body must contain ## Validation Journey. Skipped for doc-only / config-only / type-only / Epic. When runtime_behavior_change is underivable — a live legacy issue whose body carries no Target Backend Environment declaration (S8) — this gate is N/A with the same repair note rather than PASS, because a requirement conditioned on a flag nobody recorded cannot be asserted either way.
The caller controls strictness via journey_followup:
auto(default): missing section is FAIL with remediation"Invoke lisa-github-add-journey to append the section after create". Callers likelisa-github-write-issueknow to chain the followup automatically.none: missing section is FAIL the caller will not auto-fix (used by dry-run paths).
S12 — Source Precedence (when artifacts attached)
When artifacts_attached = true, body must include source-precedence guidance covering: business rules → PRD body, visual treatment → mocks, flow → prototypes, API/data → data artifacts. Cross-axis conflicts surfaced under ## Open Questions.
Accept either placement:
- A dedicated
## Source Precedencesubsection, OR - A "Source Precedence" / "authoritative source" paragraph under
## Technical Approach.
Detect by scanning for the phrase Source Precedence (case-insensitive) AND verifying the four axes (business rules, visual, flow, data) are each named.
If the spec doesn't set artifacts_attached, infer it the same way S9 infers sign-in: scan the body for design/mock/prototype/data-artifact references (design-tool links, "mock", "prototype", spreadsheet or API artifacts). If such artifacts are referenced and no source-precedence guidance exists: FAIL.
S13 — Relationship Search documented
The issue must EITHER have at least one entry in links, OR the body must contain a ## Relationship Search block listing the git history queries and gh issue list queries that were run with their outcomes. ("Searched git history for <keywords> and gh issue list for label component:X; no related work found.")
An issue with zero links and no documented search: FAIL.
S14 — Evidence manifest binding (leaf work units)
When issue_type ∈ {Bug, Task, Sub-task, Improvement} AND runtime_behavior_change = true, the ## Validation Journey must declare at least one typed [EVIDENCE: <artifact-type>: <name>] marker. These markers are the work unit's evidence manifest — the exact, enumerated set of artifacts that must be captured and attached before the issue may be closed (see the "Per-Work-Unit Evidence Contract" section of the verification rule, the Definition of Done in verification-lifecycle, and the evidence-manifest gate in tracker-evidence).
Each marker must satisfy ALL of:
<artifact-type>is one of the fixed taxonomy:screenshot,recording,http-transcript,cli-output,log-snippet,db-query-output,perf-trace,test-run-log,deploy-log,state-dump. (The legacy[SCREENSHOT: name]form is accepted asscreenshot.)<name>is kebab-case and unique within the issue.
A marker names an artifact, not an assertion. An untyped marker ([EVIDENCE: load-failure-handled-gracefully]) is an assertion label with nothing to capture and must FAIL, with a remediation that shows the typed transformation (e.g. → [EVIDENCE: screenshot: load-failure-error-state], [EVIDENCE: perf-trace: pipeline-load-tti]).
FAIL when the Validation Journey is present but declares zero binding [EVIDENCE: ...] markers, when any binding marker is untyped or uses a type outside the taxonomy, or when any binding name is empty, duplicated, or not kebab-case. A behavior-changing work unit SHOULD declare both a success marker and an error/edge marker; a journey with only one binding marker passes but the remediation should recommend adding the error/edge case.
Parse claiming markers by the exact [EVIDENCE: prefix. A cross-work-item pointer in the canonical form [EVIDENCE-REF: <work-item-ref> | <artifact-type>: <kebab-case-name>] is non-claiming. The Lisa 2.223.0 form [EVIDENCE-REF: <tracker-ref>: <artifact-type>: <kebab-case-name>] is also accepted as a legacy non-claiming alias; parse it from the right so the final two fields are type/name and a tracker URL may contain :. Exclude both forms from the manifest, S14's minimum-marker count, local marker type/name validation, and duplicate-name checks. Independently validate every EVIDENCE-REF: the native work-item reference must be non-empty and unambiguous, the artifact type must use the fixed taxonomy, and the name must be non-empty kebab-case. A malformed reference FAILs S14 as an invalid pointer but never becomes a local evidence obligation. A valid canonical or legacy reference may point to a sibling's artifact, but it never satisfies S14 for this issue. Therefore a runtime-changing leaf whose journey contains only EVIDENCE-REF entries FAILs S14 for zero local claiming markers, not because a valid legacy reference is malformed. Quoting or code-formatting another issue's [EVIDENCE: ...] marker does not make it a reference; writers must convert it to the canonical pipe form.
This gate depends on S11. It is N/A for containers — an Epic, or any item with open child work (coordination containers, not work units) — and for leaf units with runtime_behavior_change = false (doc-only / config-only / type-only). If S11 fails because the Validation Journey is absent, S14 also FAILs (there is no manifest to bind) with remediation pointing back to lisa-github-add-journey. It is likewise N/A with a repair note when runtime_behavior_change is underivable under S8 — reading an absent declaration as false is exactly the silent assumption that made this gate unauditable on a live issue.
S15 — Leaf-only build-ready
Enforces the build-side of the vendor-neutral leaf-only-lifecycle rule: only a leaf work unit may carry the build-ready role. Before evaluating this gate, resolve READY_ROLE from merged project config (.lisa.config.local.json over .lisa.config.json) at github.labels.build.ready, defaulting to status:ready per config-resolution. Use that resolved value everywhere this validator interprets build readiness; a project-specific label is not an alias for the literal default.
This is the symmetric write-side guard for the GitHub validator — a stale or hand-applied READY_ROLE on a container is a lifecycle error and must FAIL here, regardless of how the issue was produced. (Mirrors the "Build-ready label is leaf-only" rule that lisa-github-write-issue applies at write time.)
When the gate applies. Run S15 whenever the issue is build-ready — i.e. build_ready = true, or the spec/live labels include the resolved READY_ROLE. If the issue is not build-ready, S15 is N/A (nothing claims a non-ready issue, so the invariant is vacuous). For example, if github.labels.build.ready = "queue:approved", a container carrying queue:approved FAILs S15 even when it does not carry the literal status:ready default.
Resolve container vs. leaf — structural first, then nominal. Per leaf-only-lifecycle the classification is structural: an item is a container if it has child work, whatever its declared type; otherwise the type label decides. Determine child work from (in order) child_refs, native sub-issues, body task-list checkboxes, and Blocked by #<n> / parent references — the same hierarchy resolution lisa-github-read-issue uses. When validating a live ref, query sub-issues alongside the issue fetch.
Apply this decision and FAIL the two invariant-violating cases:
- Container with child work + build-ready — child work is present (any type that has open children), AND build-ready. FAIL. A parent organizes work; it is never claimed and implemented directly. Its lifecycle state rolls up from its children.
- Childless Epic + build-ready —
issue_type = Epicwith no child work, AND build-ready. Still FAIL: an Epic is a pure rollup container by design, and a childless one is an incomplete decomposition or a mis-applied role, not an implementable unit. (A childless Story or Spike is not failed here — the childless-parent exception inleaf-only-lifecyclepromotes every childless non-Epic type to a build-ready leaf.)
PASS (the childless-parent exception) when the issue is build-ready and is a leaf work unit: it has no open child work and issue_type ≠ Epic (i.e. Bug, Task, Sub-task, Improvement, or a childless Story / Spike). A flat Task/Bug, or a childless Story/Spike with no sub-issues, is a valid build-ready leaf and must not be stranded.
| issue_type | has child work | build-ready | S15 |
|---|---|---|---|
| Bug / Task / Sub-task / Improvement / Story / Spike | no | yes | PASS (leaf) |
| any type | yes | yes | FAIL (structurally a container) |
| Epic | no | yes | FAIL (childless Epic — pure rollup container, exception does not apply) |
| any | any | no | N/A (not build-ready) |
Remediation (render <READY_ROLE> as the resolved configured label, never as a hard-coded default): "Build-ready is leaf-only per leaf-only-lifecycle. Move <READY_ROLE> off this container 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."
product_relevant: false — a build-ready container is a lifecycle/decomposition error for the caller to repair, not a product question.
S16 — Source Requirement traceability (PRD-sourced issues)
Answers "why was this done?": every issue generated from a PRD must carry the requirement it exists to satisfy, quoted verbatim, at every level of the hierarchy — sub-issues included, so a leaf claimed by build-intake in isolation is self-explanatory.
When the gate applies. Run S16 whenever the spec declares prd_source
(all *-to-tracker decomposition paths set it). Without prd_source
(ad-hoc issues with no PRD lineage) the gate is N/A — but if a
Source Requirement section is present anyway, still validate its shape
so a malformed section never passes silently.
What must be present. a Source Requirement section (## markdown heading) containing:
- A link to the source PRD (the
**PRD**:line), and - At least one
**Requirementline with verbatim quoted text, or the explicit derived-work form (Derived work supporting R3, R7 — no single PRD section.).
Missing section, missing PRD link, empty/paraphrased requirement text
(quotes shorter than a few words, or prose with no quotation), or a bare
R-id with no quote: FAIL with remediation
"Add a Source Requirement section citing the PRD link and quoting the requirement(s) this issue satisfies verbatim (see the Source Requirement shared format in the *-to-tracker skills). Derived work must name the requirements it supports."
product_relevant: true — a issue whose requirement cannot be traced is
a product-clarity problem: nobody can tell why the work exists.
S17 — Improvement measurability
When issue_type = Improvement, the body must define the improvement as a measured delta per work-item-definition-of-ready:
- Metric + measurement method the agent can run (command, query, dashboard export)
- Baseline — the current measured value (a number, not an adjective)
- Target — the numeric value or bound that defines done
Without a baseline and a target an Improvement has no verifiable terminal state and can never be autonomously closed. FAIL names the missing pieces; when no baseline exists yet, the remediation is to file measuring it as the first step. N/A for every other type.
S18 — Stateless-pickup dry-run
The autonomy gate, run last, on every build-ready leaf (use the S15 classification; N/A for containers and non-build-ready items). Simulate a stateless agent reading only this item and the links it can resolve — session knowledge about the codebase does not count, because the next claimant will not have it. List every question that agent would have to ask a human before starting work, before choosing between materially different implementations, or before declaring the work done.
Zero questions → PASS. Any question → FAIL, with each question listed verbatim as its own remediation line — these are exactly the clarifying comments the caller posts to the source. The structure gates are proxies; this gate checks the readiness property itself: ready means a stateless agent can drive this item to its terminal state with zero human clarification (see work-item-definition-of-ready).
S19 — Branch Plan derivation
Enforces the derived-branch-plan rule: the ## Branch Plan section is derived only — recompute it from current config and compare; never trust the rendered branches or read them as input. Resolve the environment under the S8 grammar, map that exact configured key forward through .lisa.config.json deploy.branches, and require the mapped branch on the remote.
| Case | Verdict |
|---|---|
runtime_behavior_change = false (doc-only / config-only / type-only) or an Epic/container, with no plan |
N/A — absence is correct; never demand one |
Exempt work carrying a ## Branch Plan |
FAIL — hand-authored branches on work that declared no runtime target |
runtime_behavior_change underivable (live legacy item, no Target Backend Environment declaration) |
N/A with a repair note — absence is never read as false |
| Plan matches the recomputed plan | PASS |
| Plan conflicts with the recomputed plan | FAIL |
Plan missing the Derived from: provenance line and disagreeing |
FAIL — treated as hand-authored |
Branch from and PR into name two different branches |
FAIL — malformed; they are the same branch by construction |
Derivation hits a stop condition (env absent from deploy.branches, ambiguous / non-unique mapping, branch missing on the remote) |
FAIL — the same stop lisa-implement takes; never default to main or the remote default |
| Proposed spec (pre-write) for applicable work with no plan | FAIL — lisa-github-write-issue must render it before the write |
| Live legacy issue for applicable work with no plan | N/A with a repair note — routed to claim time, where lisa-implement writes the derived assumption as a comment and proceeds |
Failing a proposed spec is free; failing every existing issue would turn a legacy queue red for a section no human had a way to add, so legacy absence is repaired at claim time instead.
FAIL names both plans (rendered and recomputed) and points the remediation at the environment, never the branch — e.g. "Branch Plan conflicts with the environment mapping. Rendered 'Branch from: release/staging'; recomputed from Target Backend Environment 'production' via deploy.branches → 'main'. Correct the environment, not the branch — the branches are derived. Then re-render." Never silently choose between the two plans.
S20 — Named-control reachability
Enforces the vendor-neutral control-reachability rule: a work item that names an existing test as a red-before-green control must say what makes that test reach the code the change touches. A control whose reachability cannot be stated is not a control — its green tells the implementer nothing, and the issue's own stopping rule then instructs a revert of a correct fix.
When the gate applies. Run S20 when the body — Acceptance Criteria, Technical Approach, or Validation Journey — names an existing test AND predicts a state change for it when the work lands. Signals: "must go red", "currently passes and must fail", "fails before the fix and passes after", "if it still passes the fix did nothing", or any equivalent stopping rule.
When it must stay silent. N/A otherwise. A issue that introduces a new test rather than pinning an existing one carries no reachability obligation and must never be reported as incomplete on this basis. This half is load-bearing: a gate that fires on every work item is one callers learn to route around.
What must be present. One marker per named control, inside ## Validation Journey:
[CONTROL: <test-identifier> | reaches: <input-or-field>]
Parse by the exact [CONTROL: prefix and split on the single |.
<test-identifier>— non-empty; the test file path, the test name, or both. It must be enough to run the test.reaches:— the literal key, then a non-empty description of the input, field, fixture key, argument, or state that carries execution into the changed code.
FAIL when a named existing-test control has no marker, when a marker is malformed (missing the |, an empty identifier, a missing or empty reaches: half), or when the reaches: half restates the assertion instead of naming an input (reaches: the fix, reaches: the changed code path). Naming the code is not enough; the marker answers "what in this fixture gets execution there".
Remediation: "Name the input or field that makes <test> reach the code this work changes, as [CONTROL: <test> | reaches: <field>]. If the fixture does not reach it, the test cannot observe this change — extend the fixture or specify a new test instead."
product_relevant: true — an unfalsifiable stopping rule is a specification defect, and whoever wrote the issue is who can repair it. lisa-github-write-issue renders the marker; control-reachability carries the implementer-side counterpart, which forbids acting on the stopping rule before the cause of an unmoved control is established.
Feasibility Gates (require GitHub lookups; skip in --spec-only)
F1 — Issue type label exists in repo
gh label list --repo <org>/<repo> --json name --jq '.[].name'
Confirm type:<issue_type> exists. Missing labels can be auto-created by lisa-github-write-issue Phase 5 — flag the absence as a structural FAIL with remediation "Run gh label create type:<issue_type> or let the write path auto-create."
F2 — Parent sub-issue exists and is the right type
When parent_ref is set:
gh issue view <number> --repo <org>/<repo> --json number,labels,state
Confirm the parent issue exists and:
- For an
Epicchild, first resolve whether the validator can prove the native PRD-parent shape allowed byprd-lifecycle-rollup: merged project config hassource = githubandtracker = github; the spec/live child'sorgandrepomatch configuredgithub.organdgithub.repo; andparent_refnames that sameorg/repo. These facts are available from the existing spec/live ref plus project config; do not infer the exception from label spelling alone. Only in that proven same-repository GitHub self-host shape may the parent qualify by having one of the configured PRD lifecycle labels fromgithub.labels.prd(draft,ready,in_review,blocked,ticketed,shipped, orverified; defaults use theprd-*namespace). Resolve the configured values from.lisa.config.local.jsonover.lisa.config.json, using the defaults fromconfig-resolution. Thesentinellabel is not a lifecycle role and does not qualify. This is the native PRD→generated-Epic hierarchy used when GitHub is both source and tracker in one repository; the PRD parent does not needtype:Epic. If any self-host/same-repository predicate is false, a PRD label does not grant the exception: native hierarchy cannot cross repositories or source systems, so a PRD-labelled parent alone FAILs F2. - For a
Sub-taskchild: the parent hastype:Story,type:Task,type:Bug, ortype:Improvement(anything that can host sub-tasks). This allowlist is unchanged; a PRD lifecycle label ortype:Epicalone does not qualify. - For every other non-Sub-task child: the parent has
type:Epic. A PRD lifecycle label alone does not qualify, so the Epic→Story (and Epic→ordinary-leaf) hierarchy remains enforced.
| child type | relationship shape | qualifying parent label | F2 |
|---|---|---|---|
Epic |
source GitHub + tracker GitHub + configured child repo = parent repo | configured PRD lifecycle label | PASS |
Epic |
cross-repository or source/tracker is not same-repo GitHub | PRD lifecycle label only | FAIL |
Epic |
permitted self-host shape | unconfigured prd-* lookalike or prd-intake-feedback sentinel only |
FAIL |
Sub-task |
any | type:Story, type:Task, type:Bug, or type:Improvement |
PASS |
Sub-task |
any | PRD lifecycle label or type:Epic only |
FAIL |
| any other non-Sub-task | any | type:Epic |
PASS |
| any other non-Sub-task | any | PRD lifecycle label only | FAIL |
F3 — Linked issues exist
For each entry in links, run gh issue view <number> --repo <link-org>/<link-repo> to confirm the ref resolves. Flag broken refs.
F4 — Required labels populated
Per Phase 5 of lisa-github-write-issue, every issue MUST carry
type:<issue_type> and priority:<priority>. These two labels are unconditional: if either is
missing from the proposed spec or live issue, FAIL with the missing label name.
The status:* requirement uses this validator's S15 leaf/container classification while preserving
the writer's documented build_ready control-input defaults, which the ready-role-filing rule
fixes at "omitted is NOT build-ready" for every vendor:
- Classify the issue structurally using the S15 child-resolution rules. A container (any issue
with child work, plus a childless
Epic) may omitstatus:*; its state rolls up rather than being assigned directly. - For a proposed leaf spec, normalize omitted
build_readytofalse, perready-role-filing: ready is an explicit claim, so onlybuild_ready: trueasserts it. Explicitbuild_ready: falsemeans the same backlog mode. (This validator previously normalized omitted →trueto mirror GitHub's implicit-ready writer default; both were removed together, and re-introducing either here would just move the leak.) - For a live issue ref, derive
build_readyfrom the labels (trueexactly when the S15-resolvedREADY_ROLEfromgithub.labels.build.ready, defaultstatus:ready, is present). A live backlog leaf without a status label therefore validates asbuild_ready: false; live data cannot distinguish an explicit false from a historical write that omitted the label. - A leaf with normalized
build_ready: trueMUST carry that same resolvedREADY_ROLE. A leaf withbuild_ready: falsemay omitstatus:*.
| classification | normalized build_ready | status label | F4 |
|---|---|---|---|
| container | any | omitted | PASS |
| leaf | false |
omitted | PASS |
| leaf | true |
configured build-ready role (status:ready by default) |
PASS |
| leaf | true |
omitted or a different status:* label |
FAIL |
F4 does not make a container build-ready. S15 remains the independent lifecycle prohibition: any container carrying the build-ready role still FAILs S15, even though F4's status-presence check is not applicable to containers.
F5 — Required external access provable
The factory-gate rule: an input must not enter the pipeline unless the current runtime can actually reach every external surface the work requires. Enumerate the surfaces this issue depends on:
- artifact links in the body (documents, designs, dashboards, spreadsheets, recordings),
- systems named by the description, acceptance criteria, or Validation Journey ("read the CloudWatch alarms", "pull the copy from the Google Doc", "check the Sentry issues"),
- tooling the work plainly implies (a deploy target, a database, a third-party API).
For each surface, prove read access from the current runtime with a target-resource-specific,
read-only probe through its sanctioned access layer: the matching MCP tool or lisa-*-access skill,
or an authenticated fetch using environment-injected authentication. Identity-only commands such as
aws sts get-caller-identity, `g
…(truncated)