@codyswann/lisa 3.0.0 → 3.1.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 (116) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +22 -11
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/expo/create-only/.github/workflows/nightly-e2e-report.yml +71 -0
  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-atlassian-access/SKILL.md +75 -64
  9. package/plugins/lisa/.codex-plugin/skills/lisa-jam-access/SKILL.md +13 -5
  10. package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +30 -10
  11. package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +36 -23
  12. package/plugins/lisa/.codex-plugin/skills/lisa-posthog-access/SKILL.md +16 -6
  13. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +8 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-sentry-access/SKILL.md +16 -6
  15. package/plugins/lisa/.codex-plugin/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  16. package/plugins/lisa/rules/eager/integration-access-layer.md +7 -3
  17. package/plugins/lisa/rules/reference/credential-substrate-precedence.md +166 -0
  18. package/plugins/lisa/rules/reference/integration-access-layer.md +27 -15
  19. package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +76 -65
  20. package/plugins/lisa/skills/lisa-jam-access/SKILL.md +14 -6
  21. package/plugins/lisa/skills/lisa-linear-access/SKILL.md +31 -11
  22. package/plugins/lisa/skills/lisa-notion-access/SKILL.md +37 -24
  23. package/plugins/lisa/skills/lisa-posthog-access/SKILL.md +17 -7
  24. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +8 -0
  25. package/plugins/lisa/skills/lisa-sentry-access/SKILL.md +17 -7
  26. package/plugins/lisa/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  27. package/plugins/lisa-agy/plugin.json +1 -1
  28. package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +76 -65
  29. package/plugins/lisa-agy/skills/lisa-jam-access/SKILL.md +14 -6
  30. package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +31 -11
  31. package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +37 -24
  32. package/plugins/lisa-agy/skills/lisa-posthog-access/SKILL.md +17 -7
  33. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +8 -0
  34. package/plugins/lisa-agy/skills/lisa-sentry-access/SKILL.md +17 -7
  35. package/plugins/lisa-agy/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  36. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  39. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-copilot/rules/eager/integration-access-layer.md +7 -3
  43. package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +166 -0
  44. package/plugins/lisa-copilot/rules/reference/integration-access-layer.md +27 -15
  45. package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +76 -65
  46. package/plugins/lisa-copilot/skills/lisa-jam-access/SKILL.md +14 -6
  47. package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +31 -11
  48. package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +37 -24
  49. package/plugins/lisa-copilot/skills/lisa-posthog-access/SKILL.md +17 -7
  50. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +8 -0
  51. package/plugins/lisa-copilot/skills/lisa-sentry-access/SKILL.md +17 -7
  52. package/plugins/lisa-copilot/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  53. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +171 -0
  55. package/plugins/lisa-cursor/rules/integration-access-layer-reference.mdc +27 -15
  56. package/plugins/lisa-cursor/rules/integration-access-layer.mdc +7 -3
  57. package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +76 -65
  58. package/plugins/lisa-cursor/skills/lisa-jam-access/SKILL.md +14 -6
  59. package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +31 -11
  60. package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +37 -24
  61. package/plugins/lisa-cursor/skills/lisa-posthog-access/SKILL.md +17 -7
  62. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +8 -0
  63. package/plugins/lisa-cursor/skills/lisa-sentry-access/SKILL.md +17 -7
  64. package/plugins/lisa-cursor/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  65. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-expo-agy/plugin.json +1 -1
  68. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  73. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  78. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  83. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  88. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-rails-agy/plugin.json +1 -1
  93. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  97. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  98. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  102. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  103. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  105. package/plugins/src/base/rules/eager/integration-access-layer.md +7 -3
  106. package/plugins/src/base/rules/reference/credential-substrate-precedence.md +166 -0
  107. package/plugins/src/base/rules/reference/integration-access-layer.md +27 -15
  108. package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +76 -65
  109. package/plugins/src/base/skills/lisa-jam-access/SKILL.md +14 -6
  110. package/plugins/src/base/skills/lisa-linear-access/SKILL.md +31 -11
  111. package/plugins/src/base/skills/lisa-notion-access/SKILL.md +37 -24
  112. package/plugins/src/base/skills/lisa-posthog-access/SKILL.md +17 -7
  113. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +8 -0
  114. package/plugins/src/base/skills/lisa-sentry-access/SKILL.md +17 -7
  115. package/plugins/src/base/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  116. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +631 -5
@@ -15,7 +15,13 @@ this skill owns the tool selection.
15
15
  There is exactly one substrate: the **official SonarQube MCP server**, provided by
16
16
  the `sonarqube` plugin and launched by the `sonar` CLI (`sonar run mcp`). It
17
17
  authenticates headlessly from environment variables — no browser, no OS keychain —
18
- so it is the same substrate on a developer machine and in a headless cloud routine:
18
+ so it is the same substrate on a developer machine and in a headless cloud routine.
19
+
20
+ That makes this skill conformant with `credential-substrate-precedence` as a
21
+ single-substrate access layer: this MCP **is** the configured-provider substrate
22
+ (it is token-authenticated, not browser-OAuth), so there is no interactive tier to
23
+ demote and no second REST tier to add. Identity-match against the configured org
24
+ remains mandatory, as on every substrate.
19
25
 
20
26
  - `SONARQUBE_CLI_TOKEN` — required (Sonar user/analysis token).
21
27
  - `SONARQUBE_CLI_ORG` — required for SonarQube Cloud.
@@ -4,7 +4,11 @@ Skills and rules that use external integrations route through the matching
4
4
  `*-access` skill. Do not call vendor MCP tools or REST APIs directly from a
5
5
  consumer skill.
6
6
 
7
- Resolution order is MCP when available and authenticated, then documented
8
- token/REST substrate when the env var is set, then a loud error naming the env
9
- var. Full matrix and migration rules:
7
+ Resolution order is the configured-provider token/CLI substrate first when its
8
+ bootstrap credential is present and identity-matched, then the interactive MCP as
9
+ fallback, then a loud error naming the exact credential to set. Identity-match is
10
+ mandatory on every substrate — one authenticated as a different tenant is skipped,
11
+ never used. The ordering itself is settled in
12
+ [reference/credential-substrate-precedence.md](../reference/credential-substrate-precedence.md);
13
+ full matrix and migration rules:
10
14
  [reference/integration-access-layer.md](../reference/integration-access-layer.md).
@@ -0,0 +1,166 @@
1
+ # Credential-Substrate Precedence
2
+
3
+ **When more than one substrate can reach an external system, the configured credentials
4
+ provider's token/CLI path goes first and the interactive MCP is the fallback — and
5
+ identity-match verification is mandatory on every substrate, at every tier.**
6
+
7
+ This is **one shared, vendor-neutral contract cited by every `*-access` skill** (the
8
+ `leaf-only-lifecycle` / `repo-scope-split` precedent: one shared slug, never divergent
9
+ per-skill prose). An access skill states its per-vendor mechanics — which token, which
10
+ CLI, which identity anchor — and cites this rule for the ordering. It never restates,
11
+ narrows, or locally overrides the ordering.
12
+
13
+ Settled by the decision record `2026-08-12-credential-substrate-precedence` (D6). It is
14
+ a **settled decision** in the `settled-decisions` sense: re-arguing MCP-first inside a
15
+ skill is out of scope for that skill's work.
16
+
17
+ ## The ladder
18
+
19
+ ### Tier 1 — configured-provider substrate
20
+
21
+ The token or CLI path fed by `lisa-secrets-access`. Chosen **whenever its bootstrap
22
+ credential is available AND the resolved substrate identity-matches the configured
23
+ tenant/workspace/site.**
24
+
25
+ `lisa-secrets-access` is the single chokepoint that makes this tier actionable rather
26
+ than aspirational: it owns the one-store rule and the surface ladder, and its `tool:`
27
+ note line already declares which CLI a given credential is expected to drive. An access
28
+ skill resolves its credential through that chokepoint — never by reading an OS keychain
29
+ a second time.
30
+
31
+ Both halves of the gate are load-bearing. A present credential that identity-matches
32
+ nothing is **not** tier 1; it is a failed tier, and the ladder moves on.
33
+
34
+ ### Tier 2 — interactive MCP
35
+
36
+ Used when the tier 1 path is **genuinely unavailable**. The three genuine cases:
37
+
38
+ - **No bootstrap** — the provider credential is absent or the project has not adopted a
39
+ credentials provider at all.
40
+ - **No adapter for the operation** — the token/CLI substrate has no documented adapter
41
+ for the requested operation, and the MCP does (per-operation, not per-session: a skill
42
+ may resolve tier 1 for one operation and tier 2 for the next).
43
+ - **Provider outage** — the provider path is present but failing for reasons the caller
44
+ cannot fix in-session.
45
+
46
+ "The MCP happens to be authenticated" is not one of them. Neither is "tier 1 is slower."
47
+
48
+ ### Tier 3 — loud, actionable failure
49
+
50
+ When no substrate is both available and identity-matched, fail with a message naming the
51
+ exact credential to set and the exact remediation path. Never silently no-op, never
52
+ blind-retry a failed or absent substrate, and never fall through to a substrate that
53
+ failed identity-match.
54
+
55
+ ## Identity-match is mandatory on every substrate
56
+
57
+ Verification runs **in both directions** before any operation: the substrate must claim
58
+ the configured tenant, and the configured tenant must be one the substrate can actually
59
+ reach. A substrate authenticated as a **different** account is **skipped, never used**,
60
+ regardless of tier — including tier 1. A credential is not an identity claim; the
61
+ identity claim is what the provider says when asked.
62
+
63
+ | Vendor | Identity anchor | Probe |
64
+ |---|---|---|
65
+ | Atlassian | `atlassian.cloudId` / `atlassian.site` | `/rest/api/3/myself` email, acli `auth status` site, MCP accessible-resources contains the cloudId |
66
+ | Notion | `notion.workspaceId` (+ `prdDatabaseId` reachability) | `GET /v1/users/me` → `bot.workspace_name`/`workspace_id` |
67
+ | Linear | `linear.workspace` / `linear.teamKey` | `viewer`/`organization` on GraphQL; team list through the MCP |
68
+ | Sentry / PostHog / Jam / Sonar | configured org + project | the substrate's own whoami/org listing |
69
+
70
+ Skipping the check because "the user obviously meant this workspace" is forbidden. Silent
71
+ cross-tenant operations are precisely the hazard this contract exists to prevent.
72
+
73
+ ## Why provider-first (and why it overturns a working default)
74
+
75
+ MCP-first was defensible and is being overturned deliberately, not corrected as an
76
+ oversight. Three reasons outweigh it.
77
+
78
+ **Headless parity.** Cron runs, cloud sessions, CI, and subagent sessions have no
79
+ browser. Under MCP-first an interactive session and a headless session resolve through
80
+ *different* substrates and can therefore fail differently — and the failure surfaces only
81
+ in the environment nobody is watching. Provider-first makes the primary path the same one
82
+ everywhere, with MCP as the enhancement rather than the default.
83
+
84
+ **Tenant safety — the generalized Atlassian write rule.** Substrates differ in *where
85
+ their target comes from*:
86
+
87
+ - **Per-invocation-bound.** The target is part of the call. A cloudId-scoped REST URL
88
+ (`https://api.atlassian.com/ex/jira/<CLOUDID>/…`) or a workspace-scoped API token
89
+ carries its tenant in the request itself, so nothing outside the call can redirect it.
90
+ - **Ambient-bound.** The target comes from machine-global or session-global state a skill
91
+ does not own: acli's single active account, an MCP's browser OAuth session bound to
92
+ whatever account the human last used. Any other process — or the human — can change it
93
+ between the check and the call. That is a **TOCTOU** window, and a successful
94
+ pre-flight `auth status` does not close it.
95
+
96
+ Prefer the per-invocation-bound substrate. This is exactly why Atlassian JIRA *writes*
97
+ were already forced onto the cloudId-scoped curl adapter; the hazard is not specific to
98
+ Atlassian and not specific to writes. A misrouted write is loud and often reversible; a
99
+ **read through the wrong tenant silently returns wrong data**, which then propagates into
100
+ tickets, PRDs, and verification claims — harder to detect and harder to unwind. Reads get
101
+ the same ordering as writes.
102
+
103
+ **Determinism.** A token path either has its bootstrap or does not, and says so. An MCP's
104
+ readiness depends on session state a skill cannot inspect reliably — the same server
105
+ registers under different prefixes depending on install path, and its data tools register
106
+ only after OAuth completes.
107
+
108
+ ## MCP stays a first-class fallback
109
+
110
+ This is a **re-ordering, not a removal.** The strongest argument for MCP-first — that an
111
+ already-authenticated MCP is zero-setup and identity-verified — is preserved by keeping
112
+ MCP as a genuine, fully supported tier rather than deleting it:
113
+
114
+ - Every access skill keeps its MCP adapters in the dispatch table.
115
+ - An operation with no tier 1 adapter routes to MCP **as the normal path**, not as an
116
+ error.
117
+ - A project with no credentials provider is fully functional on MCP alone.
118
+ - MCP failure messages stay actionable (how to enable and authenticate the plugin).
119
+
120
+ Removing an MCP adapter is a separate decision requiring its own justification. Do not
121
+ treat this contract as license to delete one.
122
+
123
+ ## Guarded fallback for ambient-bound substrates
124
+
125
+ When the ladder does fall back to an ambient-bound substrate for a **mutating**
126
+ operation, the fallback is guarded — never the normal path:
127
+
128
+ 1. Switch to the configured profile and assert the active identity matches config
129
+ immediately before the write.
130
+ 2. Execute the write.
131
+ 3. Re-read the affected object(s) immediately afterward.
132
+ 4. Perform a **post-write tenant assertion** on the response — the tenant is proven from
133
+ the response (self URL host, cloudId in the path, or response metadata), not assumed
134
+ from the pre-flight check.
135
+ 5. On mismatch: stop, report a cross-tenant hazard, and best-effort **roll back** the
136
+ write when a safe reversal exists (delete the created object, remove the created
137
+ comment/link, revert a reversible field edit). Never continue as if it succeeded.
138
+
139
+ A successful pre-flight switch is not sufficient for tenant safety — another process can
140
+ mutate global state between the check and the write.
141
+
142
+ ## Consequences to expect
143
+
144
+ - **A stale or wrong token now fails identity-match instead of silently succeeding
145
+ through an authenticated MCP.** That is intended: it is the exact class of bug this
146
+ contract exists to surface. Fix the credential (`/lisa:setup:<vendor>`); do not
147
+ re-order the ladder to route around it.
148
+ - Headless and interactive sessions take the same primary path, so a credential problem
149
+ reproduces on a laptop instead of only at 3am in cron.
150
+ - The ordering is **not configurable per project**. A knob would let a project
151
+ reintroduce the headless divergence this contract removes. Revisit only with a concrete
152
+ need and a new decision record.
153
+
154
+ ## Adding or editing an access skill
155
+
156
+ 1. Cite this rule by name; do not restate the ordering.
157
+ 2. Document the tier 1 credential and the `lisa-secrets-access` resolution path.
158
+ 3. Document the identity anchor and its probe, and state that mismatch means skip.
159
+ 4. Keep MCP adapters and name the operations for which MCP is the only substrate.
160
+ 5. Make the terminal failure name the exact credential and remediation command.
161
+
162
+ Single-substrate access skills are conformant when their one substrate is
163
+ provider-credential-authenticated (`lisa-sonarcloud-access`: the official SonarQube MCP
164
+ authenticates headlessly from `SONARQUBE_CLI_TOKEN`, so it *is* the tier 1 substrate and
165
+ needs no separate REST tier). Reserve a multi-tier ladder for vendors whose MCP is
166
+ browser-OAuth or keychain-bound and therefore dead headless.
@@ -4,24 +4,35 @@ Every Lisa skill or rule that consumes an external integration MUST route throug
4
4
  the integration's `*-access` skill instead of calling that vendor's MCP tools or
5
5
  REST API directly.
6
6
 
7
- The access skill owns substrate resolution:
7
+ The access skill owns substrate resolution. **The ordering is not this rule's to
8
+ define** — it is the single shared contract in `credential-substrate-precedence`,
9
+ cited identically by every `*-access` skill:
8
10
 
9
- 1. MCP, when the tool is available and already authenticated to the configured
10
- workspace/account.
11
- 2. Token/REST substrate, only when the documented env var is present.
12
- 3. Loud failure naming the exact env var to set.
11
+ 1. **Configured-provider token/CLI substrate** the path fed by
12
+ `lisa-secrets-access` — when its bootstrap credential is present AND the
13
+ resolved substrate identity-matches the configured tenant/workspace/account.
14
+ 2. **Interactive MCP**, as a first-class fallback, when the provider path is
15
+ genuinely unavailable (no bootstrap, no adapter for the operation, provider
16
+ outage).
17
+ 3. Loud failure naming the exact credential to set.
13
18
 
14
- Do not blind-retry a failed or absent MCP. Fall back only after checking the
15
- documented env var for that vendor, and never silently no-op when neither tier is
16
- available.
19
+ Identity-match verification is mandatory on **every** substrate, in both
20
+ directions; one authenticated as a different tenant is skipped, never used. See the
21
+ `credential-substrate-precedence` rule for the rationale (headless parity, tenant
22
+ safety, determinism), the guarded-fallback protocol, and what "genuinely
23
+ unavailable" means.
24
+
25
+ Do not blind-retry a failed or absent substrate, and never silently no-op when no
26
+ tier is available.
17
27
 
18
28
  Some MCPs authenticate headlessly from an env token and need no separate REST
19
- tier — the MCP **is** the headless substrate on both developer machines and cloud
20
- routines. The official SonarQube MCP is one such case (`SONARQUBE_CLI_TOKEN`
29
+ tier — such an MCP **is** the configured-provider substrate on both developer
30
+ machines and cloud routines, so a single-substrate access skill is conformant. The
31
+ official SonarQube MCP is one such case (`SONARQUBE_CLI_TOKEN`
21
32
  [+ `SONARQUBE_CLI_ORG`/`SONARQUBE_CLI_SERVER`]): `lisa-sonarcloud-access` resolves it as a
22
- single substrate with no hand-rolled REST fallback. Reserve the two-tier
23
- MCP-then-REST shape for vendors whose MCP is browser-OAuth or keychain-bound and
24
- therefore dead headless.
33
+ single substrate with no hand-rolled REST fallback. Reserve the multi-tier ladder
34
+ for vendors whose MCP is browser-OAuth or keychain-bound and therefore dead
35
+ headless.
25
36
 
26
37
  ## Access Skills
27
38
 
@@ -60,7 +71,8 @@ When editing any skill listed in the matrix:
60
71
  delegate to the matching access skill.
61
72
  - Keep operation names coarse and vendor-native. Add new operation rows to the
62
73
  access skill instead of embedding REST details in the consumer.
63
- - Preserve existing MCP behavior as the first tier where Claude/Codex can expose
64
- the MCP, but make headless token fallback explicit and gated by the env var.
74
+ - Put the documented token/CLI substrate first and keep the MCP as an explicit,
75
+ fully supported fallback tier `credential-substrate-precedence`. Preserving
76
+ the MCP adapters is required; re-ordering them is not the same as removing them.
65
77
  - If a vendor has no documented token substrate, keep the MCP-only behavior and
66
78
  fail with a clear "no documented headless substrate" message.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lisa-atlassian-access
3
- description: "Vendor-neutral access layer for Atlassian (JIRA + Confluence). Every jira-* and confluence-* skill MUST delegate through this skill rather than calling Atlassian directly. Resolves a substrate per operation, binding JIRA writes to the configured cloudId via Atlassian REST whenever token auth is available and using acli only for reads or as a guarded fallback. For non-write acli operations, acli is used when installed and switchable to a profile matching the configured site; mismatched active profiles are skipped only after switch plus re-verification fails."
3
+ description: "Vendor-neutral access layer for Atlassian (JIRA + Confluence). Every jira-* and confluence-* skill MUST delegate through this skill rather than calling Atlassian directly. Per the credential-substrate-precedence contract, resolves a substrate per operation with the ATLASSIAN_API_TOKEN curl path first for reads and writes alike whenever the token is present and identity-matched binding JIRA writes to the configured cloudId then acli, then the Atlassian MCP as fallbacks. acli is used when installed and switchable to a profile matching the configured site; mismatched active profiles are skipped only after switch plus re-verification fails, and acli writes are a guarded fallback with post-write tenant assertions."
4
4
  allowed-tools: ["Bash", "Read", "Skill"]
5
5
  ---
6
6
 
@@ -35,46 +35,17 @@ EMAIL=$(jq -r '.atlassian.email // empty' .lisa.config.local.json 2>/dev/null)
35
35
  [ -z "$CLOUDID" ] && { echo "Error: atlassian.cloudId not set. Run /lisa:setup:atlassian." >&2; exit 1; }
36
36
  ```
37
37
 
38
- Probe each tier in order; the first that's ready AND identity-matches is the substrate for this operation. Identity-match is verified before any operation; substrates authenticated as a different Atlassian account are switched to the configured profile when one exists, then skipped only if the switch fails or re-verification still mismatches.
38
+ Probe each tier in order; the first that's ready AND identity-matches is the substrate for this operation. The ordering is the shared `credential-substrate-precedence` contract — the configured-provider token substrate leads for **reads and writes alike**, with acli and the MCP as identity-matched fallbacks — not an Atlassian-local choice. Identity-match is verified before any operation; substrates authenticated as a different Atlassian account are switched to the configured profile when one exists, then skipped only if the switch fails or re-verification still mismatches.
39
39
 
40
40
  ```bash
41
41
  substrate=""
42
42
 
43
- # Tier 1: acli for reads and non-write operations only.
44
- #
45
- # Do not choose acli for JIRA writes when curl/token auth is available. acli stores
46
- # one machine-global active account and workitem writes cannot pin a cloudId per
47
- # invocation, so switch-then-write is a TOCTOU risk in multi-account or concurrent
48
- # sessions. Write operations prefer the cloudId-scoped REST URL below.
49
- if [ "$OP_KIND" != "jira-write" ] && command -v acli >/dev/null 2>&1 && acli auth status >/dev/null 2>&1; then
50
- current_site=$(acli auth status 2>/dev/null | awk '/^ Site:/{print $2}')
51
- if [ "$current_site" != "$SITE" ]; then
52
- # acli installed but pointing at a different site. Try switching profiles.
53
- acli auth switch --site "$SITE" ${EMAIL:+--email "$EMAIL"} >/dev/null 2>&1 || true
54
- current_site=$(acli auth status 2>/dev/null | awk '/^ Site:/{print $2}')
55
- fi
56
- if [ "$current_site" = "$SITE" ]; then
57
- substrate="acli"
58
- fi
59
- fi
60
-
61
- # Tier 2: Atlassian MCP (if acli not ready OR the operation isn't acli-covered)
62
- # $OP_REQUIRES is a conceptual variable set by the dispatch table to "non-acli" for
63
- # operations that have no acli adapter (e.g. read-page-descendants). It is not a real
64
- # shell variable initialized here — the condition is illustrative pseudo-code.
65
- if [ -z "$substrate" ] || [ "$OP_REQUIRES" = "non-acli" ]; then
66
- # Probe via mcp__plugin_atlassian_atlassian__getAccessibleAtlassianResources.
67
- # (Pseudo-code; actual call is the MCP tool invocation, not a bash command.)
68
- # If the MCP returns a list and $CLOUDID is in it, MCP is identity-matched.
69
- # If the MCP is unauthenticated or $CLOUDID is NOT in the list, MCP is skipped.
70
- if mcp_atlassian_authenticated_and_matches_cloudid "$CLOUDID"; then
71
- : ${substrate:=mcp}
72
- # Mark MCP as available even if acli already won tier 1 — used for ops acli can't do.
73
- mcp_available=true
74
- fi
75
- fi
76
-
77
- # Tier 3: curl + API token (headless / multi-account / scoped-token path)
43
+ # Tier 1: curl + API token — the configured-provider substrate, resolved through
44
+ # lisa-secrets-access. Leads for every operation because it is per-invocation-bound:
45
+ # the cloudId-scoped gateway URL and the token's own account carry the tenant inside
46
+ # the request, so no ambient machine-global state can redirect it. acli (one global
47
+ # active account) and the MCP (browser OAuth session) are ambient-bound and therefore
48
+ # TOCTOU-exposed see credential-substrate-precedence, "tenant safety".
78
49
  read_atlassian_token() {
79
50
  local email="$1"
80
51
  [ -n "$ATLASSIAN_API_TOKEN" ] && { echo "$ATLASSIAN_API_TOKEN"; return; }
@@ -136,13 +107,50 @@ public static class LisaCred {
136
107
  esac
137
108
  }
138
109
  TOKEN=$(read_atlassian_token "$EMAIL")
139
- [ -n "$TOKEN" ] && curl_available=true && {
140
- if [ "$OP_KIND" = "jira-write" ]; then
110
+ if [ -n "$TOKEN" ]; then
111
+ # Identity-match before use: /rest/api/3/myself must report the configured account
112
+ # (Step 2). A present-but-wrong token fails the gate loudly instead of quietly
113
+ # deferring to an acli profile or MCP session authenticated somewhere else — that
114
+ # silent success is the bug class the precedence contract exists to surface.
115
+ if atlassian_token_matches_config "$TOKEN" "$EMAIL" "$CLOUDID"; then
116
+ curl_available=true
141
117
  substrate="curl"
142
118
  else
143
- : ${substrate:=curl}
119
+ echo "Warning: ATLASSIAN_API_TOKEN does not match the configured account/site. Skipping curl tier." >&2
144
120
  fi
145
- }
121
+ fi
122
+
123
+ # Tier 2: acli — identity-matched fallback. Used when no token is available, or for
124
+ # operations with no curl adapter. Never the primary path for JIRA writes when token
125
+ # auth is available: acli stores one machine-global active account and workitem writes
126
+ # cannot pin a cloudId per invocation, so switch-then-write is a TOCTOU risk in
127
+ # multi-account or concurrent sessions. When a write does land here it is the *guarded*
128
+ # fallback documented in the dispatch table (assert, write, re-read, assert, roll back).
129
+ if command -v acli >/dev/null 2>&1 && acli auth status >/dev/null 2>&1; then
130
+ current_site=$(acli auth status 2>/dev/null | awk '/^ Site:/{print $2}')
131
+ if [ "$current_site" != "$SITE" ]; then
132
+ # acli installed but pointing at a different site. Try switching profiles.
133
+ acli auth switch --site "$SITE" ${EMAIL:+--email "$EMAIL"} >/dev/null 2>&1 || true
134
+ current_site=$(acli auth status 2>/dev/null | awk '/^ Site:/{print $2}')
135
+ fi
136
+ if [ "$current_site" = "$SITE" ]; then
137
+ acli_available=true
138
+ # Mark acli available even if curl already won tier 1 — used for ops curl can't do.
139
+ : ${substrate:=acli}
140
+ fi
141
+ fi
142
+
143
+ # Tier 3: Atlassian MCP — first-class interactive fallback, for when neither tier above
144
+ # is available or covers the operation (e.g. an op with no curl and no acli adapter).
145
+ # Probe via mcp__plugin_atlassian_atlassian__getAccessibleAtlassianResources.
146
+ # (Pseudo-code; actual call is the MCP tool invocation, not a bash command.)
147
+ # If the MCP returns a list and $CLOUDID is in it, MCP is identity-matched.
148
+ # If the MCP is unauthenticated or $CLOUDID is NOT in the list, MCP is skipped.
149
+ if mcp_atlassian_authenticated_and_matches_cloudid "$CLOUDID"; then
150
+ : ${substrate:=mcp}
151
+ # Mark MCP as available even if an earlier tier won — used for ops they can't do.
152
+ mcp_available=true
153
+ fi
146
154
 
147
155
  # Fail loudly with actionable remediation if nothing works.
148
156
  if [ -z "$substrate" ]; then
@@ -154,15 +162,25 @@ if [ -z "$substrate" ]; then
154
162
  cat >&2 <<EOF
155
163
  Error: no Atlassian access substrate available for site $SITE.
156
164
 
157
- Attempted:
165
+ Attempted (in credential-substrate-precedence order):
166
+ curl — no ATLASSIAN_API_TOKEN found for $EMAIL (env, slug-suffixed env, or keychain) OR the token does not match the configured account/site
158
167
  acli — $(command -v acli >/dev/null && echo "installed but identity mismatch or unauthenticated" || echo "not installed")
159
168
  MCP — $([ "$plugin_enabled_global" = "true" ] || [ "$plugin_enabled_project" = "true" ] || [ "$plugin_enabled_local" = "true" ] && echo "plugin enabled but not authenticated or cloudId $CLOUDID not in accessible resources" || echo "plugin not enabled in any settings.json scope")
160
- curl — no ATLASSIAN_API_TOKEN found for $EMAIL (env, slug-suffixed env, or keychain)
161
169
 
162
- Remediation paths (pick one):
170
+ Remediation paths (the first is the contract's primary path):
171
+
172
+ 1. Provision an API token — works headless, in CI, in subagents, and in
173
+ multi-account setups, and is the substrate this project resolves first.
174
+
175
+ Run /lisa:setup:atlassian — guided flow with clipboard-piped keychain store.
176
+
177
+ 2. Install acli and authenticate (identity-matched fallback for multi-account developers).
163
178
 
164
- 1. Install the Atlassian MCP plugin (local scope — per-developer, gitignored).
165
- This is the simplest path for single-account developers.
179
+ brew tap atlassian/homebrew-acli && brew install acli
180
+ acli auth login # OAuth as the account matching $EMAIL
181
+
182
+ 3. Install the Atlassian MCP plugin (local scope — per-developer, gitignored).
183
+ The supported fallback when no credentials provider is configured.
166
184
 
167
185
  Run in your terminal:
168
186
 
@@ -174,25 +192,16 @@ Remediation paths (pick one):
174
192
  Then restart Claude Code (or run /restart-mcp) to load the plugin, and
175
193
  invoke 'mcp__plugin_atlassian_atlassian__authenticate' to complete OAuth.
176
194
 
177
- 2. Install acli and authenticate (best for multi-account developers).
178
-
179
- brew tap atlassian/homebrew-acli && brew install acli
180
- acli auth login # OAuth as the account matching $EMAIL
181
-
182
- 3. Provision an API token (headless / CI / scoped-token environments).
183
-
184
- Run /lisa:setup:atlassian — guided flow with clipboard-piped keychain store.
185
-
186
195
  EOF
187
196
  exit 1
188
197
  fi
189
198
  ```
190
199
 
191
- Operation dispatch then uses `$substrate` for the primary route. If the operation has no `acli` adapter and `$substrate=acli`, fall through to `$mcp_available` then `$curl_available` for the actual call. The fall-through stops at the first available tier that can perform the operation.
200
+ Operation dispatch then uses `$substrate` for the primary route. If the operation has no adapter for the selected substrate, fall through in contract order — `$curl_available`, then `$acli_available`, then `$mcp_available` skipping the tier already tried. The fall-through stops at the first available tier that can perform the operation. A tier that failed identity-match is never in the fall-through set.
192
201
 
193
202
  ### Step 2 — Connection-match check
194
203
 
195
- The active connection MUST point at the cloudId/site declared in `.lisa.config.json`. Step 1's substrate selection already tries to switch mismatched acli profiles and verifies the result before selection. This step repeats the assertion before any operation runs — defensive in case the substrate state changed since selection.
204
+ The active connection MUST point at the cloudId/site declared in `.lisa.config.json`. Identity-match is mandatory on **every** substrate, tier 1 included (`credential-substrate-precedence`); the "curl mode check" below *is* the tier-1 gate referenced as `atlassian_token_matches_config` in Step 1. Step 1's substrate selection already validates the token account and tries to switch mismatched acli profiles before selection. This step repeats the assertion before any operation runs — defensive in case the substrate state changed since selection.
196
205
 
197
206
  Read configured site:
198
207
 
@@ -291,12 +300,12 @@ Rules:
291
300
 
292
301
  ### Step 3 — Operation dispatch
293
302
 
294
- Substrate column meanings:
303
+ Substrate column meanings (ordering per `credential-substrate-precedence`):
295
304
 
296
- - **`acli`**: routes through `acli`. Preferred when available and identity-matched.
297
- - **`MCP`**: routes through the Atlassian MCP. Preferred when acli can't do the op and the MCP is identity-matched (cloudId in `getAccessibleAtlassianResources`).
298
- - **`curl`**: routes through curl + Basic auth + `ATLASSIAN_API_TOKEN`. Used when neither acli nor MCP is available.
299
- - Multiple cells filled means tier ordering applies — try acli, then MCP, then curl, taking the first that has an adapter for the op AND is identity-matched.
305
+ - **`curl`**: routes through curl + Basic auth + `ATLASSIAN_API_TOKEN` — the configured-provider substrate. Preferred for every operation, read or write, whenever the token is present and identity-matched.
306
+ - **`acli`**: routes through `acli`. Identity-matched fallback used when no token is available or the op has no curl adapter. For JIRA writes it is the *guarded* fallback (see the tenant-safety rule below).
307
+ - **`MCP`**: routes through the Atlassian MCP. First-class fallback for ops neither tier above covers, when identity-matched (cloudId in `getAccessibleAtlassianResources`).
308
+ - Multiple cells filled means tier ordering applies — try curl, then acli, then MCP, taking the first that has an adapter for the op AND is identity-matched.
300
309
  - One cell means only that substrate can perform the op.
301
310
 
302
311
  `<SITE>` = `.atlassian.site` (e.g. `acme.atlassian.net`). `<CLOUDID>` = `.atlassian.cloudId`. `<AUTH>` = `Basic $(printf '%s:%s' "$email" "$ATLASSIAN_API_TOKEN" | base64)`. JIRA curl writes use the cloudId-bound Atlassian gateway `https://api.atlassian.com/ex/jira/<CLOUDID>/rest/api/3/...`; JIRA curl reads may use either that gateway or `https://<SITE>/rest/api/3/...` after the token account check. Confluence uses `/wiki/rest/api/...` (v1) or `/api/v2/...` (v2).
@@ -334,7 +343,7 @@ Substrate column meanings:
334
343
 
335
344
  **acli flag note:** acli's `--output` flag does not exist; the correct flag is `--json`. List commands require `--paginate` or `--limit` (no implicit fetch-all). `acli jira workitem view` defaults to a restricted field set (`key,issuetype,summary,status,assignee,description`), so `read-ticket` MUST pass `--fields '*all'` or an explicit equivalent that includes every downstream dependency: parent, subtasks, issue links, components, labels, priority, status, issue type, summary, description, fix versions, affected versions, attachments, comments, estimates, sprint/story-point fields, and project-required custom fields. Never rely on the default view fields; they hide parent/components/labels and corrupt leaf-only, relationship-search, build-ready, and required-custom-field gates. Several documented adapters are nominal — verify against `acli <subcmd> --help` before relying on them. When acli's adapter is broken or missing for a specific op, fall through to MCP (if identity-matched) then curl per the tier ordering.
336
345
 
337
- **JIRA write tenant-safety rule:** create, edit, transition, comment, and link are write operations. They MUST prefer the curl adapter whenever token auth is available because the URL includes `<CLOUDID>` and cannot be redirected by the user-global acli active account. If the flow must fall back to acli for a write, it is a guarded fallback, not the normal path:
346
+ **JIRA write tenant-safety rule** — the Atlassian instance of the shared guarded-fallback protocol in `credential-substrate-precedence` (which states the general rule: prefer the per-invocation-bound substrate over the ambient-bound one, for reads and writes alike; the rationale is not restated here). Create, edit, transition, comment, and link are write operations. They MUST use the curl adapter whenever token auth is available because the URL includes `<CLOUDID>` and cannot be redirected by the user-global acli active account. If the flow must fall back to acli for a write, it is a guarded fallback, not the normal path:
338
347
 
339
348
  1. Switch and assert the active `acli auth status` site/email matches config immediately before the write.
340
349
  2. Execute the write.
@@ -378,15 +387,17 @@ Do not paraphrase substrate output beyond JSON normalization.
378
387
  ## Invariants
379
388
 
380
389
  - Caller skills never invoke `acli` or `curl` against Atlassian directly. They only invoke this skill.
390
+ - Tier order is the shared `credential-substrate-precedence` contract — token curl first (reads **and** writes), then acli, then the Atlassian MCP. Do not restate or locally override the ordering here.
391
+ - acli and the Atlassian MCP remain first-class **fallbacks**, not removed tiers: every adapter stays in the dispatch table, and a project with no credentials provider is fully functional on them.
381
392
  - Substrate is decided once per skill invocation and never switches mid-operation.
382
- - Connection match is mandatory. Operations that bypass it (because "the user obviously meant the configured site") are forbidden.
393
+ - Connection match is mandatory on every tier, including the token tier. Operations that bypass it (because "the user obviously meant the configured site") are forbidden. A present-but-wrong token fails the gate rather than deferring to another substrate.
383
394
  - Profile mutations (`acli auth switch`) are allowed when acli is the active substrate. The curl substrate never mutates the token — if `ATLASSIAN_API_TOKEN` doesn't match the configured account, fail loud rather than silently substituting.
384
395
  - JIRA writes are cloudId-bound by default. `acli` write adapters are fallback-only and must perform post-write tenant assertions plus safe rollback on mismatch.
385
396
  - `.lisa.config.local.json` overrides `.lisa.config.json` per-key — the same precedence rule as every other consumer of project config.
386
397
 
387
398
  ## Headless behavior
388
399
 
389
- In a headless / non-interactive context, the MCP tier is unavailable (its OAuth flow needs a browser). The substrate ladder collapses to: acli (if pre-authenticated, e.g., a CI image baked with a service-account token) curl + `ATLASSIAN_API_TOKEN`. Never block on interactive prompts. If both fail readiness checks, exit non-zero with a deterministic error.
400
+ In a headless / non-interactive context, the MCP tier is unavailable (its OAuth flow needs a browser). The ladder collapses to: curl + `ATLASSIAN_API_TOKEN` → acli (if pre-authenticated, e.g., a CI image baked with a service-account token). Because curl is already tier 1 interactively, headless and interactive sessions take the **same primary path** — that is the "headless parity" arm of `credential-substrate-precedence`, and it is why a credential problem reproduces on a laptop instead of only in cron. Never block on interactive prompts. If both fail readiness checks, exit non-zero with a deterministic error.
390
401
 
391
402
  Treat all four of these as headless:
392
403
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lisa-jam-access
3
- description: "Vendor-neutral access layer for Jam. Jam triage rules and skills MUST delegate through this skill rather than calling Jam MCP tools directly. Resolves Jam MCP first when available, then falls back to the JAM_PAT-authenticated Jam CLI for headless routines."
3
+ description: "Vendor-neutral access layer for Jam. Jam triage rules and skills MUST delegate through this skill rather than calling Jam MCP tools directly. Per the credential-substrate-precedence contract, resolves the JAM_PAT-authenticated Jam CLI first when the PAT is present and identity-matched, then falls back to the Jam MCP."
4
4
  allowed-tools: ["Bash", "Read", "Skill"]
5
5
  ---
6
6
 
@@ -21,13 +21,20 @@ Return parsed JSON or a concise structured summary in a `<result>` block.
21
21
 
22
22
  ## Substrate Selection
23
23
 
24
- Probe in order:
24
+ Probe in order — the ordering is the shared `credential-substrate-precedence`
25
+ contract, not a Jam-local choice. The first tier that is ready **and**
26
+ identity-matches the configured Jam account is used; one authenticated elsewhere
27
+ is skipped, never used.
25
28
 
26
- 1. Jam MCP, if the tool is available and authenticated.
27
- 2. Jam CLI authenticated with `JAM_PAT`.
29
+ 1. **Tier 1 configured-provider substrate: Jam CLI authenticated with `JAM_PAT`**,
30
+ resolved through `lisa-secrets-access`.
31
+ 2. **Tier 2 — interactive MCP fallback: Jam MCP**, if the tool is available and
32
+ authenticated. Used when tier 1 is genuinely unavailable: no `JAM_PAT`, no CLI
33
+ adapter for the operation, or a Jam outage.
28
34
 
29
35
  Jam documents a PAT-authenticated CLI that is cleaner for remote routines than
30
- editing `.mcp.json` headers. The headless tier uses:
36
+ editing `.mcp.json` headers, and it is the same substrate interactively and
37
+ headlessly — which is why it leads. The CLI tier uses:
31
38
 
32
39
  ```bash
33
40
  curl -fsSL https://native.jam.dev/install | bash
@@ -44,7 +51,8 @@ Error: no Jam access substrate available. Authenticate the Jam MCP or set JAM_PA
44
51
 
45
52
  ## Invariants
46
53
 
47
- - Fallback is gated on `JAM_PAT`; do not retry Jam MCP failures blindly.
54
+ - Tier order is `credential-substrate-precedence`: `JAM_PAT` CLI first, Jam MCP as
55
+ a preserved first-class fallback. Do not retry a failed tier blindly.
48
56
  - Never commit a Jam PAT into `.mcp.json` or any generated setup artifact.
49
57
  - Headless Jam access requires `native.jam.dev` for the installer and
50
58
  `api.jam.dev` for CLI/API calls in any custom remote network allowlist.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lisa-linear-access
3
- description: "Vendor-neutral access layer for Linear. Linear skills MUST delegate through this skill rather than calling Linear MCP tools or Linear GraphQL directly. Resolves Linear MCP first when authenticated, then falls back to LINEAR_API_KEY + Linear GraphQL in headless environments."
3
+ description: "Vendor-neutral access layer for Linear. Linear skills MUST delegate through this skill rather than calling Linear MCP tools or Linear GraphQL directly. Per the credential-substrate-precedence contract, resolves LINEAR_API_KEY + Linear GraphQL first ahead of the Linear MCP — whenever the key is present and identity-matches the configured workspace/team, and falls back to the Linear MCP when the token path is unavailable. Identity-match is mandatory on both substrates."
4
4
  allowed-tools: ["Bash", "Read", "Skill"]
5
5
  ---
6
6
 
@@ -46,15 +46,27 @@ WORKSPACE=$(jq -r '.linear.workspace // empty' .lisa.config.json 2>/dev/null)
46
46
  TEAM_KEY=$(jq -r '.linear.teamKey // empty' .lisa.config.json 2>/dev/null)
47
47
  ```
48
48
 
49
- Probe in order:
50
-
51
- 1. Linear MCP, if `mcp__linear-server__list_teams` is available and can list the
52
- configured workspace/team.
53
- 2. `LINEAR_API_KEY` with Linear GraphQL (`https://api.linear.app/graphql`).
49
+ Probe in order — the ordering is the shared `credential-substrate-precedence`
50
+ contract, not a Linear-local choice. The first tier that is ready **and**
51
+ identity-matches wins; a substrate authenticated against a different
52
+ workspace/team is **skipped, never used**, at either tier.
53
+
54
+ 1. **Tier 1 — configured-provider substrate: `LINEAR_API_KEY` with Linear GraphQL**
55
+ (`https://api.linear.app/graphql`), resolved through `lisa-secrets-access`.
56
+ Identity-match by querying `viewer { organization { urlKey } }` (plus
57
+ `teams` when `linear.teamKey` is configured) and comparing to
58
+ `.lisa.config.json`. A key that resolves to a different organization fails the
59
+ gate — warn, skip the tier, and continue down the ladder.
60
+ 2. **Tier 2 — interactive MCP fallback: Linear MCP**, if
61
+ `mcp__linear-server__list_teams` is available and can list the configured
62
+ workspace/team (that listing *is* the identity match). Used when tier 1 is
63
+ genuinely unavailable: no `LINEAR_API_KEY`, no GraphQL adapter for the
64
+ operation, or a Linear API outage.
54
65
 
55
66
  The Linear GraphQL docs support personal API keys for scripts and authenticate
56
- with an `Authorization: <API_KEY>` header. Treat `LINEAR_API_KEY` as the
57
- headless substrate. If neither tier works, fail with:
67
+ with an `Authorization: <API_KEY>` header, so the token tier works identically on
68
+ a developer laptop, in CI, in a cloud routine, and in a subagent — which is why it
69
+ leads. If neither tier works, fail with:
58
70
 
59
71
  ```text
60
72
  Error: no Linear access substrate available. Authenticate the Linear MCP or set LINEAR_API_KEY.
@@ -204,8 +216,16 @@ query($id:String!){
204
216
 
205
217
  ## Invariants
206
218
 
207
- - MCP is preferred when it is present and already authenticated.
208
- - GraphQL fallback runs only when `LINEAR_API_KEY` is present.
209
- - Missing MCP plus missing token is a hard failure naming `LINEAR_API_KEY`.
219
+ - Tier order is the shared `credential-substrate-precedence` contract:
220
+ `LINEAR_API_KEY` + GraphQL first when present and identity-matched, then the
221
+ Linear MCP. Do not restate or locally override the ordering here.
222
+ - The Linear MCP remains a first-class **fallback**, not a removed tier: it stays
223
+ the substrate whenever `LINEAR_API_KEY` is absent, the operation has no GraphQL
224
+ adapter, or the token path is failing.
225
+ - Identity-match is mandatory on **both** substrates. A substrate authenticated
226
+ against a different Linear organization or team is skipped, never used — that
227
+ includes a present-but-wrong `LINEAR_API_KEY`, which fails the gate loudly
228
+ instead of silently deferring to an authenticated MCP.
229
+ - Missing token plus missing MCP is a hard failure naming `LINEAR_API_KEY`.
210
230
  - Mutations send only the fields being changed, matching existing Linear skill
211
231
  guidance that `save_*` style updates should not clobber unrelated fields.