@codyswann/lisa 3.14.3 → 3.14.5

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 (72) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +13 -3
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/expo/create-only/.github/workflows/maestro-e2e.yml +21 -8
  5. package/package.json +1 -1
  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 +54 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +95 -0
  10. package/plugins/lisa/scripts/lifecycle-label-trust.mjs +494 -0
  11. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +54 -0
  12. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +95 -0
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-agy/scripts/lifecycle-label-trust.mjs +494 -0
  15. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +54 -0
  16. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +95 -0
  17. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/scripts/lifecycle-label-trust.mjs +494 -0
  24. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +54 -0
  25. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +95 -0
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cursor/scripts/lifecycle-label-trust.mjs +494 -0
  28. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +54 -0
  29. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +95 -0
  30. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-agy/plugin.json +1 -1
  33. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  53. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-agy/plugin.json +1 -1
  58. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  63. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  68. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/src/base/scripts/lifecycle-label-trust.mjs +494 -0
  71. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +54 -0
  72. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +95 -0
@@ -71,11 +71,15 @@ on:
71
71
 
72
72
  permissions:
73
73
  contents: read
74
- # Read-only, and only this run's own job list. It is what
75
- # `serialize_platform_legs` below needs to know when the iOS leg has
76
- # finished — a called workflow cannot hold a permission the caller did not
77
- # grant, so dropping this line turns that input into a red job rather than a
78
- # silent no-op. Harmless when the input is off.
74
+ # Read-only, and only this run's own job list. It is granted HERE, in the
75
+ # caller, because that is the only place it can safely be granted: a called
76
+ # workflow that DECLARES a scope its caller did not grant is a
77
+ # `startup_failure` for the whole run. So this line plus the forwarded
78
+ # LEG_ORDER_TOKEN secret below are what let `serialize_platform_legs` read
79
+ # when the iOS leg finished.
80
+ #
81
+ # Drop this line and nothing breaks — the ordering job reports the gap and
82
+ # lets the Android leg start. You lose the ordering, not the suite.
79
83
  actions: read
80
84
 
81
85
  # Keyed on the ENVIRONMENT this suite drives, not on the ref. Every run of this
@@ -126,10 +130,19 @@ jobs:
126
130
  # other's session, so the loser hits a sign-in screen mid-suite and fails
127
131
  # an assertion that has nothing wrong with it. This runs iOS first, then
128
132
  # Android. It is ordering, not gating: a FAILING iOS leg still releases
129
- # Android, so turning it on never narrows what gets tested. Requires the
130
- # `actions: read` permission above. Leave it off if your legs use
131
- # per-platform accounts the concurrency is free coverage in that case.
133
+ # Android, so turning it on never narrows what gets tested. Needs the
134
+ # `actions: read` permission above AND the LEG_ORDER_TOKEN secret below;
135
+ # without either it reports the gap and runs the legs concurrently rather
136
+ # than failing. Leave it off if your legs use per-platform accounts — the
137
+ # concurrency is free coverage in that case. The cost when on is the suite
138
+ # taking iOS PLUS Android instead of the slower of the two.
132
139
  # serialize_platform_legs: true
133
140
  secrets:
134
141
  EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
135
142
  MAESTRO_SECRET_ENV: ${{ secrets.MAESTRO_SECRET_ENV }}
143
+ # Only read when serialize_platform_legs is true. A forwarded secret
144
+ # carries THIS workflow's token and therefore THIS workflow's
145
+ # `actions: read` — which is why the scope can be granted above without
146
+ # the reusable workflow having to declare it and startup-fail every caller
147
+ # that grants less. Harmless to pass when the input is off.
148
+ LEG_ORDER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/package.json CHANGED
@@ -125,7 +125,7 @@
125
125
  }
126
126
  },
127
127
  "name": "@codyswann/lisa",
128
- "version": "3.14.3",
128
+ "version": "3.14.5",
129
129
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
130
130
  "main": "dist/index.js",
131
131
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.14.3",
3
+ "version": "3.14.5",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.14.3",
3
+ "version": "3.14.5",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -168,6 +168,60 @@ gh label list --repo <org>/<repo> --json name \
168
168
 
169
169
  If none of the configured role labels exist on the repo → label convention not adopted, surface a setup error and exit. If the role labels exist but none are `$READY` on any open issue matching the resolved assignee filter (or any open issue when the filter is empty) → genuinely empty queue, exit cleanly with `"No GitHub issues labeled $READY. Nothing to do."`
170
170
 
171
+ #### 2b. Lifecycle-label trust resolution (bot-authored labels are not signals)
172
+
173
+ **A `status:*` label is only a lifecycle signal if a trustworthy actor applied it.** Third-party GitHub Apps write labels through the same API humans do, and at least one — `coderabbitai[bot]` — stamps `status:*` on issues seconds after filing. Measured on CodySwannGT/lisa#2460–#2540: seven of eight bot-applied lifecycle labels landed 26–118s after creation. Those labels are guesses wearing the costume of the intake contract, and they break the queue in **both** directions:
174
+
175
+ - a bot-applied **`$CLAIMED`** makes an unworked issue look like somebody's active work, so no cycle picks it up and no human re-examines it (#2470 sat unworked for a day this way);
176
+ - a bot-applied **`$READY`** puts an issue into the build queue that no human ever flipped ready (#2538), inverting the gate the ready role exists to be.
177
+
178
+ Before treating any candidate's lifecycle labels as meaningful, resolve which ones are trustworthy:
179
+
180
+ ```bash
181
+ TRUST_DIR=$(mktemp -d)
182
+ trap 'rm -rf "$TRUST_DIR"' EXIT
183
+
184
+ gh api "repos/<org>/<repo>/issues/<n>" > "$TRUST_DIR/issue.json"
185
+
186
+ # --paginate emits ONE ARRAY PER PAGE. Reading `$t[0]` would pass only page one,
187
+ # silently dropping later label events — the newest of which is exactly the
188
+ # application this classifier keys on. --slurp + `add` flattens all pages.
189
+ gh api "repos/<org>/<repo>/issues/<n>/timeline?per_page=100" --paginate --slurp \
190
+ > "$TRUST_DIR/pages.json"
191
+ jq 'add // []' "$TRUST_DIR/pages.json" > "$TRUST_DIR/timeline.json"
192
+
193
+ # Resolve config the SAME way `read_role` does: the local file overrides the
194
+ # global one key by key. Reading only `.lisa.config.json` would resolve a stale
195
+ # terminal `done` label for any project that overrides it locally, and
196
+ # `lisa-repair-intake` WRITES on the drift direction that mistake produces.
197
+ # Both files are optional — a missing one must not abort the cycle.
198
+ jq -s '(.[0] // {}) * (.[1] // {})' \
199
+ <(cat .lisa.config.json 2>/dev/null || echo '{}') \
200
+ <(cat .lisa.config.local.json 2>/dev/null || echo '{}') \
201
+ > "$TRUST_DIR/config.json"
202
+
203
+ jq -n --slurpfile i "$TRUST_DIR/issue.json" \
204
+ --slurpfile t "$TRUST_DIR/timeline.json" \
205
+ --slurpfile c "$TRUST_DIR/config.json" \
206
+ '{issue: $i[0], timeline: $t[0], config: $c[0]}' \
207
+ > "$TRUST_DIR/input.json"
208
+
209
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/lifecycle-label-trust.mjs" < "$TRUST_DIR/input.json"
210
+ ```
211
+
212
+ The temp directory is **per invocation**. Fixed `/tmp/lisa-*.json` paths let two concurrent intake cycles interleave one issue's metadata with another's timeline, which yields a confident verdict about an item that was never examined.
213
+
214
+ The classifier returns `trusted`, `untrusted`, `unknownProvenance`, and a per-label `evaluated` list carrying the actor and the latency behind each decision. **Use `trusted` wherever this skill would otherwise read the raw label set**, and specifically:
215
+
216
+ - a candidate whose `$READY` is **untrusted** is **not claimable** — skip it and leave it for a human to flip genuinely ready; report it in the summary rather than dispatching it;
217
+ - a candidate whose `$CLAIMED` is **untrusted** is **not claimed** — if its `$READY` is trusted it stays a normal candidate, exactly as if the bot label were absent.
218
+
219
+ **Never unlabel to correct this.** The bot re-applies its label on each subsequent review event, so reverting produces a label-flap loop that is worse than the defect. The guard is that intake stops *believing* the label; it writes nothing. Distrust is idempotent and cannot race.
220
+
221
+ A label whose provenance cannot be established (applied at creation, which GitHub records no `labeled` event for) is trusted but listed in `unknownProvenance` — failing closed there would ignore the human `status:ready` that opens the queue. Report that list; do not silently drop it.
222
+
223
+ Lifecycle membership is decided by the **`status:` prefix**, never by a pinned member list. The live family has drifted 7 → 6 members, and a literal set breaks silently: an unrecognised member simply fails to match, so the guard would report clean on exactly the case it stopped covering.
224
+
171
225
  ### Phase 3 — Process the first eligible ready issue
172
226
 
173
227
  #### 3a.0 Repo-scope gate (claim only current-repo issues)
@@ -560,6 +560,101 @@ native-open / active / unresolved:
560
560
  4. Post a compact `[lisa-repair-intake]` note only when the native close-out changed state or when
561
561
  an actionable setup error must be surfaced. Do not spam already-closed terminal items.
562
562
 
563
+ ### Lifecycle label contradicts native state — walk BOTH directions
564
+
565
+ A lifecycle label can lie in two ways, and historically only one of them had an owner:
566
+
567
+ | label | native state | owned by |
568
+ |---|---|---|
569
+ | terminal `done` role | still open / active | the `Build terminal-open` section above |
570
+ | non-terminal role (`ready`, `claimed`, `blocked`, or any other `status:` member) | closed / completed | **nothing, until now** |
571
+
572
+ A pass that walks one direction leaves the other accumulating silently. Measured on the Linear TUN
573
+ board: **TUN-556 and TUN-503 both carry a terminal `status:done` while still natively open in
574
+ `On Dev`** — invisible to intake and indistinguishable from handled, exactly like a bot-applied
575
+ `status:in-progress` on GitHub.
576
+
577
+ Resolve both directions in one pass with the shipped detector, which always reports
578
+ `directionsWalked` covering both, so a "clean" verdict is an assertion that both were examined.
579
+
580
+ **Build the classifier input inside this cycle. Never consume a temp file another skill wrote.**
581
+ repair-intake runs as its own cycle, so borrowing `lisa-github-build-intake`'s scratch file fails
582
+ two ways: absent, the redirect fails and `set -e` aborts the whole cycle; stale from an earlier run
583
+ on a *different* item, the resolver classifies that other item and this section reports `drifts: []`
584
+ while `directionsWalked` still claims both directions were walked. That is precisely the
585
+ silent-clean verdict this section promises cannot happen.
586
+
587
+ ```bash
588
+ TRUST_DIR=$(mktemp -d)
589
+ trap 'rm -rf "$TRUST_DIR"' EXIT
590
+
591
+ gh api "repos/<org>/<repo>/issues/<n>" > "$TRUST_DIR/issue.json"
592
+
593
+ # --paginate emits ONE ARRAY PER PAGE; --slurp + `add` flattens all of them so a
594
+ # label event on page two is not silently dropped.
595
+ gh api "repos/<org>/<repo>/issues/<n>/timeline?per_page=100" --paginate --slurp \
596
+ > "$TRUST_DIR/pages.json"
597
+ jq 'add // []' "$TRUST_DIR/pages.json" > "$TRUST_DIR/timeline.json"
598
+
599
+ # Same resolution `read_role` uses: local overrides global, both optional.
600
+ # Resolving a stale terminal `done` here would make the true terminal label look
601
+ # like `open-label-closed-state` — and that direction WRITES.
602
+ jq -s '(.[0] // {}) * (.[1] // {})' \
603
+ <(cat .lisa.config.json 2>/dev/null || echo '{}') \
604
+ <(cat .lisa.config.local.json 2>/dev/null || echo '{}') \
605
+ > "$TRUST_DIR/config.json"
606
+
607
+ jq -n --slurpfile i "$TRUST_DIR/issue.json" \
608
+ --slurpfile t "$TRUST_DIR/timeline.json" \
609
+ --slurpfile c "$TRUST_DIR/config.json" \
610
+ '{issue: $i[0], timeline: $t[0], config: $c[0]}' \
611
+ > "$TRUST_DIR/input.json"
612
+
613
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/lifecycle-label-trust.mjs" < "$TRUST_DIR/input.json"
614
+ ```
615
+
616
+ **Untrusted labels are excluded from `drifts` before any repair runs** — the classifier does this
617
+ itself, so repair must **never act on a label the classifier refused to believe**. Without it, a
618
+ bot-applied label on a natively closed item would be advanced to the terminal `done` role by the
619
+ `open-label-closed-state` repair, laundering the exact input the guard rejected into a real write.
620
+ The `excluded` array reports what was held back; surface it, do not repair it.
621
+
622
+ 1. **`terminal-label-open-state`** → hand to the `Build terminal-open → native close` section above.
623
+ Do not restate its rules; the terminality test lives there (intermediate env rungs like
624
+ `status:on-dev` / `status:on-stg` are **not** terminal).
625
+ 2. **`open-label-closed-state`** → the previously unowned direction. The item is natively closed or
626
+ completed while still wearing a non-terminal lifecycle role. Advance the label to the env-resolved
627
+ terminal `done` role and post one idempotent `[lisa-repair-intake]` note. This is a write on
628
+ **Lisa's own** lifecycle surface, not a contest with another writer.
629
+ Apply the leaf/container check from the **Lifecycle ownership guard** section *before* this
630
+ branch: repair-intake owns container repair, so a natively-closed `ready` **leaf** is skipped
631
+ here and left to the build lane rather than claimed by this pass.
632
+ 3. **Vendor caveat — Linear.** On Linear the lifecycle surface is the native workflow **state**, not a
633
+ `status:*` label (see `config-resolution`). A `status:*` label there is leftover cruft that no
634
+ repair direction reads, which is precisely why TUN-556 and TUN-503 rotted. Treat Linear's native
635
+ state as authoritative and remove the contradicting stale label; never move the state to match a
636
+ label.
637
+
638
+ Membership in the lifecycle namespace is decided by the **`status:` prefix**, never a pinned member
639
+ list — the live family has drifted 7 → 6 members, and a literal set fails silently by simply not
640
+ matching the member it stopped covering.
641
+
642
+ ### Bot-authored lifecycle label → distrust, never revert (read-only)
643
+
644
+ `status:*` labels written by a **bot** actor within the plausibility window of the item's creation are
645
+ not lifecycle signals — see `lisa-github-build-intake` Phase 2b for the measurement and the
646
+ classifier. Report them here so an operator can see how much of the board is wearing a label nobody
647
+ chose:
648
+
649
+ - list each item alongside the untrusted label, the bot login, and the latency, from the classifier's
650
+ `untrusted` array;
651
+ - list `unknownProvenance` labels separately — trusted, but unattributable;
652
+ - **do not unlabel and do not relabel.** The bot re-applies on each subsequent review event, so a
653
+ reverting reconciler produces a label-flap loop that is worse than the defect. Repair happens by
654
+ intake declining to *believe* the label, which is idempotent and writes nothing.
655
+
656
+ This class is deliberately **read-only** and never counts toward the repair cap's actionable work.
657
+
563
658
  ### Build parent rollup reconciliation (intermediate-env or terminal close-out)
564
659
 
565
660
  For each parent/container item (an Epic, a Linear Project, or any item — of any type — with open child work),