@codyswann/lisa 2.352.0 → 2.353.0

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 (78) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +9 -7
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/expo/create-only/.github/workflows/maestro-e2e.yml +10 -1
  5. package/expo/create-only/.github/workflows/nightly-e2e-health.yml +5 -0
  6. package/package.json +1 -1
  7. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  9. package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +34 -0
  10. package/plugins/lisa/.codex-plugin/skills/lisa-validate-tracker-mapping/SKILL.md +33 -4
  11. package/plugins/lisa/rules/reference/config-resolution.md +7 -0
  12. package/plugins/lisa/scripts/queue-contract-resolution.mjs +50 -1
  13. package/plugins/lisa/skills/lisa-linear-access/SKILL.md +34 -0
  14. package/plugins/lisa/skills/lisa-validate-tracker-mapping/SKILL.md +33 -4
  15. package/plugins/lisa-agy/plugin.json +1 -1
  16. package/plugins/lisa-agy/scripts/queue-contract-resolution.mjs +50 -1
  17. package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +34 -0
  18. package/plugins/lisa-agy/skills/lisa-validate-tracker-mapping/SKILL.md +33 -4
  19. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/rules/reference/config-resolution.md +7 -0
  26. package/plugins/lisa-copilot/scripts/queue-contract-resolution.mjs +50 -1
  27. package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +34 -0
  28. package/plugins/lisa-copilot/skills/lisa-validate-tracker-mapping/SKILL.md +33 -4
  29. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +7 -0
  31. package/plugins/lisa-cursor/scripts/queue-contract-resolution.mjs +50 -1
  32. package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +34 -0
  33. package/plugins/lisa-cursor/skills/lisa-validate-tracker-mapping/SKILL.md +33 -4
  34. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo-agy/plugin.json +1 -1
  37. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  57. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails-agy/plugin.json +1 -1
  62. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  67. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  72. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/src/base/rules/reference/config-resolution.md +7 -0
  75. package/plugins/src/base/scripts/queue-contract-resolution.mjs +50 -1
  76. package/plugins/src/base/skills/lisa-linear-access/SKILL.md +34 -0
  77. package/plugins/src/base/skills/lisa-validate-tracker-mapping/SKILL.md +33 -4
  78. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +76 -10
@@ -31,7 +31,16 @@ on:
31
31
  workflow_dispatch:
32
32
  inputs:
33
33
  platform:
34
- description: 'Platform(s) to run'
34
+ # NARROWING THIS DOES NOT CLEAR THE NIGHTLY GATE. Picking `android` or
35
+ # `ios` skips the other platform's job, and GitHub still concludes the
36
+ # run `success` — a skipped job does not redden its run. The
37
+ # nightly-e2e-health gate reads the jobs behind a `success` run for
38
+ # exactly this reason (truth-table row 26 in Lisa's
39
+ # `docs/nightly-e2e-gate.md`), so a narrowed run reports
40
+ # `incomplete_run` and keeps blocking. Use these options to iterate on
41
+ # one platform; dispatch with `all` when the point is to clear a red
42
+ # nightly.
43
+ description: 'Platform(s) to run — use `all` when dispatching to clear the nightly gate'
35
44
  type: choice
36
45
  default: all
37
46
  options:
@@ -102,6 +102,11 @@ jobs:
102
102
  # `{"mode":"job_pattern","pattern":"^…$"}` only for matrix job names that
103
103
  # no single string can cover.
104
104
  #
105
+ # `{"mode":"run"}` reads the run's conclusion AND every job behind it,
106
+ # because GitHub concludes a run `success` when jobs were skipped — a
107
+ # suite dispatched for one platform only would otherwise report green
108
+ # about the platform it never tested (row 26 / §2.4 of the contract).
109
+ #
105
110
  # ONLY `maestro-e2e.yml` by default, because that is the only nightly
106
111
  # suite this template actually ships. Naming a workflow file that does not
107
112
  # exist is truth-table row 11 — a HARD failure, not missing evidence —
package/package.json CHANGED
@@ -120,7 +120,7 @@
120
120
  }
121
121
  },
122
122
  "name": "@codyswann/lisa",
123
- "version": "2.352.0",
123
+ "version": "2.353.0",
124
124
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
125
125
  "main": "dist/index.js",
126
126
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.352.0",
3
+ "version": "2.353.0",
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": "2.352.0",
3
+ "version": "2.353.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -106,6 +106,40 @@ linear_graphql() {
106
106
  Map operation names to Linear GraphQL queries/mutations in this access skill.
107
107
  Consumers pass business-shaped arguments only; they do not embed GraphQL.
108
108
 
109
+ ## `list-workflow-states` — the team's states, and which one it creates into
110
+
111
+ `list-workflow-states team:<ID>` returns one node per state with `id`, `name`,
112
+ `type`, `position`, and **`isTeamDefault`**.
113
+
114
+ `isTeamDefault` is `true` for the single state named by the team's
115
+ `defaultIssueState` — where Linear puts every brand-new Issue. Callers need it to
116
+ enforce the rule that `linear.workflow.ready` must be a lane a human moves an
117
+ Issue **into**: pointing `ready` at the default inverts the gate, so the
118
+ claimable lane means "nobody has touched this" rather than "a human marked this
119
+ ready", and build-intake dispatches unapproved work. `/lisa:setup:linear` refuses
120
+ to resolve `ready` onto it, and `/lisa:validate-tracker-mapping` classifies a
121
+ config that already does as `INVERTED`.
122
+
123
+ It belongs on this operation rather than in a separate `get-team` call because
124
+ every caller that needs it is already enumerating states, and a second round trip
125
+ is a second chance for the two answers to disagree. A team with no
126
+ `defaultIssueState` set yields `isTeamDefault: false` on every node — report that
127
+ honestly; do not fall back to guessing by name or position.
128
+
129
+ ```graphql
130
+ query($teamId:String!){
131
+ team(id:$teamId){
132
+ defaultIssueState{ id }
133
+ states(first:100){ nodes{ id name type position } }
134
+ }
135
+ }
136
+ ```
137
+
138
+ Set `isTeamDefault` per node by comparing `node.id` against
139
+ `team.defaultIssueState.id`. On the MCP substrate, which exposes states without
140
+ the team's default, resolve the default through the team record and join on `id`
141
+ the same way.
142
+
109
143
  ## `history` — transition history (read-only)
110
144
 
111
145
  `history id:<ID>` returns an Issue's ordered past state changes — the raw
@@ -71,7 +71,8 @@ Resolve the **effective** role → name mapping using the same defaults `/lisa:i
71
71
  - **Missing / empty tracker**: report `UNRESOLVABLE` with setup guidance (`/lisa:setup:jira`, `/lisa:setup:github`, or `/lisa:setup:linear`). Do not default to JIRA.
72
72
  - **JIRA build workflow** (`jira.workflow`): `ready`, `claimed`, optional `review`, `blocked`, and each `done.<env>` (`dev` / `staging` / `production`). Defaults: `Ready`, `In Progress`, `Code Review`, `Blocked`, `{dev: "On Dev", staging: "On Stg", production: "Done"}`.
73
73
  - **GitHub build/prd labels** (`github.labels.build`, `github.labels.prd`): each configured label string.
74
- - **Linear build/prd labels** (`linear.labels.build`, `linear.labels.prd`): each configured label/state string.
74
+ - **Linear build workflow** (`linear.workflow`): `ready`, `claimed`, `review`, `blocked`, and each `done.<env>` — native workflow **states**, the Linear analogue of `jira.workflow`, not of `github.labels`. Defaults: `Ready`, `In Progress`, `In Review`, `Blocked`, `{dev: "On Dev", staging: "On Stg", production: "Done"}`.
75
+ - **Linear labels** (`linear.labels`): the `prd.*` map plus the one surviving build-lane key, the `human_needed` marker (`linear.labels.build.human_needed`). A config that predates the state model may still carry `ready` / `claimed` / `blocked` / `done` under `linear.labels.build`; those are **inert** — nothing reads them. Do not audit them against the live label set, and do not report them as drift. Report them once as a migration note pointing at `/lisa:setup:linear`, which removes them.
75
76
  - **Notion PRD values** (`notion.values`): each configured select-option value, validated against the `notion.statusProperty` property's options.
76
77
  - **Confluence PRD parents** (`confluence.parents`): each configured parent page id, validated by existence.
77
78
 
@@ -103,7 +104,9 @@ gh label list --repo "$REPO" --limit 200 --json name -q '.[].name'
103
104
 
104
105
  ### Linear / Notion / Confluence
105
106
 
106
- Enumerate via the corresponding access surface (Linear MCP workflow states + labels; Notion data-source select options for `notion.statusProperty`; Confluence page-exists check per parent id). Same compare-exact-case contract as JIRA.
107
+ Enumerate via the corresponding access surface (`lisa-linear-access` workflow states + labels; Notion data-source select options for `notion.statusProperty`; Confluence page-exists check per parent id). Same compare-exact-case contract as JIRA.
108
+
109
+ For Linear, `lisa-linear-access operation: list-workflow-states` returns each state's `name`, `type`, `position` and `isTeamDefault` (the access layer sets it from the team's `defaultIssueState`). **Keep `isTeamDefault`** — Step 4 needs it, and it is the only authoritative answer to "which state does this team create Issues into". Do not approximate it with a name guess: `Todo` is merely the stock name, and a team that renamed its `defaultIssueState` is exactly the case a name guess misses.
107
110
 
108
111
  ## Step 4 — Compare (exact case)
109
112
 
@@ -113,10 +116,14 @@ For each `(role, configured-name)` pair, classify against the live name set:
113
116
  - **CASE_DRIFT** — a case-insensitive match exists but no exact-case match (e.g. config `"On Stg"` vs live `"ON STG"`). Canonical = the live exact name.
114
117
  - **MISSING** — no case-insensitive match exists. The name was renamed beyond recognition or deleted.
115
118
 
119
+ One role carries a further check that name-existence cannot express:
120
+
121
+ - **INVERTED** — Linear only, `ready` only: the configured state exists, but it is the team's **default created state** (`isTeamDefault`). This is worse than a name that does not resolve. The name resolves perfectly, so every existence check passes while the gate runs backwards: `ready` is supposed to mean "a human moved this Issue here", and the team's default means "nobody has touched this". Build-intake claims from that lane, so an INVERTED mapping dispatches work no human ever approved. Measured on the first team it hit: 20 Issues in the claimable lane, 12 never marked ready — including decision tickets shaped like leaves, which the leaf-only gate cannot catch either. Report it even when every other role is VALID.
122
+
116
123
  A project's verdict:
117
124
 
118
125
  - **VALID** — every role is VALID.
119
- - **DRIFTED** — at least one CASE_DRIFT or MISSING role, none of which is UNRESOLVABLE.
126
+ - **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.
120
127
  - **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.
121
128
 
122
129
  ## Step 5 — Report
@@ -134,7 +141,17 @@ Per project, print a terminal-first section:
134
141
  done.production Done Done VALID
135
142
  ```
136
143
 
137
- 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).
144
+ An INVERTED `ready` gets a full line rather than a one-word status, because the operator reading it is not necessarily an engineer and the word alone does not convey the stakes:
145
+
146
+ ```
147
+ ready Todo Todo INVERTED
148
+ "Todo" is the state this Linear team puts every NEW issue into, so the
149
+ build queue is currently claiming issues nobody marked ready. Pick or
150
+ create a state a person moves an issue into (Lisa's default is "Ready")
151
+ via /lisa:setup:linear, then set linear.workflow.ready to it.
152
+ ```
153
+
154
+ 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).
138
155
 
139
156
  ## Step 6 — Repair (only when `repair=true`)
140
157
 
@@ -166,6 +183,18 @@ Compute the closest live candidates (case-insensitive token/substring overlap, t
166
183
 
167
184
  Only write on an explicit pick. Never auto-select. If the user leaves it unchanged, keep the project `DRIFTED` and surface the admin remediation (add the status back, or fix it in the tracker).
168
185
 
186
+ ### INVERTED — never auto-repair
187
+
188
+ **Never auto-repair an INVERTED `ready`, even with `repair=true`.** Every other classification has one correct answer that the live tracker already knows: CASE_DRIFT has the canonical casing, MISSING has a shortlist of near-matches. INVERTED has neither. The configured name is live and correctly cased; what is wrong is which lane the project chose to mean "build-ready", and nothing in the config or the tracker records what the human intended instead. Guessing would silently repoint the queue at a lane that may hold nothing, or worse, at another lane the team fills automatically — swapping one wrong answer for a quieter one.
189
+
190
+ The team may also genuinely not have a dedicated ready lane yet, in which case the repair is to **create a state**, not to rewrite a string — `/lisa:validate-tracker-mapping` audits config, it does not mutate the tracker.
191
+
192
+ So: present the team's non-default states via `AskUserQuestion`, and write `linear.workflow.ready` only on an explicit pick. If none fits, or the user declines, leave the config untouched, keep the project `DRIFTED`, and hand off:
193
+
194
+ > Linear has no dedicated build-ready state on this team. Run `/lisa:setup:linear` — it offers to create `Ready` and records the mapping.
195
+
196
+ Until then, say plainly that the build queue is claiming unapproved work and that pausing build intake is the safe interim.
197
+
169
198
  ### Invalidate the verification cache
170
199
 
171
200
  After any JIRA repair, clear the `setup-jira` reachability cache so it re-verifies the new mapping:
@@ -449,6 +449,13 @@ Every lifecycle skill operates on a fixed set of **roles** (`ready`, `claimed`,
449
449
 
450
450
  **`ready` must never resolve to the team's DEFAULT state.** `Todo` is where Linear puts a brand-new issue, so using it for `ready` inverts the gate: the lane stops meaning "a human flipped this to build-ready" and starts meaning "nobody has touched this". Measured on the first team migrated: 20 issues in the lane, only 8 ever explicitly marked ready. JIRA avoids this because `jira.workflow.ready` is a dedicated `Ready` status while a fresh ticket lands in the project default.
451
451
 
452
+ **That rule is enforced, not merely stated — a correct default is only half the fix.** The default is `Ready`, but any project can override `linear.workflow.ready`, and an override reproduces the inversion exactly. Two arms catch it, deliberately split by what each can see:
453
+
454
+ - **Static, always-on.** The queue-contract resolver refuses a `ready` naming a stock default created state (`Todo`, `To Do`, `Backlog`, `Triage`) and throws rather than resolving. It needs no network, so it runs everywhere, including offline and in CI. It cannot see a team that renamed its default.
455
+ - **Live, authoritative.** `/lisa:validate-tracker-mapping` compares the configured `ready` against the team's real `defaultIssueState` (surfaced as `isTeamDefault` on `lisa-linear-access operation: list-workflow-states`) and classifies a match as `INVERTED` — never `VALID`, and never auto-repaired, because nothing records what lane the human meant instead.
456
+
457
+ `INVERTED` is not a name-resolution failure; it is the opposite. The name resolves perfectly, which is precisely why every existence check passes while the gate runs backwards.
458
+
452
459
  **Linear state resolution is `type`-aware.** When a configured name is missing, a lifecycle skill may fall back to the team's states by `type` — `claimed`/`review` → the lowest-position `started`, `blocked` → `started` or `unstarted`, terminal `done` → `completed` — but only to *read*. **`ready` has no fallback on purpose:** every candidate would be the team's default unstarted state, which is exactly the inversion described above. A missing `ready` state is reported, never guessed; it must never invent a state to write into. Missing states are a setup defect, repaired by `/lisa:setup:linear`, not papered over at runtime.
453
460
 
454
461
  `blocked` is what every vendor agent flips to when triage finds unresolved ambiguities or the build path is blocked by something the agent can't resolve. Different from `claimed` because it explicitly signals "human attention required."
@@ -32,6 +32,24 @@ const DEFAULT_LINEAR_BUILD_DONE = {
32
32
  production: "Done",
33
33
  };
34
34
 
35
+ // The state names a stock Linear team creates brand-new Issues into. `ready`
36
+ // must name a lane a human moves an Issue INTO, so it may never be one of
37
+ // these: the claimable lane would stop meaning "a human flipped this to
38
+ // build-ready" and start meaning "nobody has touched this", and build-intake
39
+ // would dispatch work nobody approved.
40
+ //
41
+ // This list is a STATIC backstop for the stock names, which is the case that
42
+ // actually bit us. It cannot see a team that renamed its default — only the
43
+ // live `Team.defaultIssueState` can, which is why `/lisa:validate-tracker-mapping`
44
+ // carries the authoritative arm (`INVERTED`) and this one carries the arm that
45
+ // needs no network and therefore always runs.
46
+ const LINEAR_STOCK_DEFAULT_CREATED_STATES = [
47
+ "todo",
48
+ "to do",
49
+ "backlog",
50
+ "triage",
51
+ ];
52
+
35
53
  const DEFAULT_GITHUB_LINEAR_PRD_ROLES = {
36
54
  draft: "prd-draft",
37
55
  ready: "prd-ready",
@@ -272,6 +290,35 @@ export function resolvePrdLifecycleRoles(
272
290
  }
273
291
  }
274
292
 
293
+ /**
294
+ * Reject a Linear `ready` override that names a state a stock team creates
295
+ * brand-new Issues into.
296
+ *
297
+ * Failing loudly is the safe direction. An operator staring at a hard error has
298
+ * a broken queue; an operator with a silently inverted gate has agents shipping
299
+ * work no human approved, which is the failure mode that produced this guard.
300
+ *
301
+ * @param {string} configured The resolved `linear.workflow.ready` name.
302
+ * @returns {string} The same name, once it is proven not to be a default lane.
303
+ * @throws {Error} When the name is a stock default created state.
304
+ */
305
+ function assertLinearReadyIsNotDefaultState(configured) {
306
+ const normalized = configured.trim().toLowerCase();
307
+
308
+ if (!LINEAR_STOCK_DEFAULT_CREATED_STATES.includes(normalized)) {
309
+ return configured;
310
+ }
311
+
312
+ throw new Error(
313
+ `linear.workflow.ready is set to "${configured.trim()}", which is where Linear puts a ` +
314
+ "brand-new Issue. That inverts the build-ready gate: the claimable lane would mean " +
315
+ '"nobody has touched this" instead of "a human marked this ready", so intake would ' +
316
+ "claim untouched backlog items. Point linear.workflow.ready at a dedicated state a " +
317
+ "human moves Issues into (the default is `Ready`), then re-run /lisa:setup:linear to " +
318
+ "create it if the team does not have one."
319
+ );
320
+ }
321
+
275
322
  /**
276
323
  * Resolve the build lifecycle roles for the configured tracker vendor.
277
324
  *
@@ -307,7 +354,9 @@ export function resolveBuildLifecycleRoles(
307
354
  vendor: "linear",
308
355
  kind: "workflow",
309
356
  roles: {
310
- ready: config.linear?.workflow?.ready || "Ready",
357
+ ready: assertLinearReadyIsNotDefaultState(
358
+ config.linear?.workflow?.ready || "Ready"
359
+ ),
311
360
  claimed: config.linear?.workflow?.claimed || "In Progress",
312
361
  review: config.linear?.workflow?.review || "In Review",
313
362
  blocked: config.linear?.workflow?.blocked || "Blocked",
@@ -106,6 +106,40 @@ linear_graphql() {
106
106
  Map operation names to Linear GraphQL queries/mutations in this access skill.
107
107
  Consumers pass business-shaped arguments only; they do not embed GraphQL.
108
108
 
109
+ ## `list-workflow-states` — the team's states, and which one it creates into
110
+
111
+ `list-workflow-states team:<ID>` returns one node per state with `id`, `name`,
112
+ `type`, `position`, and **`isTeamDefault`**.
113
+
114
+ `isTeamDefault` is `true` for the single state named by the team's
115
+ `defaultIssueState` — where Linear puts every brand-new Issue. Callers need it to
116
+ enforce the rule that `linear.workflow.ready` must be a lane a human moves an
117
+ Issue **into**: pointing `ready` at the default inverts the gate, so the
118
+ claimable lane means "nobody has touched this" rather than "a human marked this
119
+ ready", and build-intake dispatches unapproved work. `/lisa:setup:linear` refuses
120
+ to resolve `ready` onto it, and `/lisa:validate-tracker-mapping` classifies a
121
+ config that already does as `INVERTED`.
122
+
123
+ It belongs on this operation rather than in a separate `get-team` call because
124
+ every caller that needs it is already enumerating states, and a second round trip
125
+ is a second chance for the two answers to disagree. A team with no
126
+ `defaultIssueState` set yields `isTeamDefault: false` on every node — report that
127
+ honestly; do not fall back to guessing by name or position.
128
+
129
+ ```graphql
130
+ query($teamId:String!){
131
+ team(id:$teamId){
132
+ defaultIssueState{ id }
133
+ states(first:100){ nodes{ id name type position } }
134
+ }
135
+ }
136
+ ```
137
+
138
+ Set `isTeamDefault` per node by comparing `node.id` against
139
+ `team.defaultIssueState.id`. On the MCP substrate, which exposes states without
140
+ the team's default, resolve the default through the team record and join on `id`
141
+ the same way.
142
+
109
143
  ## `history` — transition history (read-only)
110
144
 
111
145
  `history id:<ID>` returns an Issue's ordered past state changes — the raw
@@ -71,7 +71,8 @@ Resolve the **effective** role → name mapping using the same defaults `/lisa:i
71
71
  - **Missing / empty tracker**: report `UNRESOLVABLE` with setup guidance (`/lisa:setup:jira`, `/lisa:setup:github`, or `/lisa:setup:linear`). Do not default to JIRA.
72
72
  - **JIRA build workflow** (`jira.workflow`): `ready`, `claimed`, optional `review`, `blocked`, and each `done.<env>` (`dev` / `staging` / `production`). Defaults: `Ready`, `In Progress`, `Code Review`, `Blocked`, `{dev: "On Dev", staging: "On Stg", production: "Done"}`.
73
73
  - **GitHub build/prd labels** (`github.labels.build`, `github.labels.prd`): each configured label string.
74
- - **Linear build/prd labels** (`linear.labels.build`, `linear.labels.prd`): each configured label/state string.
74
+ - **Linear build workflow** (`linear.workflow`): `ready`, `claimed`, `review`, `blocked`, and each `done.<env>` — native workflow **states**, the Linear analogue of `jira.workflow`, not of `github.labels`. Defaults: `Ready`, `In Progress`, `In Review`, `Blocked`, `{dev: "On Dev", staging: "On Stg", production: "Done"}`.
75
+ - **Linear labels** (`linear.labels`): the `prd.*` map plus the one surviving build-lane key, the `human_needed` marker (`linear.labels.build.human_needed`). A config that predates the state model may still carry `ready` / `claimed` / `blocked` / `done` under `linear.labels.build`; those are **inert** — nothing reads them. Do not audit them against the live label set, and do not report them as drift. Report them once as a migration note pointing at `/lisa:setup:linear`, which removes them.
75
76
  - **Notion PRD values** (`notion.values`): each configured select-option value, validated against the `notion.statusProperty` property's options.
76
77
  - **Confluence PRD parents** (`confluence.parents`): each configured parent page id, validated by existence.
77
78
 
@@ -103,7 +104,9 @@ gh label list --repo "$REPO" --limit 200 --json name -q '.[].name'
103
104
 
104
105
  ### Linear / Notion / Confluence
105
106
 
106
- Enumerate via the corresponding access surface (Linear MCP workflow states + labels; Notion data-source select options for `notion.statusProperty`; Confluence page-exists check per parent id). Same compare-exact-case contract as JIRA.
107
+ Enumerate via the corresponding access surface (`lisa-linear-access` workflow states + labels; Notion data-source select options for `notion.statusProperty`; Confluence page-exists check per parent id). Same compare-exact-case contract as JIRA.
108
+
109
+ For Linear, `lisa-linear-access operation: list-workflow-states` returns each state's `name`, `type`, `position` and `isTeamDefault` (the access layer sets it from the team's `defaultIssueState`). **Keep `isTeamDefault`** — Step 4 needs it, and it is the only authoritative answer to "which state does this team create Issues into". Do not approximate it with a name guess: `Todo` is merely the stock name, and a team that renamed its `defaultIssueState` is exactly the case a name guess misses.
107
110
 
108
111
  ## Step 4 — Compare (exact case)
109
112
 
@@ -113,10 +116,14 @@ For each `(role, configured-name)` pair, classify against the live name set:
113
116
  - **CASE_DRIFT** — a case-insensitive match exists but no exact-case match (e.g. config `"On Stg"` vs live `"ON STG"`). Canonical = the live exact name.
114
117
  - **MISSING** — no case-insensitive match exists. The name was renamed beyond recognition or deleted.
115
118
 
119
+ One role carries a further check that name-existence cannot express:
120
+
121
+ - **INVERTED** — Linear only, `ready` only: the configured state exists, but it is the team's **default created state** (`isTeamDefault`). This is worse than a name that does not resolve. The name resolves perfectly, so every existence check passes while the gate runs backwards: `ready` is supposed to mean "a human moved this Issue here", and the team's default means "nobody has touched this". Build-intake claims from that lane, so an INVERTED mapping dispatches work no human ever approved. Measured on the first team it hit: 20 Issues in the claimable lane, 12 never marked ready — including decision tickets shaped like leaves, which the leaf-only gate cannot catch either. Report it even when every other role is VALID.
122
+
116
123
  A project's verdict:
117
124
 
118
125
  - **VALID** — every role is VALID.
119
- - **DRIFTED** — at least one CASE_DRIFT or MISSING role, none of which is UNRESOLVABLE.
126
+ - **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.
120
127
  - **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.
121
128
 
122
129
  ## Step 5 — Report
@@ -134,7 +141,17 @@ Per project, print a terminal-first section:
134
141
  done.production Done Done VALID
135
142
  ```
136
143
 
137
- 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).
144
+ An INVERTED `ready` gets a full line rather than a one-word status, because the operator reading it is not necessarily an engineer and the word alone does not convey the stakes:
145
+
146
+ ```
147
+ ready Todo Todo INVERTED
148
+ "Todo" is the state this Linear team puts every NEW issue into, so the
149
+ build queue is currently claiming issues nobody marked ready. Pick or
150
+ create a state a person moves an issue into (Lisa's default is "Ready")
151
+ via /lisa:setup:linear, then set linear.workflow.ready to it.
152
+ ```
153
+
154
+ 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).
138
155
 
139
156
  ## Step 6 — Repair (only when `repair=true`)
140
157
 
@@ -166,6 +183,18 @@ Compute the closest live candidates (case-insensitive token/substring overlap, t
166
183
 
167
184
  Only write on an explicit pick. Never auto-select. If the user leaves it unchanged, keep the project `DRIFTED` and surface the admin remediation (add the status back, or fix it in the tracker).
168
185
 
186
+ ### INVERTED — never auto-repair
187
+
188
+ **Never auto-repair an INVERTED `ready`, even with `repair=true`.** Every other classification has one correct answer that the live tracker already knows: CASE_DRIFT has the canonical casing, MISSING has a shortlist of near-matches. INVERTED has neither. The configured name is live and correctly cased; what is wrong is which lane the project chose to mean "build-ready", and nothing in the config or the tracker records what the human intended instead. Guessing would silently repoint the queue at a lane that may hold nothing, or worse, at another lane the team fills automatically — swapping one wrong answer for a quieter one.
189
+
190
+ The team may also genuinely not have a dedicated ready lane yet, in which case the repair is to **create a state**, not to rewrite a string — `/lisa:validate-tracker-mapping` audits config, it does not mutate the tracker.
191
+
192
+ So: present the team's non-default states via `AskUserQuestion`, and write `linear.workflow.ready` only on an explicit pick. If none fits, or the user declines, leave the config untouched, keep the project `DRIFTED`, and hand off:
193
+
194
+ > Linear has no dedicated build-ready state on this team. Run `/lisa:setup:linear` — it offers to create `Ready` and records the mapping.
195
+
196
+ Until then, say plainly that the build queue is claiming unapproved work and that pausing build intake is the safe interim.
197
+
169
198
  ### Invalidate the verification cache
170
199
 
171
200
  After any JIRA repair, clear the `setup-jira` reachability cache so it re-verifies the new mapping:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.352.0",
3
+ "version": "2.353.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -32,6 +32,24 @@ const DEFAULT_LINEAR_BUILD_DONE = {
32
32
  production: "Done",
33
33
  };
34
34
 
35
+ // The state names a stock Linear team creates brand-new Issues into. `ready`
36
+ // must name a lane a human moves an Issue INTO, so it may never be one of
37
+ // these: the claimable lane would stop meaning "a human flipped this to
38
+ // build-ready" and start meaning "nobody has touched this", and build-intake
39
+ // would dispatch work nobody approved.
40
+ //
41
+ // This list is a STATIC backstop for the stock names, which is the case that
42
+ // actually bit us. It cannot see a team that renamed its default — only the
43
+ // live `Team.defaultIssueState` can, which is why `/lisa:validate-tracker-mapping`
44
+ // carries the authoritative arm (`INVERTED`) and this one carries the arm that
45
+ // needs no network and therefore always runs.
46
+ const LINEAR_STOCK_DEFAULT_CREATED_STATES = [
47
+ "todo",
48
+ "to do",
49
+ "backlog",
50
+ "triage",
51
+ ];
52
+
35
53
  const DEFAULT_GITHUB_LINEAR_PRD_ROLES = {
36
54
  draft: "prd-draft",
37
55
  ready: "prd-ready",
@@ -272,6 +290,35 @@ export function resolvePrdLifecycleRoles(
272
290
  }
273
291
  }
274
292
 
293
+ /**
294
+ * Reject a Linear `ready` override that names a state a stock team creates
295
+ * brand-new Issues into.
296
+ *
297
+ * Failing loudly is the safe direction. An operator staring at a hard error has
298
+ * a broken queue; an operator with a silently inverted gate has agents shipping
299
+ * work no human approved, which is the failure mode that produced this guard.
300
+ *
301
+ * @param {string} configured The resolved `linear.workflow.ready` name.
302
+ * @returns {string} The same name, once it is proven not to be a default lane.
303
+ * @throws {Error} When the name is a stock default created state.
304
+ */
305
+ function assertLinearReadyIsNotDefaultState(configured) {
306
+ const normalized = configured.trim().toLowerCase();
307
+
308
+ if (!LINEAR_STOCK_DEFAULT_CREATED_STATES.includes(normalized)) {
309
+ return configured;
310
+ }
311
+
312
+ throw new Error(
313
+ `linear.workflow.ready is set to "${configured.trim()}", which is where Linear puts a ` +
314
+ "brand-new Issue. That inverts the build-ready gate: the claimable lane would mean " +
315
+ '"nobody has touched this" instead of "a human marked this ready", so intake would ' +
316
+ "claim untouched backlog items. Point linear.workflow.ready at a dedicated state a " +
317
+ "human moves Issues into (the default is `Ready`), then re-run /lisa:setup:linear to " +
318
+ "create it if the team does not have one."
319
+ );
320
+ }
321
+
275
322
  /**
276
323
  * Resolve the build lifecycle roles for the configured tracker vendor.
277
324
  *
@@ -307,7 +354,9 @@ export function resolveBuildLifecycleRoles(
307
354
  vendor: "linear",
308
355
  kind: "workflow",
309
356
  roles: {
310
- ready: config.linear?.workflow?.ready || "Ready",
357
+ ready: assertLinearReadyIsNotDefaultState(
358
+ config.linear?.workflow?.ready || "Ready"
359
+ ),
311
360
  claimed: config.linear?.workflow?.claimed || "In Progress",
312
361
  review: config.linear?.workflow?.review || "In Review",
313
362
  blocked: config.linear?.workflow?.blocked || "Blocked",
@@ -106,6 +106,40 @@ linear_graphql() {
106
106
  Map operation names to Linear GraphQL queries/mutations in this access skill.
107
107
  Consumers pass business-shaped arguments only; they do not embed GraphQL.
108
108
 
109
+ ## `list-workflow-states` — the team's states, and which one it creates into
110
+
111
+ `list-workflow-states team:<ID>` returns one node per state with `id`, `name`,
112
+ `type`, `position`, and **`isTeamDefault`**.
113
+
114
+ `isTeamDefault` is `true` for the single state named by the team's
115
+ `defaultIssueState` — where Linear puts every brand-new Issue. Callers need it to
116
+ enforce the rule that `linear.workflow.ready` must be a lane a human moves an
117
+ Issue **into**: pointing `ready` at the default inverts the gate, so the
118
+ claimable lane means "nobody has touched this" rather than "a human marked this
119
+ ready", and build-intake dispatches unapproved work. `/lisa:setup:linear` refuses
120
+ to resolve `ready` onto it, and `/lisa:validate-tracker-mapping` classifies a
121
+ config that already does as `INVERTED`.
122
+
123
+ It belongs on this operation rather than in a separate `get-team` call because
124
+ every caller that needs it is already enumerating states, and a second round trip
125
+ is a second chance for the two answers to disagree. A team with no
126
+ `defaultIssueState` set yields `isTeamDefault: false` on every node — report that
127
+ honestly; do not fall back to guessing by name or position.
128
+
129
+ ```graphql
130
+ query($teamId:String!){
131
+ team(id:$teamId){
132
+ defaultIssueState{ id }
133
+ states(first:100){ nodes{ id name type position } }
134
+ }
135
+ }
136
+ ```
137
+
138
+ Set `isTeamDefault` per node by comparing `node.id` against
139
+ `team.defaultIssueState.id`. On the MCP substrate, which exposes states without
140
+ the team's default, resolve the default through the team record and join on `id`
141
+ the same way.
142
+
109
143
  ## `history` — transition history (read-only)
110
144
 
111
145
  `history id:<ID>` returns an Issue's ordered past state changes — the raw
@@ -71,7 +71,8 @@ Resolve the **effective** role → name mapping using the same defaults `/lisa:i
71
71
  - **Missing / empty tracker**: report `UNRESOLVABLE` with setup guidance (`/lisa:setup:jira`, `/lisa:setup:github`, or `/lisa:setup:linear`). Do not default to JIRA.
72
72
  - **JIRA build workflow** (`jira.workflow`): `ready`, `claimed`, optional `review`, `blocked`, and each `done.<env>` (`dev` / `staging` / `production`). Defaults: `Ready`, `In Progress`, `Code Review`, `Blocked`, `{dev: "On Dev", staging: "On Stg", production: "Done"}`.
73
73
  - **GitHub build/prd labels** (`github.labels.build`, `github.labels.prd`): each configured label string.
74
- - **Linear build/prd labels** (`linear.labels.build`, `linear.labels.prd`): each configured label/state string.
74
+ - **Linear build workflow** (`linear.workflow`): `ready`, `claimed`, `review`, `blocked`, and each `done.<env>` — native workflow **states**, the Linear analogue of `jira.workflow`, not of `github.labels`. Defaults: `Ready`, `In Progress`, `In Review`, `Blocked`, `{dev: "On Dev", staging: "On Stg", production: "Done"}`.
75
+ - **Linear labels** (`linear.labels`): the `prd.*` map plus the one surviving build-lane key, the `human_needed` marker (`linear.labels.build.human_needed`). A config that predates the state model may still carry `ready` / `claimed` / `blocked` / `done` under `linear.labels.build`; those are **inert** — nothing reads them. Do not audit them against the live label set, and do not report them as drift. Report them once as a migration note pointing at `/lisa:setup:linear`, which removes them.
75
76
  - **Notion PRD values** (`notion.values`): each configured select-option value, validated against the `notion.statusProperty` property's options.
76
77
  - **Confluence PRD parents** (`confluence.parents`): each configured parent page id, validated by existence.
77
78
 
@@ -103,7 +104,9 @@ gh label list --repo "$REPO" --limit 200 --json name -q '.[].name'
103
104
 
104
105
  ### Linear / Notion / Confluence
105
106
 
106
- Enumerate via the corresponding access surface (Linear MCP workflow states + labels; Notion data-source select options for `notion.statusProperty`; Confluence page-exists check per parent id). Same compare-exact-case contract as JIRA.
107
+ Enumerate via the corresponding access surface (`lisa-linear-access` workflow states + labels; Notion data-source select options for `notion.statusProperty`; Confluence page-exists check per parent id). Same compare-exact-case contract as JIRA.
108
+
109
+ For Linear, `lisa-linear-access operation: list-workflow-states` returns each state's `name`, `type`, `position` and `isTeamDefault` (the access layer sets it from the team's `defaultIssueState`). **Keep `isTeamDefault`** — Step 4 needs it, and it is the only authoritative answer to "which state does this team create Issues into". Do not approximate it with a name guess: `Todo` is merely the stock name, and a team that renamed its `defaultIssueState` is exactly the case a name guess misses.
107
110
 
108
111
  ## Step 4 — Compare (exact case)
109
112
 
@@ -113,10 +116,14 @@ For each `(role, configured-name)` pair, classify against the live name set:
113
116
  - **CASE_DRIFT** — a case-insensitive match exists but no exact-case match (e.g. config `"On Stg"` vs live `"ON STG"`). Canonical = the live exact name.
114
117
  - **MISSING** — no case-insensitive match exists. The name was renamed beyond recognition or deleted.
115
118
 
119
+ One role carries a further check that name-existence cannot express:
120
+
121
+ - **INVERTED** — Linear only, `ready` only: the configured state exists, but it is the team's **default created state** (`isTeamDefault`). This is worse than a name that does not resolve. The name resolves perfectly, so every existence check passes while the gate runs backwards: `ready` is supposed to mean "a human moved this Issue here", and the team's default means "nobody has touched this". Build-intake claims from that lane, so an INVERTED mapping dispatches work no human ever approved. Measured on the first team it hit: 20 Issues in the claimable lane, 12 never marked ready — including decision tickets shaped like leaves, which the leaf-only gate cannot catch either. Report it even when every other role is VALID.
122
+
116
123
  A project's verdict:
117
124
 
118
125
  - **VALID** — every role is VALID.
119
- - **DRIFTED** — at least one CASE_DRIFT or MISSING role, none of which is UNRESOLVABLE.
126
+ - **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.
120
127
  - **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.
121
128
 
122
129
  ## Step 5 — Report
@@ -134,7 +141,17 @@ Per project, print a terminal-first section:
134
141
  done.production Done Done VALID
135
142
  ```
136
143
 
137
- 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).
144
+ An INVERTED `ready` gets a full line rather than a one-word status, because the operator reading it is not necessarily an engineer and the word alone does not convey the stakes:
145
+
146
+ ```
147
+ ready Todo Todo INVERTED
148
+ "Todo" is the state this Linear team puts every NEW issue into, so the
149
+ build queue is currently claiming issues nobody marked ready. Pick or
150
+ create a state a person moves an issue into (Lisa's default is "Ready")
151
+ via /lisa:setup:linear, then set linear.workflow.ready to it.
152
+ ```
153
+
154
+ 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).
138
155
 
139
156
  ## Step 6 — Repair (only when `repair=true`)
140
157
 
@@ -166,6 +183,18 @@ Compute the closest live candidates (case-insensitive token/substring overlap, t
166
183
 
167
184
  Only write on an explicit pick. Never auto-select. If the user leaves it unchanged, keep the project `DRIFTED` and surface the admin remediation (add the status back, or fix it in the tracker).
168
185
 
186
+ ### INVERTED — never auto-repair
187
+
188
+ **Never auto-repair an INVERTED `ready`, even with `repair=true`.** Every other classification has one correct answer that the live tracker already knows: CASE_DRIFT has the canonical casing, MISSING has a shortlist of near-matches. INVERTED has neither. The configured name is live and correctly cased; what is wrong is which lane the project chose to mean "build-ready", and nothing in the config or the tracker records what the human intended instead. Guessing would silently repoint the queue at a lane that may hold nothing, or worse, at another lane the team fills automatically — swapping one wrong answer for a quieter one.
189
+
190
+ The team may also genuinely not have a dedicated ready lane yet, in which case the repair is to **create a state**, not to rewrite a string — `/lisa:validate-tracker-mapping` audits config, it does not mutate the tracker.
191
+
192
+ So: present the team's non-default states via `AskUserQuestion`, and write `linear.workflow.ready` only on an explicit pick. If none fits, or the user declines, leave the config untouched, keep the project `DRIFTED`, and hand off:
193
+
194
+ > Linear has no dedicated build-ready state on this team. Run `/lisa:setup:linear` — it offers to create `Ready` and records the mapping.
195
+
196
+ Until then, say plainly that the build queue is claiming unapproved work and that pausing build intake is the safe interim.
197
+
169
198
  ### Invalidate the verification cache
170
199
 
171
200
  After any JIRA repair, clear the `setup-jira` reachability cache so it re-verifies the new mapping:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.352.0",
3
+ "version": "2.353.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.352.0",
3
+ "version": "2.353.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.352.0",
3
+ "version": "2.353.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.352.0",
3
+ "version": "2.353.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"