@codyswann/lisa 4.27.9 → 4.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  2. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  3. package/dist/core/upstream-evidence-manifest.js +43 -8
  4. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  5. package/package.json +5 -4
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +21 -2
  9. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +21 -2
  10. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +21 -2
  11. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +65 -5
  12. package/plugins/lisa/.codex-plugin/skills/lisa-validate-tracker-mapping/SKILL.md +134 -3
  13. package/plugins/lisa/rules/eager/config-resolution.md +23 -0
  14. package/plugins/lisa/rules/reference/config-resolution.md +65 -0
  15. package/plugins/lisa/scripts/repo-scope-vocabulary-audit.mjs +458 -0
  16. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +21 -2
  17. package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +21 -2
  18. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +21 -2
  19. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +65 -5
  20. package/plugins/lisa/skills/lisa-validate-tracker-mapping/SKILL.md +135 -4
  21. package/plugins/lisa-agy/plugin.json +1 -1
  22. package/plugins/lisa-agy/scripts/repo-scope-vocabulary-audit.mjs +458 -0
  23. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +21 -2
  24. package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +21 -2
  25. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +21 -2
  26. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +65 -5
  27. package/plugins/lisa-agy/skills/lisa-validate-tracker-mapping/SKILL.md +135 -4
  28. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  31. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-copilot/rules/eager/config-resolution.md +23 -0
  35. package/plugins/lisa-copilot/rules/reference/config-resolution.md +65 -0
  36. package/plugins/lisa-copilot/scripts/repo-scope-vocabulary-audit.mjs +458 -0
  37. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +21 -2
  38. package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +21 -2
  39. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +21 -2
  40. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +65 -5
  41. package/plugins/lisa-copilot/skills/lisa-validate-tracker-mapping/SKILL.md +135 -4
  42. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +65 -0
  44. package/plugins/lisa-cursor/rules/config-resolution.mdc +23 -0
  45. package/plugins/lisa-cursor/scripts/repo-scope-vocabulary-audit.mjs +458 -0
  46. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +21 -2
  47. package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +21 -2
  48. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +21 -2
  49. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +65 -5
  50. package/plugins/lisa-cursor/skills/lisa-validate-tracker-mapping/SKILL.md +135 -4
  51. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-expo-agy/plugin.json +1 -1
  54. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  74. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-rails-agy/plugin.json +1 -1
  79. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  84. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  89. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/src/base/rules/eager/config-resolution.md +23 -0
  92. package/plugins/src/base/rules/reference/config-resolution.md +65 -0
  93. package/plugins/src/base/scripts/repo-scope-vocabulary-audit.mjs +458 -0
  94. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +21 -2
  95. package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +21 -2
  96. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +21 -2
  97. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +65 -5
  98. package/plugins/src/base/skills/lisa-validate-tracker-mapping/SKILL.md +135 -4
  99. package/scripts/lib/mutation-performance-reporter.mjs +135 -0
  100. package/scripts/mutation-performance-measure.mjs +1794 -0
  101. package/scripts/mutation-performance.schema.json +103 -0
  102. package/typescript/create-only/.github/workflows/review-evidence.yml +18 -10
@@ -352,8 +352,9 @@ the vendor build-intake runs**, skipping the claim transition (the item is alrea
352
352
  2. **On agent success**, apply the scanner's post-agent transition yourself: `claimed → done`,
353
353
  where `done` is **env-resolved** exactly as `lisa:<tracker>-build-intake` resolves it (per
354
354
  `config-resolution` env-keyed `done`: explicit `target_env` arg wins; else reverse-lookup the
355
- env from the resulting PR's base branch via `deploy.branches`; if `done` is a map and env is
356
- unresolvable, fail loudly — never guess). repair-intake owns this transition because it is
355
+ env from the resulting PR's base branch via `deploy.branches`; then **cap the resolved env by
356
+ the promotion-completeness gate** — see "Promotion completeness" below; if `done` is a map and
357
+ env is unresolvable, fail loudly — never guess). repair-intake owns this transition because it is
357
358
  standing in for the scanner that never got to finish it.
358
359
  3. **On a surfaced blocker** (agent reports it cannot proceed), leave/move the item to `blocked`
359
360
  with a `[lisa-repair-intake]` note (see Loop prevention). When the surfaced blocker is something
@@ -393,9 +394,14 @@ same reviewer. Use the same paginated REST reduction documented by
393
394
  complete and the only thing missing is the env transition the build-intake never applied (its merge
394
395
  gate left the item `claimed` because the merge landed after its agent returned). Do **not** re-dispatch
395
396
  or file anything: apply the scanner's post-agent env-resolved `claimed → done` transition (the
396
- resume-sequence step 2, env-resolved from the merged PR's base branch) and record it as a repair
397
- write. Where the env deploy is observable, confirm it did not fail first; a failed post-merge deploy
398
- falls through to the blocker path (step 4).
397
+ resume-sequence step 2, env-resolved from the merged PR's base branch **and capped by the
398
+ promotion-completeness gate below**) and record it as a repair write.
399
+
400
+ **Run the gate before the write, never after.** The blocker path (step 4) assumes the item is still
401
+ `claimed`, so a deploy failure discovered *after* a lifecycle write has nowhere to go. Resolve every
402
+ rung first: if the rung's deploy concluded anything but `success`, write **nothing**, leave the item
403
+ `claimed`, and fall through to the blocker path; if it has not concluded, write nothing and leave it
404
+ `claimed` for a later cycle. Only a fully resolved gate result is ever written.
399
405
 
400
406
  **3. PR only behind its base → re-sync in place (mechanical, not a blocker).** If the PR is clean but
401
407
  behind its base — `mergeStateStatus == BEHIND` while `mergeable != CONFLICTING` and no required check
@@ -495,6 +501,48 @@ recurred (<date>, <ref>), so we're raising it once more for your review.`); a ma
495
501
  _completed_ is a regression path, not a decline. Honor the backoff window and state fingerprint
496
502
  (Loop prevention) so re-runs over the same unchanged blocker are no-ops.
497
503
 
504
+ #### Promotion completeness: the base branch is not the delivery
505
+
506
+ A merged PR's base branch names the environment the change **entered**, not the environments it has
507
+ **reached**. A hotfix merged straight to `main` enters production out of order: the reverse-lookup
508
+ resolves `production`, `production` is terminal, and the item closes while `staging` has none of the
509
+ fix and cannot deploy. The evidence is real — merged PR, merge-commit ancestry in `main`, a green
510
+ production deploy — and the inference from it is still wrong.
511
+
512
+ So before writing any env-resolved `done`, walk `deploy.order` from its **lowest** rung and keep
513
+ rungs while they are **reached**, stopping at the first that is not. A rung is reached when both
514
+ hold for its `deploy.branches` branch:
515
+
516
+ - **Ancestry** — `git fetch origin <branch> && git merge-base --is-ancestor <merge-sha> origin/<branch>`,
517
+ asserted for **every** env branch at or below the resolved env — never only the PR's base.
518
+ - **Deploy health** — that branch's most recent deploy **concluded `success`**. Read the
519
+ `conclusion`, never the `status`: an in-flight deploy has a null conclusion and is
520
+ indistinguishable from a pass on the status field alone. Only `success` promotes — a null
521
+ conclusion and every other conclusion (`failure`, `cancelled`, `timed_out`, `neutral`, `skipped`,
522
+ `stale`, `action_required`) leave the rung unreached. An unconcluded deploy is unknown, not green
523
+ — do not write that rung this cycle; a later cycle reads a concluded run. Where a project exposes
524
+ no deploy surface for a branch at all, ancestry alone decides that rung.
525
+
526
+ Write the highest **contiguously reached** rung at or below the resolved env — never a higher rung,
527
+ even when the higher rung is reached and a lower one is not. A merge present in `dev` and `main` but
528
+ absent from `staging` writes `dev` (`On Dev`), which is intermediate: the item stays natively open
529
+ and the promotion gap is what holds it open. When no rung is reached, write nothing and leave the
530
+ item in its current role.
531
+
532
+ The refusal is named, never silent. When the gate caps the env below the resolved one, the
533
+ `[lisa-repair-intake]` note MUST carry all three fields —
534
+ `<first unreached env> (<its branch>) — <condition>` — where the condition is exactly one of
535
+ `missing ancestry`, `deploy unknown: <run URL, or "no concluded run">` (an in-flight run, or a
536
+ branch with a deploy surface but nothing concluded), or
537
+ `deploy concluded <conclusion>: <run URL>`. A failing run named without its environment and branch,
538
+ or an environment named without its condition, is an incomplete note.
539
+
540
+ **Do not classify the gap away.** An open back-fill PR against a skipped environment branch is
541
+ *outstanding delivery*, not branch hygiene: it is the evidence that a rung was skipped, so it holds
542
+ the item open. Recording "the fix reached dev and production but skipped staging, so that PR is a
543
+ branch back-fill, not outstanding delivery" and then closing anyway is this defect exactly — the
544
+ condition observed, filed under the wrong heading, and overridden.
545
+
498
546
  ### Build `blocked` → re-evaluate, unblock if cleared
499
547
 
500
548
  1. Read the block reason and classify the blocker (see Blocker classification & clearing). An item
@@ -556,6 +604,18 @@ native-open / active / unresolved:
556
604
  2. Verify the terminal `done` role is the true final value per `leaf-only-lifecycle` and
557
605
  `config-resolution` env-keyed `done`. Intermediate env labels (for example `status:on-dev` or
558
606
  `status:on-stg`) are not terminal and must stay open.
607
+
608
+ Verify too that the terminal role was **earned**, not merely worn: re-run the
609
+ promotion-completeness gate (above) against the item's merge commit, and close **only** when the
610
+ gate reports the terminal rung itself as reached. On a branch with a deploy surface that means
611
+ `conclusion == success`; an in-flight run, or no concluded run at all, does **not** reach the
612
+ rung and does **not** authorize closure. Where a branch exposes no deploy surface, ancestry alone
613
+ decides it, exactly as the gate documents. If any env branch at or below the terminal one lacks
614
+ the commit or fails its deploy-health test, do **not** close: demote the item to the highest
615
+ contiguously reached rung, keep it open, and refresh a `[lisa-repair-intake]` note carrying the
616
+ full refusal reason (env, branch, condition). A terminal label applied before the gate existed is
617
+ exactly how an out-of-order hotfix reaches native closure — so the label alone is never
618
+ sufficient evidence to close.
559
619
  3. Perform the provider-native terminal action idempotently:
560
620
  - GitHub: `gh issue close <number> --repo <org>/<repo> --reason completed`.
561
621
  - Linear: move the issue to the configured Done / Completed native workflow state if available;
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lisa-validate-tracker-mapping
3
- description: "Detect and repair drift between a project's configured Lisa status/label mappings and the live tracker/source workflow. Compares every lifecycle role in `.lisa.config.json` (JIRA `jira.workflow` statuses, GitHub/Linear `labels.{build,prd}`, Notion `notion.values` select options, Confluence `confluence.parents`) against the authoritative live names the access layer reports — catching renames, deletions, and case drift (e.g. config `On Stg` vs live `ON STG`). Read-only by default; `repair=true` rewrites the config to the canonical live names (config is fixed, never the tracker). Also reports advisory GitHub scoping-label smells that no config declares — `type:`/`priority:` outside their closed sets, `component:` synonyms of an established value, off-Fibonacci `points:` — which never gate and are never repaired. Audits the current repo by default, or sweeps a set of projects via `projects=<glob>` / `workspaces=<file>`. Safe to schedule for continuous drift detection."
3
+ description: "Detect and repair drift between a project's configured Lisa status/label mappings and the live tracker/source workflow. Compares every lifecycle role in `.lisa.config.json` (JIRA `jira.workflow` statuses, GitHub/Linear `labels.{build,prd}`, Notion `notion.values` select options, Confluence `confluence.parents`) against the authoritative live names the access layer reports — catching renames, deletions, and case drift (e.g. config `On Stg` vs live `ON STG`). Read-only by default; `repair=true` rewrites the config to the canonical live names (config is fixed, never the tracker). Also audits the vocabularies no config key declares: `repo:` scope, where an item scoped only by an alias is valid yet invisible to every `repo:<name>` scan (DRIFTED), and `type:`/`priority:`/`component:`/`points:` smells (advisory). Neither is auto-repaired. Audits the current repo by default, or sweeps a set of projects via `projects=<glob>` / `workspaces=<file>`. Safe to schedule for continuous drift detection."
4
4
  allowed-tools: ["Skill", "Bash", "Read", "Write", "Edit", "AskUserQuestion"]
5
5
  ---
6
6
 
@@ -125,8 +125,8 @@ One role carries a further check that name-existence cannot express:
125
125
  A project's verdict:
126
126
 
127
127
  - **VALID** — every role is VALID.
128
- - **DRIFTED** — at least one CASE_DRIFT, MISSING, or INVERTED role, none of which is UNRESOLVABLE. An INVERTED `ready` is never VALID, no matter how cleanly it resolves.
129
- - **UNRESOLVABLE** — the live set couldn't be enumerated (auth mismatch, missing tracker config, access failure). Distinguish this loudly from VALID — an unresolved audit is not a passing audit.
128
+ - **DRIFTED** — at least one CASE_DRIFT, MISSING, or INVERTED role, none of which is UNRESOLVABLE, **or** any Step 4b repo-scope finding. An INVERTED `ready` is never VALID, no matter how cleanly it resolves.
129
+ - **UNRESOLVABLE** — the live set couldn't be enumerated (auth mismatch, missing tracker config, access failure), **or** Step 4b could not derive a repo vocabulary. Distinguish this loudly from VALID — an unresolved audit is not a passing audit.
130
130
 
131
131
  ## Step 4b — Scoping-label smells (GitHub, advisory)
132
132
 
@@ -211,6 +211,99 @@ Three finding kinds, all `severity: "advisory"`:
211
211
  Do not re-derive any of these thresholds in prose or in a second script. The
212
212
  module is the single definition; this step calls it.
213
213
 
214
+ ## Step 4c — Repo-scope vocabulary (all trackers, counts toward the verdict)
215
+
216
+ Steps 2–4 audit **lifecycle** roles, and they can only ever audit lifecycle
217
+ roles: every pair they compare comes from a `(role, configured-name)` mapping
218
+ declared in `.lisa.config.json`. The **scoping** vocabulary is declared in no
219
+ config key at all, so there is nothing to compare a live name against.
220
+
221
+ `repo:` is the scoping family where that blind spot costs the most, because a
222
+ wrong answer there is not a matter of taste: it decides which work items a
223
+ repository's build queue can see. When the repo vocabulary splits, a scan
224
+ filtering on the canonical spelling returns **fewer items, not an error**, and
225
+ an empty result is indistinguishable from "nothing to do". Work sits unrouted
226
+ and nobody is told. This step is what makes that impossible.
227
+
228
+ > The **open** scoping families — `type:`, `priority:`, `points:`,
229
+ > `component:` — are a separate problem with a separate answer, tracked
230
+ > upstream on #3420. An open vocabulary has no wrong member, only inconsistent
231
+ > ones, so findings about it must stay advisory and must never become a gate.
232
+ > That constraint exists to stop a *guess* becoming a gate; it is not weakened
233
+ > here, because nothing in this step is a guess. Do not extend this step to
234
+ > those families.
235
+
236
+ `assertRepoScope` (`lisa-work-item.mjs`, #1957) accepts **three** spellings as
237
+ valid repo scope, while every build-intake scanner filters on one:
238
+
239
+ | spelling | accepted by validation | found by a `repo:<name>` scan |
240
+ | --- | --- | --- |
241
+ | `repo:<name>` (canonical) | yes | **yes** |
242
+ | bare `<name>` label | yes | **no** |
243
+ | Jira component `<name>` | yes | **no** |
244
+
245
+ The bare branch is deliberate and load-bearing — Sentry-provenance items arrive
246
+ carrying only the bare repo name — so it is **not** to be tightened away. What
247
+ this step reports is the *disagreement* between the two columns: an item that
248
+ passes validation while being invisible to every scan looking for it. That is a
249
+ checkable fact about a specific item, not a heuristic about a name, which is
250
+ why these findings count toward the verdict.
251
+
252
+ ### Derive the vocabulary — never declare it in config
253
+
254
+ Do **not** add a config key for this, and do not accept one if you find it. A
255
+ hand-written repo vocabulary is per-project curation of a list that is already
256
+ derivable, and it becomes its own drift surface — the same objection that
257
+ rejected declaring the open vocabularies. A key nothing reads would look like
258
+ an assertion while asserting nothing. Derive it instead:
259
+
260
+ - **Single-project mode**: the current-repo identity ladder from
261
+ `config-resolution` — `.lisa.config.local.json` `repo` → `.lisa.config.json`
262
+ `repo` → `.lisa.config.json` `github.repo` → `basename -s .git` of the git
263
+ remote.
264
+ - **Batch mode** (`projects=<glob>` / `workspaces=<file>`): the union of that
265
+ ladder across every resolved project, which is exactly the set of repos this
266
+ one tracker serves.
267
+
268
+ If the vocabulary resolves **empty**, the verdict is `UNRESOLVABLE`, never
269
+ `VALID`. A check that reports "nothing wrong" having looked at nothing is the
270
+ same failure this step exists to catch.
271
+
272
+ ### Collect and classify
273
+
274
+ Feed the derived vocabulary, the live work items with their labels (and Jira
275
+ components), and the tracker's declared label set — the declared set is what
276
+ catches a malformed marker carrying zero items:
277
+
278
+ ```bash
279
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/repo-scope-vocabulary-audit.mjs" \
280
+ < /tmp/lisa-repo-scope-input.json
281
+ ```
282
+
283
+ Input is `{ knownRepos: [...], items: [{ ref, labels, components }], labels: [...] }`.
284
+ It returns `{ verdict, findings, knownAliases, vocabulary }` and always exits
285
+ `0` — the verdict is the answer, and the exit status is not a second, quieter
286
+ channel for it. Three finding kinds, all `severity: "drift"`:
287
+
288
+ - **`unstamped-alias`** — work items carry an accepted alias (bare label or
289
+ Jira component) for a known repo **without** the canonical `repo:<name>`
290
+ beside it. They pass validation and no scan finds them. The finding names the
291
+ alias and every item carrying it.
292
+ - **`malformed-marker`** — a label shaped like a repo marker with the wrong
293
+ separator (`repo-frontend`, `repo_backend`). It scopes nothing at all: no
294
+ scan filters on it and validation does not accept it either.
295
+ - **`undeclared-scope`** — a `repo:<value>` label naming a repo outside the
296
+ derived vocabulary. Either the project set is incomplete or the label is a
297
+ misspelling routing work nowhere.
298
+
299
+ An alias on an item that **also** carries the canonical marker is reported in
300
+ `knownAliases`, **not** as drift — every scan still finds that item, so nothing
301
+ is wrong. That distinction is the whole check: an alias cannot be judged from
302
+ the label set alone, only per item.
303
+
304
+ Fold `DRIFTED` from this step into the project verdict exactly as a
305
+ `CASE_DRIFT` or `MISSING` role does.
306
+
214
307
  ## Step 5 — Report
215
308
 
216
309
  Per project, print a terminal-first section:
@@ -236,6 +329,24 @@ An INVERTED `ready` gets a full line rather than a one-word status, because the
236
329
  via /lisa:setup:linear, then set linear.workflow.ready to it.
237
330
  ```
238
331
 
332
+ Print the Step 4c findings **with this mapping table**, not as a separate
333
+ advisory block — they are drift, and a reader must not mistake them for
334
+ smells. Name the items, because "which tickets are invisible" is the operator's
335
+ actual question:
336
+
337
+ ```
338
+ repo scope derived vocabulary: backend, frontend, infrastructure
339
+ backend 2 items scoped by the bare label only UNSTAMPED_ALIAS
340
+ SE-9, SE-10 pass repo-scope validation but carry no "repo:backend",
341
+ so every build scan for backend skips them and reports no error.
342
+ Stamping "repo:backend" on those items is a tracker change a human makes.
343
+ repo-frontend 0 items MALFORMED_MARKER
344
+ repo:mobile outside the derived vocabulary UNDECLARED_SCOPE
345
+ ```
346
+
347
+ A project whose repo vocabulary is in good order prints
348
+ `repo scope: VALID` and nothing else.
349
+
239
350
  Print the Step 4b findings, when there are any, as a clearly separate advisory
240
351
  block **below** the mapping table so nobody reads a smell as a mapping failure:
241
352
 
@@ -250,7 +361,7 @@ Write those lines for a non-technical operator: say what was seen and what a
250
361
  person might do about it, never "FAIL". A healthy repository prints
251
362
  `scoping labels: none` and nothing else.
252
363
 
253
- End with a roll-up: counts of VALID / DRIFTED / UNRESOLVABLE projects and the exact next command (`… repair=true` when drift is auto-repairable; an admin note when a status is genuinely MISSING; `/lisa:setup:linear` when a `ready` is INVERTED). Scoping findings are reported alongside that roll-up as a **separate count**, never folded into DRIFTED.
364
+ End with a roll-up: counts of VALID / DRIFTED / UNRESOLVABLE projects and the exact next command (`… repair=true` when drift is auto-repairable; an admin note when a status is genuinely MISSING; `/lisa:setup:linear` when a `ready` is INVERTED; an admin note naming the items when a repo scope is UNSTAMPED_ALIAS). Scoping-label findings are reported alongside that roll-up as a **separate count**, never folded into DRIFTED.
254
365
 
255
366
  ## Step 6 — Repair (only when `repair=true`)
256
367
 
@@ -305,6 +416,22 @@ real issue metadata on a coin flip. Report the finding, name the two spellings,
305
416
  and leave the decision to a human — and note that reconciling them means
306
417
  relabelling the issues, which is a tracker mutation this skill never performs.
307
418
 
419
+ ### Repo-scope findings — never repaired, for a different reason
420
+
421
+ Step 4c findings are never auto-repaired either, but not because the right
422
+ answer is unknowable — often it is obvious. It is because **every** available
423
+ fix is a **tracker** mutation, and `repair=true` writes the config only:
424
+
425
+ - an `unstamped-alias` is fixed by adding `repo:<name>` to the work items,
426
+ - a `malformed-marker` by renaming or deleting a label,
427
+ - an `undeclared-scope` by deciding whether the name is a real repository (so
428
+ the project set is incomplete) or a misspelling (so the label is wrong).
429
+
430
+ There is no config edit that resolves any of them, and there is deliberately no
431
+ config key to edit — see Step 4c on why the vocabulary is derived. Report the
432
+ finding, name the items, and leave the tracker to a human.
433
+
434
+
308
435
  ### Invalidate the verification cache
309
436
 
310
437
  After any JIRA repair, clear the `setup-jira` reachability cache so it re-verifies the new mapping:
@@ -334,3 +461,7 @@ Re-print the project section showing the post-repair mapping and the new verdict
334
461
  - Reuse the config-resolution defaults and the vendor access skills (`atlassian-access`, `gh`, Linear/Notion MCP); do not invent a parallel mapping or lifecycle vocabulary.
335
462
  - Scoping-label findings (Step 4b) are **advisory**. They never change a project's verdict, never change this skill's exit status, and are never auto-repaired. Do not add a flag that turns them into a gate.
336
463
  - Never let Step 4b fail the run. A `gh` error, a non-GitHub tracker, or an empty repository means the scoping section reports "not collected" — the lifecycle verdict is unaffected either way.
464
+ - Step 4c repo-scope findings are **drift**, not smells: they count toward the verdict and are reported with the mapping table. They are still never auto-repaired, because every fix is a tracker mutation.
465
+ - Derive the repo vocabulary from the current-repo identity ladder (and, in batch mode, the resolved project set). Never introduce a config key declaring it, and never trust one — a hand-written vocabulary is a second thing to drift, and a key nothing reads would assert nothing while looking like an assertion.
466
+ - An empty derived repo vocabulary is `UNRESOLVABLE`, never `VALID`. The whole point of Step 4c is that a silent zero is the failure, so a zero-length vocabulary must never read as a pass.
467
+ - Do not extend Step 4c to the open scoping families (`type:`, `priority:`, `points:`, `component:`). Findings about those are heuristic and must stay advisory — see #3420.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.27.9",
3
+ "version": "4.28.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -605,9 +605,73 @@ Skills that transition to `done` MUST resolve the env first:
605
605
  1. **Explicit caller arg** (`target_env=staging`) — always wins.
606
606
  2. **Branch inference** — derive from the PR's base branch via `deploy.branches`. Reverse-lookup: if base branch is `staging`, env is `staging`.
607
607
  3. **Failure** — if neither resolves and `done` is a map, fail loudly. Never pick arbitrarily.
608
+ 4. **Promotion completeness** — cap the resolved env by the gate below before writing anything. Steps 1–2 resolve a *candidate*; the gate decides what is written.
608
609
 
609
610
  If a project's terminal state is the same regardless of env, set `done` to a string instead of a map (lifecycle skills accept either shape).
610
611
 
612
+ #### Promotion completeness: every environment at or below the resolved one
613
+
614
+ A merged PR's base branch says which environment the change **entered**. It never says which
615
+ environments the change has **reached**. A hotfix merged straight to `main` enters production by an
616
+ out-of-order route: the reverse-lookup resolves `production`, `production` is the terminal value,
617
+ and the item closes while `staging` has none of the fix. Reaching the terminal environment is not
618
+ the same as being delivered, and a base branch alone cannot tell those apart.
619
+
620
+ So the resolved env is not the env that gets written. Walk `deploy.order` from its **lowest** rung
621
+ and keep rungs while they are **reached**, stopping at the first that is not. A rung is reached
622
+ when both hold for its `deploy.branches` branch:
623
+
624
+ - **Ancestry** — the merge commit is an ancestor of the remote env branch:
625
+ `git fetch origin <branch> && git merge-base --is-ancestor <merge-sha> origin/<branch>`. Assert
626
+ this for **every** env branch at or below the resolved one — never only the PR's base.
627
+ - **Deploy health** — that branch's most recent deploy **concluded `success`**. Read the
628
+ `conclusion`, never the `status`: an in-flight deploy has a null conclusion and is
629
+ indistinguishable from a pass on the status field alone. Only `success` promotes — a null
630
+ conclusion and every other conclusion (`failure`, `cancelled`, `timed_out`, `neutral`, `skipped`,
631
+ `stale`, `action_required`) leave the rung **unreached**. "Did not fail" is not the test: a
632
+ cancelled or skipped run is not evidence the change deployed.
633
+
634
+ The written env is the highest **contiguously reached** rung at or below the resolved one — never a
635
+ higher rung, even when the higher rung is reached and a lower one is not. A merge present in `dev`
636
+ and `main` but absent from `staging` resolves to `dev` (`On Dev`), not `production`: the item stays
637
+ open at an intermediate waypoint, and the promotion gap is what holds it open. When no rung at all
638
+ is reached, write nothing and leave the item in its current role.
639
+
640
+ Refusals are named, never silent, and one schema covers every case. Whenever the gate caps the env
641
+ below the resolved one, the recorded reason MUST carry **all three** fields — none is optional in
642
+ any case:
643
+
644
+ ```
645
+ <first unreached env> (<its branch>) — <condition>
646
+ ```
647
+
648
+ `<condition>` is exactly one of:
649
+
650
+ - `missing ancestry` — the merge commit is not an ancestor of that branch.
651
+ - `deploy unknown: <run URL, or "no concluded run">` — the branch has a deploy surface, but its most
652
+ recent run has not concluded, or nothing has ever concluded for it.
653
+ - `deploy concluded <conclusion>: <run URL>` — the most recent run concluded as something other than
654
+ `success` (`failure`, `cancelled`, `timed_out`, `neutral`, `skipped`, `stale`, `action_required`).
655
+
656
+ "Merged to `main` and production deployed green" and "not in `staging`" are both facts; a reason that
657
+ carries only the first is the defect. A failing run named without its environment and branch, or an
658
+ environment named without the condition that failed it, is an incomplete reason and not acceptable.
659
+
660
+ Two classifications this gate forbids:
661
+
662
+ - An **open back-fill PR** against a skipped environment branch is *outstanding delivery*, not
663
+ branch hygiene. It is the evidence that a rung was skipped, so it is a reason to hold the item
664
+ open — never a reason to discount the gap and close anyway.
665
+ - An **in-flight deploy** is unknown, not green. When a rung's ancestry holds but its deploy run has
666
+ not concluded, do not write that rung this cycle: leave the item where it is and let a later cycle
667
+ read a concluded run. Only a concluded success promotes. A project that exposes **no** deploy
668
+ surface for a branch is a different case — there, ancestry alone decides the rung; the absence of
669
+ a deploy system is not an unconcluded deploy.
670
+
671
+ Where `deploy.order` is absent the ladder is the single resolved env, and the gate is the ancestry
672
+ and deploy-health check on that one rung. Multi-branch projects MUST set `deploy.order` (see "Env
673
+ order" below), so an absent order can never silently skip the ladder.
674
+
611
675
  ### Env → base branch (forward: the build base and PR base)
612
676
 
613
677
  `deploy.branches` is also read in the **forward** direction by the build flow (`lisa-implement`): the environment a work item targets determines the branch the work is built on and the branch the PR opens against.
@@ -639,6 +703,7 @@ The true terminal `done` value is also the only value that triggers provider-nat
639
703
  - If `done` is a string, that value is terminal.
640
704
  - If `done` is an env-keyed map, the production / final environment's value is terminal. The conventional key is `production`; project-specific final env names must be explicit in deploy config or the lifecycle skill must fail rather than guessing.
641
705
  - Intermediate env values (`dev`, `staging`, or configured equivalents) are deployment waypoints. Applying them must not close / resolve / complete the native tracker item.
706
+ - The terminal value is earned only by a **promotion-complete** merge: every env branch at or below the terminal one carries the merge commit, and none of their most recent concluded deploys failed. A merge that reached the terminal environment out of order has not earned the terminal value — see "Promotion completeness" above.
642
707
 
643
708
  ### Env order (sync-down chain)
644
709
 
@@ -94,4 +94,27 @@ A non-integration environment bug is fixed, merged, and verified on that
94
94
  environment branch first, then
95
95
  forward cherry-picked down to the integration branch via a linked follow-up.
96
96
 
97
+ ## Env-keyed `done` — promotion completeness
98
+
99
+ A merged PR's base branch names the environment a change **entered**, never the environments it has
100
+ **reached**. Before writing an env-keyed `done`, walk `deploy.order` from its lowest rung and write
101
+ the highest **contiguously reached** rung at or below the resolved env. A rung is reached only when
102
+ the merge commit is an ancestor of its `deploy.branches` branch
103
+ (`git merge-base --is-ancestor <merge-sha> origin/<branch>`, asserted for **every** env branch at or
104
+ below the resolved one, not only the PR's base) **and** that branch's most recent deploy
105
+ **concluded `success`** — read the `conclusion`, never the `status`, because an in-flight deploy has
106
+ a null conclusion and looks identical to a pass. Only a concluded `success` promotes: a null
107
+ conclusion and every other conclusion (`failure`, `cancelled`, `timed_out`, `neutral`, `skipped`,
108
+ `stale`, `action_required`) leave the rung unreached. Where `deploy.order` is absent the ladder is
109
+ the single resolved env; where a branch exposes no deploy surface at all, ancestry alone decides
110
+ that rung.
111
+
112
+ A hotfix straight to `main` that skipped `staging` therefore resolves to the rung below the gap and
113
+ stays open; the terminal value, and provider-native closure with it, is earned only by a
114
+ promotion-complete merge. The recorded reason always carries all three fields —
115
+ `<first unreached env> (<its branch>) — <condition>`, where the condition is `missing ancestry`,
116
+ `deploy unknown: <run URL or "no concluded run">`, or `deploy concluded <conclusion>: <run URL>`. A
117
+ failing run named without its environment and branch is an incomplete reason. An open back-fill PR
118
+ against a skipped environment branch is outstanding delivery, not branch hygiene.
119
+
97
120
  Full reference: [reference/config-resolution.md](config-resolution-reference.mdc).