@codyswann/lisa 4.33.6 → 4.33.9

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 (132) hide show
  1. package/README.md +22 -0
  2. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +44 -1
  3. package/all/copy-overwrite/scripts/lisa-work-item.mjs +361 -10
  4. package/dist/cli/apply.d.ts.map +1 -1
  5. package/dist/cli/apply.js +8 -1
  6. package/dist/cli/apply.js.map +1 -1
  7. package/dist/configs/vitest/base.d.ts +76 -11
  8. package/dist/configs/vitest/base.d.ts.map +1 -1
  9. package/dist/configs/vitest/base.js +121 -14
  10. package/dist/configs/vitest/base.js.map +1 -1
  11. package/dist/core/apply-receipt.d.ts +18 -0
  12. package/dist/core/apply-receipt.d.ts.map +1 -1
  13. package/dist/core/apply-receipt.js +7 -5
  14. package/dist/core/apply-receipt.js.map +1 -1
  15. package/dist/core/config.d.ts +34 -0
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  19. package/dist/core/lisa-owned-hash-ledger.js +12 -0
  20. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  21. package/dist/core/lisa.d.ts +75 -0
  22. package/dist/core/lisa.d.ts.map +1 -1
  23. package/dist/core/lisa.js +125 -6
  24. package/dist/core/lisa.js.map +1 -1
  25. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  26. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  27. package/dist/core/upstream-evidence-manifest.js +31 -13
  28. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  29. package/dist/core/workflow-deletion-ownership.d.ts +69 -0
  30. package/dist/core/workflow-deletion-ownership.d.ts.map +1 -0
  31. package/dist/core/workflow-deletion-ownership.js +156 -0
  32. package/dist/core/workflow-deletion-ownership.js.map +1 -0
  33. package/dist/opencode/plugin-templates/parity-safety-net.sh +44 -1
  34. package/package.json +4 -4
  35. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  38. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +1 -1
  39. package/plugins/lisa/.codex-plugin/skills/lisa-intake-explain/SKILL.md +1 -1
  40. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +32 -20
  41. package/plugins/lisa/.codex-plugin/skills/lisa-ticket-triage/SKILL.md +1 -1
  42. package/plugins/lisa/hooks/parity-safety-net.sh +44 -1
  43. package/plugins/lisa/rules/eager/blocker-containment.md +44 -0
  44. package/plugins/lisa/rules/reference/blocker-containment.md +92 -0
  45. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  46. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +1 -1
  47. package/plugins/lisa/skills/lisa-intake-explain/SKILL.md +1 -1
  48. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +32 -20
  49. package/plugins/lisa/skills/lisa-ticket-triage/SKILL.md +1 -1
  50. package/plugins/lisa-agy/hooks/parity-safety-net.sh +44 -1
  51. package/plugins/lisa-agy/plugin.json +1 -1
  52. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  53. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +1 -1
  54. package/plugins/lisa-agy/skills/lisa-intake-explain/SKILL.md +1 -1
  55. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +32 -20
  56. package/plugins/lisa-agy/skills/lisa-ticket-triage/SKILL.md +1 -1
  57. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  60. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +44 -1
  64. package/plugins/lisa-copilot/rules/eager/blocker-containment.md +44 -0
  65. package/plugins/lisa-copilot/rules/reference/blocker-containment.md +92 -0
  66. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  67. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +1 -1
  68. package/plugins/lisa-copilot/skills/lisa-intake-explain/SKILL.md +1 -1
  69. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +32 -20
  70. package/plugins/lisa-copilot/skills/lisa-ticket-triage/SKILL.md +1 -1
  71. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +44 -1
  73. package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +97 -0
  74. package/plugins/lisa-cursor/rules/blocker-containment.mdc +49 -0
  75. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  76. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +1 -1
  77. package/plugins/lisa-cursor/skills/lisa-intake-explain/SKILL.md +1 -1
  78. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +32 -20
  79. package/plugins/lisa-cursor/skills/lisa-ticket-triage/SKILL.md +1 -1
  80. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-expo-agy/plugin.json +1 -1
  83. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  88. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  93. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  97. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  98. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  102. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  103. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  107. package/plugins/lisa-rails-agy/plugin.json +1 -1
  108. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  112. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  113. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  115. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  117. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  118. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  120. package/plugins/src/base/hooks/parity-safety-net.sh +44 -1
  121. package/plugins/src/base/rules/eager/blocker-containment.md +44 -0
  122. package/plugins/src/base/rules/reference/blocker-containment.md +92 -0
  123. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  124. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +1 -1
  125. package/plugins/src/base/skills/lisa-intake-explain/SKILL.md +1 -1
  126. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +32 -20
  127. package/plugins/src/base/skills/lisa-ticket-triage/SKILL.md +1 -1
  128. package/scripts/two-channel-couplings.json +1 -1
  129. package/typescript/copy-overwrite/.github/GITHUB_ACTIONS.md +2 -2
  130. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +96 -12
  131. package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking.mjs +3 -2
  132. package/typescript/deletions.json +8 -1
@@ -190,7 +190,7 @@ Apply gate verdicts in the same order as build intake:
190
190
 
191
191
  1. **Repo-scope gate.** A build item carrying `repo:<other>` and not `repo:<current>` is outside this repo's pickup lane. Return `MISCONFIGURED` when repo scope is absent or contradictory enough that the current repo cannot be determined confidently; otherwise explain the repo-scope mismatch and recommend running intake in the target repo or fixing the `repo:<name>` label. For an unlabeled item whose target repo is obvious from the item body, report the inferred repo signal but stay read-only: execution intake would stamp `repo:<name>`, while intake-explain only says it would do so. A multi-repo leaf is not directly buildable; explain that execution intake would split it per `repo-scope-split`, but this read-only diagnosis does not split.
192
192
  2. **Leaf-only gate.** If the item has open child work from native GitHub sub-issues or body parentage, return `NON_LEAF_CONTAINER` and explain that direct build pickup is leaf-only per `leaf-only-lifecycle`. If it has no open children but carries `type:Epic`, also return `NON_LEAF_CONTAINER` because a childless Epic is a pure rollup container that still needs decomposition or reclassification. (A childless `type:Story` or `type:Spike` is a valid leaf per `leaf-only-lifecycle` and is NOT a `NON_LEAF_CONTAINER` — it is directly buildable.) The next action for a childless Epic is decomposition, moving `status:ready` to leaf children, or correcting the issue type. Execution build intake would move such a stale ready container out of the pickup queue; this read-only diagnosis must not perform that repair.
193
- 3. **Dependency hold gate.** If a single-repo leaf for the current repo has explicit blockers, read each blocker. Closed blockers are clear. Open blockers are clear only when they carry a cleared build status such as `status:code-review`, `status:on-dev`, `status:on-stg`, `status:done`, or the configured done-equivalent labels. Open blockers with `status:ready`, `status:in-progress`, no cleared status label, or inaccessible state are active. Return `HELD_BY_BLOCKERS`, list the active blocker refs, and make the next action blocker resolution rather than `/lisa:intake`.
193
+ 3. **Dependency hold gate.** If a single-repo leaf for the current repo has explicit blockers, read each blocker and clear-check it **by containment, per the `blocker-containment` rule** — cite that slug for the full contract rather than restating it. A blocker is clear only when its merged PRs are ancestors of the branch this leaf will be built from (resolved from the leaf's `## Target Backend Environment` through `.lisa.config.json` `deploy.branches`, never from a rendered `Branch Plan`), or when the rule's narrow no-code carve-out or human override applies. **A build status label is never the test** — `status:code-review`, `status:on-dev`, `status:on-stg` and `status:done` are name matches that hold in a single-trunk workflow and come apart in a promotion workflow, where `main` is the trailing branch. Every other state, and every case where containment is not computable, is active: fail closed. Return `HELD_BY_BLOCKERS`, list the active blocker refs with the rule's reason key for each, and make the next action blocker resolution rather than `/lisa:intake`.
194
194
  4. **Ready leaf.** A build item in the configured ready role, scoped to the current repo, with no open child work and no active blockers returns `ELIGIBLE_FOR_INTAKE`. The `Why:` line should say it is a single-repo leaf for the current repo and that leaf-only, repo-scope, and dependency gates all pass.
195
195
 
196
196
  Relevant `Signals:` should include the decisive context, not every field: for example `repo:lisa; type:Sub-task; no open children`, `open children #12/#13`, `repo:api but current repo is web`, or `active blockers CodySwannGT/lisa#123`.
@@ -1005,28 +1005,40 @@ is no normalized `is blocked by` field. Read the bundle, then extract blockers p
1005
1005
  - **Linear**: inspect the native issue **relations** from Linear MCP `get_issue` and select
1006
1006
  blocker relations.
1007
1007
 
1008
- Then classify each blocker:
1009
-
1010
- - **Closed, or shipped to any environment** → **cleared**. A blocker is cleared at **any**
1011
- env-staged `done` role — not only the production terminal. The `done` role is configured
1012
- per-env as a `{dev, staging, production}` map (`config-resolution`), so `On Dev`, `On Stg`,
1013
- **and** production `Done` all mean the blocker's code is merged and deployed to ≥1 environment.
1014
- A post-build `review` role (e.g. `Code Review`) is likewise cleared — the change exists and is
1015
- in flight. An `is blocked by` link is a **development** dependency: it is satisfied once the
1016
- blocker's code is in trunk; it must NOT wait for the blocker to reach production. (This matches
1017
- the intake-path dependency-hold gate in `lisa-intake-explain`, which already treats
1018
- `code-review` / `on-dev` / `on-stg` / `done` as cleared — repair-intake must not be stricter.
1019
- In an env-staged workflow where `Done` means "in production", a strict production-terminal
1020
- check strands every dependent forever behind a blocker that is already merged and sitting at
1021
- `On Stg` / `On Dev`.)
1022
- - **Open** in a pre-merge role (`ready` / `claimed` / unknown — code not yet in trunk) → **still
1023
- blocking**.
1024
- - **Inaccessible** (deleted, cross-org, permission denied) → **still blocking**, unless the item
1025
- body or a newer human comment explicitly states the dependency is resolved.
1008
+ Then classify each blocker **by containment, per the `blocker-containment` rule** — cite that
1009
+ slug for the full contract; do not restate its resolution steps or its fail-closed table here.
1010
+
1011
+ The one question is whether the blocker's code is on the branch **this item** will be built from:
1012
+ resolve this item's base branch from its `## Target Backend Environment` through
1013
+ `.lisa.config.json` `deploy.branches` (`derived-branch-plan`, `config-resolution` — never by
1014
+ reading a rendered `Branch Plan`), resolve the blocker's merge commit through the vendor access
1015
+ layer, and test ancestry against the remote.
1016
+
1017
+ - **Contained** — every merged PR on the blocker is an ancestor of this item's base branch →
1018
+ **cleared**.
1019
+ - **Not contained**, or containment **not computable** for any reason the rule enumerates (no
1020
+ linked PR, PR not merged, blocker inaccessible, base branch underivable or absent from the
1021
+ remote, ancestry query errored) → **still blocking**, with the rule's reason key in the run
1022
+ record.
1023
+ - **Ships no code** — the blocker is closed as completed, positively declares
1024
+ `Target Backend Environment: None — no runtime behavior change`, and has no merged PR →
1025
+ **cleared** under the rule's carve-out. This is a positive determination, never an absence:
1026
+ "no PR found" is `no-pr` and stays blocking.
1027
+ - **Human override** — an explicit human statement on this item that the dependency is satisfied
1028
+ clears it and outranks a failed containment check. Name it in the run record.
1029
+
1030
+ **A lifecycle status name is never the test.** `done`, `on-stg`, `on-dev` and `code-review` are
1031
+ labels that correlate with containment in a single-trunk workflow and come apart from it in a
1032
+ promotion workflow, where `main` is the trailing branch — a blocker at `On Stg` is on `staging`
1033
+ and absent from `main`, and a blocker in `Code Review` is merged to nothing. Containment gets
1034
+ both the loose and the strict failure right at once: a blocker merged into the branch this item
1035
+ builds from clears **immediately**, with no wait for production, so a dependent is never stranded
1036
+ behind work already merged where it needs it.
1026
1037
 
1027
1038
  Only re-dispatch when **every** parsed blocker is cleared. When in doubt, stay blocked — a
1028
- false-negative (left blocked) is cheap; a false-positive (re-dispatched into a real blocker)
1029
- wastes a build cycle.
1039
+ false-negative (left blocked) costs one cycle and is visible; a false-positive is **invisible**,
1040
+ because the item moves to `ready`, which looks exactly like a repair that worked, and the failure
1041
+ surfaces days later as work begun before its dependency existed.
1030
1042
 
1031
1043
  ### Class B — validation / quality-gate self-block
1032
1044
 
@@ -46,7 +46,7 @@ If relevant code IS found, proceed to Phase 2.
46
46
 
47
47
  From the context bundle, evaluate relationships before analyzing this ticket in isolation:
48
48
 
49
- - **Open blockers (`is blocked by`)**: if any blocker is not `Done` or its linked PR is not merged, raise an ambiguity: "Blocker {KEY} is not shipped — work cannot meaningfully start." This is an automatic `BLOCKED` verdict unless the human confirms the blocker state is acceptable.
49
+ - **Open blockers (`is blocked by`)**: clear-check each blocker **by containment, per the `blocker-containment` rule** — cite that slug for the full contract rather than restating it. A blocker is satisfied when its merged PRs are ancestors of the branch this ticket will be built from (resolved from this ticket's `## Target Backend Environment` through `.lisa.config.json` `deploy.branches`, never from a rendered `Branch Plan`), or when the rule's narrow no-code carve-out or human override applies. **`Done` is not the test** — a state name holds in a single-trunk workflow and comes apart in a promotion workflow, and requiring the production terminal strands every dependent behind work already merged where this ticket needs it. This is the same predicate the `DUPLICATE_ALREADY_FIXED` bullet below already requires ("present on the relevant base branch… never from a name/label match alone"), applied here too. For a blocker that is not contained, or where containment is not computable, raise an ambiguity naming the rule's reason key: "Blocker {KEY} is not on {BRANCH} ({REASON}) — work cannot meaningfully start." That is an automatic `BLOCKED` verdict unless the human confirms the blocker state is acceptable.
50
50
  - **Epic siblings in progress**: if a sibling under the same epic is `In Progress` / `In Review` with a different assignee and overlapping scope, raise it as an edge case in Phase 4 ("Duplicate-work risk with {KEY}").
51
51
  - **`duplicates` / `is duplicated by` links**:
52
52
  - If this ticket is a duplicate of an open canonical ticket whose fix is not yet merged into the base branch, verdict is `BLOCKED` with the recommendation to close as duplicate manually rather than implement.
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "inspected": {
7
7
  "workflows": 22,
8
- "steps": 677,
8
+ "steps": 678,
9
9
  "couplings": 33,
10
10
  "inventory": 69
11
11
  },
@@ -45,6 +45,8 @@ For full functionality, configure the following secrets:
45
45
  |--------|---------|--------------|
46
46
  | `EXPO_TOKEN` | EAS builds and updates | Mobile deployment |
47
47
  | `SENTRY_AUTH_TOKEN` | Error tracking | Release monitoring |
48
+ | `SENTRY_ORG` | Sentry organization slug | Release monitoring |
49
+ | `SENTRY_PROJECT` | Sentry project slug | Release monitoring |
48
50
  | `SONAR_TOKEN` | Code quality analysis | Security scanning |
49
51
  | `SNYK_TOKEN` | Vulnerability scanning | Security scanning |
50
52
 
@@ -535,8 +537,6 @@ Variables are non-sensitive configuration values. Set them in **Settings** > **S
535
537
  | `JIRA_BASE_URL` | Jira instance base URL (enables Jira triage workflow) | `https://company.atlassian.net` |
536
538
  | `JIRA_USER_EMAIL` | Email associated with the Jira API token | `user@company.com` |
537
539
  | `JIRA_PROJECT_KEY` | Jira project key for ticket queries | `PROJ` |
538
- | `SENTRY_ORG` | Sentry organization slug | `my-company` |
539
- | `SENTRY_PROJECT` | Sentry project slug | `frontend-app` |
540
540
 
541
541
  ## External Service Setup
542
542
 
@@ -197,6 +197,87 @@ export const OUTCOMES = Object.freeze({
197
197
  noFloorApplied: "mutation-gate: no-floor-applied",
198
198
  });
199
199
 
200
+ /**
201
+ * The outcomes under which Stryker generated mutants and produced a score.
202
+ *
203
+ * THE POINT OF #3668. Every other outcome in {@link OUTCOMES} ends a run that
204
+ * MEASURED NOTHING, and until this set existed the exit-0 members of that group
205
+ * were indistinguishable from a real pass at the only layer a merge decision
206
+ * consults: `🔍 Quality Checks / 🧬 Mutation Testing Gate` printed `pass` for a
207
+ * pull request no mutant had touched. Measured on #3664 — a 400-line change to
208
+ * a shipped guard script, `nothing-to-mutate`, `pass`.
209
+ *
210
+ * The file said so itself, above {@link OUTCOMES}, and had for months: "both
211
+ * exit 0, and only the marker says which one happened." The marker lives in a
212
+ * log; a log is not a control. This set is what lets the workflow render the
213
+ * difference.
214
+ *
215
+ * MEMBERSHIP IS THE STRYKER RUN, not the verdict. `scoped` and `wholeList` are
216
+ * the two outcomes that carry a `reportRun`, so they are the two under which
217
+ * mutants existed — whether the score then cleared the floor or not is a
218
+ * different question, answered by the exit code. A run that measured and FAILED
219
+ * is still a run that measured.
220
+ */
221
+ export const MEASURED_OUTCOMES = Object.freeze([
222
+ OUTCOMES.scoped,
223
+ OUTCOMES.wholeList,
224
+ ]);
225
+
226
+ /**
227
+ * Whether an outcome means mutants were generated against this change.
228
+ *
229
+ * An unknown marker answers `false`, and that direction is deliberate: a new
230
+ * outcome added without being classified must not inherit "measured". Failing
231
+ * closed here costs a `skipping` row on a run that did measure; failing open
232
+ * costs exactly the false green this issue is about.
233
+ *
234
+ * @param {string} outcome - One {@link OUTCOMES} marker
235
+ * @returns {boolean} True when Stryker produced mutants
236
+ */
237
+ export const measuredAnything = outcome =>
238
+ MEASURED_OUTCOMES.includes(String(outcome));
239
+
240
+ /**
241
+ * Publishes the outcome where the workflow can render it, then returns the code.
242
+ *
243
+ * Every exit in {@link runGate} goes through here, and that is the property
244
+ * worth more than the plumbing: an exit that forgets to name its outcome is now
245
+ * impossible to write, rather than merely discouraged by a convention. It is
246
+ * the same shape the automation-runbook contract asks of a loop — end in
247
+ * exactly one named outcome — applied to a gate.
248
+ *
249
+ * `$GITHUB_OUTPUT` rather than a check-run publish, and the difference is
250
+ * forced rather than chosen: `test_mutation` in `quality.yml` declares
251
+ * `permissions: contents: read`, and `quality.yml` is `workflow_call`-only. A
252
+ * called workflow may only DOWNGRADE its caller's grant — asking for `checks:
253
+ * write` is a `startup_failure` for the entire run (#2049), which is why
254
+ * `review-evidence.yml` is a standalone workflow rather than a job. So the
255
+ * verdict leaves as a job output and a downstream job renders it.
256
+ *
257
+ * Best-effort, for the reason every reporting path in this repository is: a
258
+ * lost output line must never change the exit code the gate worked to earn.
259
+ *
260
+ * @param {string} outcome - The {@link OUTCOMES} marker this run ended on
261
+ * @param {number} code - The exit code to return unchanged
262
+ * @param {NodeJS.ProcessEnv} [env] - Environment, injectable for tests
263
+ * @returns {number} `code`, unchanged
264
+ */
265
+ export const finish = (outcome, code, env = process.env) => {
266
+ const target = env.GITHUB_OUTPUT;
267
+ if (target === undefined || target === "") return code;
268
+ try {
269
+ fs.appendFileSync(
270
+ target,
271
+ `mutation_outcome=${outcome}\nmutation_measured=${measuredAnything(outcome)}\n`
272
+ );
273
+ } catch (error) {
274
+ console.error(
275
+ `[mutation-gate] outcome outputs not written: ${error.message}`
276
+ );
277
+ }
278
+ return code;
279
+ };
280
+
200
281
  /**
201
282
  * Stryker config file names, in the order Stryker itself resolves them.
202
283
  *
@@ -2080,7 +2161,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2080
2161
  `⚪ ${OUTCOMES.disabled} — mutation.gate.json says "enabled": false. Skipping.\n` +
2081
2162
  ' Flip "enabled": true (and tune thresholds.break in stryker.conf.json) to turn it on.'
2082
2163
  );
2083
- return 0;
2164
+ return finish(OUTCOMES.disabled, 0);
2084
2165
  }
2085
2166
 
2086
2167
  const declaration = resolveMutateDeclaration(cwd);
@@ -2093,7 +2174,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2093
2174
  ` ${error instanceof Error ? error.message : String(error)}\n` +
2094
2175
  ` Fix the \`mutate\` patterns in ${declaration.source} before re-running.`
2095
2176
  );
2096
- return 1;
2177
+ return finish(OUTCOMES.invalidMutatePattern, 1);
2097
2178
  }
2098
2179
 
2099
2180
  if (countMutateTargetsInRepo(cwd, patterns) === 0) {
@@ -2105,7 +2186,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2105
2186
  " that is switched on and wired to nothing.\n" +
2106
2187
  " Fix the `mutate` patterns in your Stryker config, or turn the gate off."
2107
2188
  );
2108
- return 1;
2189
+ return finish(OUTCOMES.inertConfig, 1);
2109
2190
  }
2110
2191
 
2111
2192
  const unparseable = selectUninstrumentableMutateTargets(cwd, patterns);
@@ -2127,7 +2208,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2127
2208
  " script against a payload table and asserts the blocked/allowed\n" +
2128
2209
  " verdict — never by this gate."
2129
2210
  );
2130
- return 1;
2211
+ return finish(OUTCOMES.uninstrumentableTarget, 1);
2131
2212
  }
2132
2213
 
2133
2214
  if (argv.includes(WHOLE_LIST_FLAG)) {
@@ -2135,7 +2216,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2135
2216
  `🧬 ${OUTCOMES.wholeList} — Stryker over every pattern in ` +
2136
2217
  `${declaration.source}, with no diff scoping.`
2137
2218
  );
2138
- return reportRun(cwd, runStryker(cwd, []));
2219
+ return finish(OUTCOMES.wholeList, reportRun(cwd, runStryker(cwd, [])));
2139
2220
  }
2140
2221
 
2141
2222
  const base = resolveDiffBase(cwd, since);
@@ -2145,7 +2226,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2145
2226
  " unknown ref). Skipping rather than mutating the whole repository.\n" +
2146
2227
  " Nothing was measured; this is not a mutation score."
2147
2228
  );
2148
- return 0;
2229
+ return finish(OUTCOMES.noBase, 0);
2149
2230
  }
2150
2231
 
2151
2232
  let scope;
@@ -2168,7 +2249,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2168
2249
  " NO mutant was generated and NO score was computed. Nothing was measured,\n" +
2169
2250
  " so nothing passed."
2170
2251
  );
2171
- return 1;
2252
+ return finish(OUTCOMES.diffFailed, 1);
2172
2253
  }
2173
2254
 
2174
2255
  if (scope.uninstrumentable.length > 0) {
@@ -2191,7 +2272,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2191
2272
  " on both sides. Check that one exists; nothing here did."
2192
2273
  );
2193
2274
  if (scope.selected.length === 0 && scope.noCurrentLines.length === 0)
2194
- return 0;
2275
+ return finish(OUTCOMES.uninstrumentableLanguage, 0);
2195
2276
  }
2196
2277
 
2197
2278
  if (scope.selected.length === 0) {
@@ -2207,7 +2288,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2207
2288
  " Stryker can place mutants only on current lines. NO mutant was generated\n" +
2208
2289
  " and NO score was computed; this is not a measured pass."
2209
2290
  );
2210
- return 0;
2291
+ return finish(OUTCOMES.noCurrentLines, 0);
2211
2292
  }
2212
2293
  console.log(
2213
2294
  `⚪ ${OUTCOMES.nothingToMutate}\n` +
@@ -2216,7 +2297,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2216
2297
  " NO mutant was generated and NO score was computed. Nothing was measured,\n" +
2217
2298
  " so nothing passed — do not read this as evidence about your tests."
2218
2299
  );
2219
- return 0;
2300
+ return finish(OUTCOMES.nothingToMutate, 0);
2220
2301
  }
2221
2302
 
2222
2303
  // `--mutate` is one comma-separated argument, so a path containing a comma
@@ -2234,7 +2315,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2234
2315
  `${listed}\n` +
2235
2316
  " Rename them, or exclude them in your Stryker config."
2236
2317
  );
2237
- return 1;
2318
+ return finish(OUTCOMES.unrepresentablePath, 1);
2238
2319
  }
2239
2320
 
2240
2321
  console.log(
@@ -2243,7 +2324,10 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2243
2324
  );
2244
2325
  for (const file of scope.selected) console.log(` • ${file}`);
2245
2326
 
2246
- return reportRun(cwd, runStryker(cwd, scope.selected));
2327
+ return finish(
2328
+ OUTCOMES.scoped,
2329
+ reportRun(cwd, runStryker(cwd, scope.selected))
2330
+ );
2247
2331
  };
2248
2332
 
2249
2333
  /**
@@ -36,8 +36,9 @@ function validateAuthority(findings, trackers) {
36
36
  if (!Array.isArray(findings) || findings.length !== 2) {
37
37
  refuse(exactSuiteMessage);
38
38
  }
39
- const labels = findings.map(finding => finding?.label).sort();
40
- const expected = [...TRACKED_SUITE_LABELS].sort();
39
+ const byLabel = (left, right) => String(left).localeCompare(String(right));
40
+ const labels = findings.map(finding => finding?.label).sort(byLabel);
41
+ const expected = [...TRACKED_SUITE_LABELS].sort(byLabel);
41
42
  if (JSON.stringify(labels) !== JSON.stringify(expected)) {
42
43
  refuse(exactSuiteMessage);
43
44
  }
@@ -52,5 +52,12 @@
52
52
  ".github/workflows/reusable-claude-nightly-test-improvement-rails.yml",
53
53
  ".github/workflows/required-checks-drift.yml"
54
54
  ],
55
- "keep": []
55
+ "keep": [],
56
+ "force": {
57
+ ".github/workflows/auto-update-pr-branches.yml": "Removed fleet-wide (CodySwannGT/lisa#3590): it force-updated PR branches with the workflow's own github.token, and GitHub will not start runs from such a push, so required checks went ABSENT at head and pull requests silently stopped becoming mergeable. An edited copy does the same damage, so this removal overrides workflow ownership.",
58
+ ".github/workflows/auto-update-pr-branches-dispatch.yml": "Removed fleet-wide (CodySwannGT/lisa#3590): it force-updated PR branches with the workflow's own github.token, and GitHub will not start runs from such a push, so required checks went ABSENT at head and pull requests silently stopped becoming mergeable. An edited copy does the same damage, so this removal overrides workflow ownership.",
59
+ ".github/workflows/reusable-auto-update-pr-branches.yml": "Removed fleet-wide (CodySwannGT/lisa#3590): it force-updated PR branches with the workflow's own github.token, and GitHub will not start runs from such a push, so required checks went ABSENT at head and pull requests silently stopped becoming mergeable. An edited copy does the same damage, so this removal overrides workflow ownership.",
60
+ ".github/workflows/reusable-auto-update-pr-branches-dispatch.yml": "Removed fleet-wide (CodySwannGT/lisa#3590): it force-updated PR branches with the workflow's own github.token, and GitHub will not start runs from such a push, so required checks went ABSENT at head and pull requests silently stopped becoming mergeable. An edited copy does the same damage, so this removal overrides workflow ownership.",
61
+ ".github/workflows/required-checks-drift.yml": "Removed fleet-wide (CodySwannGT/lisa#3599): it read the live ruleset over the GitHub API, which needs administration:read and therefore a personal access token held in a permanent repository secret. An edited copy still asks for that token, so this removal overrides workflow ownership."
62
+ }
56
63
  }