@codyswann/lisa 3.0.0 → 3.2.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 (139) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +50 -23
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +211 -121
  5. package/expo/copy-overwrite/scripts/bdd/contract.mjs +10 -2
  6. package/expo/copy-overwrite/scripts/bdd/envelope.mjs +3 -2
  7. package/expo/copy-overwrite/scripts/bdd/render.mjs +2 -2
  8. package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +45 -8
  9. package/expo/copy-overwrite/scripts/classify-maestro-failures.mjs +775 -0
  10. package/expo/create-only/.github/workflows/nightly-e2e-report.yml +71 -0
  11. package/expo/create-only/.maestro/flake-classification.json +21 -0
  12. package/expo/create-only/bdd/coverage-map.json +1 -2
  13. package/expo/package-lisa/package.lisa.json +1 -0
  14. package/package.json +1 -1
  15. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +75 -64
  18. package/plugins/lisa/.codex-plugin/skills/lisa-jam-access/SKILL.md +13 -5
  19. package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +30 -10
  20. package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +36 -23
  21. package/plugins/lisa/.codex-plugin/skills/lisa-posthog-access/SKILL.md +16 -6
  22. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +8 -0
  23. package/plugins/lisa/.codex-plugin/skills/lisa-sentry-access/SKILL.md +16 -6
  24. package/plugins/lisa/.codex-plugin/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  25. package/plugins/lisa/hooks/threshold-ratchet-families.mjs +24 -0
  26. package/plugins/lisa/rules/eager/credential-substrate-precedence.md +52 -0
  27. package/plugins/lisa/rules/eager/integration-access-layer.md +7 -3
  28. package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +19 -8
  29. package/plugins/lisa/rules/reference/credential-substrate-precedence.md +166 -0
  30. package/plugins/lisa/rules/reference/integration-access-layer.md +27 -15
  31. package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +76 -65
  32. package/plugins/lisa/skills/lisa-jam-access/SKILL.md +14 -6
  33. package/plugins/lisa/skills/lisa-linear-access/SKILL.md +31 -11
  34. package/plugins/lisa/skills/lisa-notion-access/SKILL.md +37 -24
  35. package/plugins/lisa/skills/lisa-posthog-access/SKILL.md +17 -7
  36. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +8 -0
  37. package/plugins/lisa/skills/lisa-sentry-access/SKILL.md +17 -7
  38. package/plugins/lisa/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  39. package/plugins/lisa-agy/plugin.json +1 -1
  40. package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +76 -65
  41. package/plugins/lisa-agy/skills/lisa-jam-access/SKILL.md +14 -6
  42. package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +31 -11
  43. package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +37 -24
  44. package/plugins/lisa-agy/skills/lisa-posthog-access/SKILL.md +17 -7
  45. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +8 -0
  46. package/plugins/lisa-agy/skills/lisa-sentry-access/SKILL.md +17 -7
  47. package/plugins/lisa-agy/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  48. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  51. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +24 -0
  55. package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +52 -0
  56. package/plugins/lisa-copilot/rules/eager/integration-access-layer.md +7 -3
  57. package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +19 -8
  58. package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +166 -0
  59. package/plugins/lisa-copilot/rules/reference/integration-access-layer.md +27 -15
  60. package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +76 -65
  61. package/plugins/lisa-copilot/skills/lisa-jam-access/SKILL.md +14 -6
  62. package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +31 -11
  63. package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +37 -24
  64. package/plugins/lisa-copilot/skills/lisa-posthog-access/SKILL.md +17 -7
  65. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +8 -0
  66. package/plugins/lisa-copilot/skills/lisa-sentry-access/SKILL.md +17 -7
  67. package/plugins/lisa-copilot/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  68. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +24 -0
  70. package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +19 -8
  71. package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +171 -0
  72. package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +57 -0
  73. package/plugins/lisa-cursor/rules/integration-access-layer-reference.mdc +27 -15
  74. package/plugins/lisa-cursor/rules/integration-access-layer.mdc +7 -3
  75. package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +76 -65
  76. package/plugins/lisa-cursor/skills/lisa-jam-access/SKILL.md +14 -6
  77. package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +31 -11
  78. package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +37 -24
  79. package/plugins/lisa-cursor/skills/lisa-posthog-access/SKILL.md +17 -7
  80. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +8 -0
  81. package/plugins/lisa-cursor/skills/lisa-sentry-access/SKILL.md +17 -7
  82. package/plugins/lisa-cursor/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  83. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-expo-agy/plugin.json +1 -1
  86. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  91. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  96. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  101. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  106. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-rails-agy/plugin.json +1 -1
  111. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  116. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  121. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/src/base/hooks/threshold-ratchet-families.mjs +24 -0
  124. package/plugins/src/base/rules/eager/credential-substrate-precedence.md +52 -0
  125. package/plugins/src/base/rules/eager/integration-access-layer.md +7 -3
  126. package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +19 -8
  127. package/plugins/src/base/rules/reference/credential-substrate-precedence.md +166 -0
  128. package/plugins/src/base/rules/reference/integration-access-layer.md +27 -15
  129. package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +76 -65
  130. package/plugins/src/base/skills/lisa-jam-access/SKILL.md +14 -6
  131. package/plugins/src/base/skills/lisa-linear-access/SKILL.md +31 -11
  132. package/plugins/src/base/skills/lisa-notion-access/SKILL.md +37 -24
  133. package/plugins/src/base/skills/lisa-posthog-access/SKILL.md +17 -7
  134. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +8 -0
  135. package/plugins/src/base/skills/lisa-sentry-access/SKILL.md +17 -7
  136. package/plugins/src/base/skills/lisa-sonarcloud-access/SKILL.md +7 -1
  137. package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +24 -0
  138. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +631 -5
  139. package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +24 -0
@@ -0,0 +1,71 @@
1
+ name: 🌙 Nightly E2E Report
2
+
3
+ # The REPORTING half of the nightly e2e standard, and the companion to
4
+ # `nightly-e2e-health.yml`.
5
+ #
6
+ # The gate tells whoever opened a pull request that the nightly is red. It tells
7
+ # nobody else. A red suite with no open pull requests is invisible, there is
8
+ # nothing to assign, and there is no record that it came back. This workflow
9
+ # keeps exactly ONE open tracking issue per suite: filed the first red night,
10
+ # refreshed every night it is still red, closed automatically the night a full
11
+ # green run lands.
12
+ #
13
+ # ## This is NOT a gate — never make it a required check
14
+ #
15
+ # Its status answers "did reporting work", not "is the suite green". A red
16
+ # nightly reported correctly is a successful report. Requiring this context would
17
+ # hand an issues-API outage the power to block every pull request, which is
18
+ # exactly the coupling the two-workflow split exists to prevent: the gate
19
+ # workflow does not request `issues:` scope at all, so it cannot file, and this
20
+ # one is not required, so it cannot block.
21
+ #
22
+ # ## Wiring this up
23
+ #
24
+ # 1. Keep `suites` and `branch` IDENTICAL to your `nightly-e2e-health.yml`
25
+ # caller. Two different tables means the issues describe a different set of
26
+ # suites than the one blocking merges.
27
+ # 2. Schedule it AFTER your nightly suites finish, not before. It reports on
28
+ # the runs they produced; running it first reports on yesterday's.
29
+ # 3. Leave the `nightly-e2e` label alone once issues exist under it — the label
30
+ # is how this job finds the issue it already owns.
31
+ #
32
+ # ## Concurrency
33
+ #
34
+ # Deliberately absent here. The non-cancelling group lives in Lisa's reusable
35
+ # workflow, on the job that writes, so an adopter cannot forget it — a forgotten
36
+ # group lets two overlapping reports both read "no open issue" and both file one.
37
+
38
+ on:
39
+ schedule:
40
+ # ~90 minutes after a midnight-UTC nightly, so the suites it reports on have
41
+ # finished. Move it if yours run at another hour; reporting on a suite that
42
+ # is still running produces "no completed run" every night.
43
+ - cron: '30 1 * * *'
44
+ workflow_dispatch:
45
+
46
+ permissions:
47
+ contents: read
48
+ # Reading the nightly run history — the same read the gate does.
49
+ actions: read
50
+ # The one scope the GATE deliberately does not have. Filing lives here so an
51
+ # issues outage can never redden a required check.
52
+ issues: write
53
+
54
+ jobs:
55
+ report:
56
+ name: 🌙 Nightly E2E Report
57
+ # PIN AN IMMUTABLE REF, for the same reason the gate caller does: the two
58
+ # halves must speak the same contract major, and `@main` can change between
59
+ # a report and the gate reading the same night.
60
+ uses: CodySwannGT/lisa/.github/workflows/nightly-e2e-report.yml@v2.345.1
61
+ with:
62
+ branch: dev
63
+ # KEEP THIS IDENTICAL to the `suites` value in nightly-e2e-health.yml.
64
+ suites: |
65
+ [
66
+ {
67
+ "label": "Maestro native e2e",
68
+ "workflow": "maestro-e2e.yml",
69
+ "match": { "mode": "run" }
70
+ }
71
+ ]
@@ -0,0 +1,21 @@
1
+ {
2
+ "_comment": "Configuration for `bun run maestro:classify` (scripts/classify-maestro-failures.mjs) — a DIAGNOSTIC that scores a red Maestro run before anybody attributes a regression to it. It gates nothing and cannot change any check's conclusion. This file is create-only: Lisa writes it once and never overwrites your tuned values.",
3
+
4
+ "_signInMarkers": "Selectors that identify a subflow as performing sign-in. Preamble identity is DERIVED from these rather than from a list of filenames, so the classifier cannot drift when a gate is edited. Replace the shipped values with the selectors YOUR sign-in flow uses. Leaving them unmatched is safe: nothing qualifies as a preamble and every failure reads as a product failure, which is the direction this tool is required to err in — a false 'product' costs an investigation, a false 'preamble' hides a real regression.",
5
+ "signInMarkers": ["landing:sign-in", "signin:email-input"],
6
+
7
+ "_knownIntermittent": "Flows that fail SOMETIMES on an unchanged build. Every entry MUST carry a `measured` block — `failures`/`runs` from a real repeated run, the ISO date it was measured, and the methodology behind it. An entry missing any of those is reported as a registry defect and annotates nothing, on purpose: an unmeasured 'known flake' entry is exactly how a real regression gets dismissed. Keep the narrative — what was ruled out, the local repro recipe — in your BDD README and point at it from `notes`; this file holds only the claim a machine can check. Remeasure and update `measuredAt` rather than letting a stale rate justify today's failure.",
8
+ "_exampleEntry": {
9
+ "flow": "saved-insight-save-and-unsave.yaml",
10
+ "platforms": ["android"],
11
+ "measured": {
12
+ "failures": 2,
13
+ "runs": 7,
14
+ "measuredAt": "2026-08-10",
15
+ "method": "seven local runs on one emulator (1080x2400 @ 420dpi, matching the CI AVD pins) against build 0.0.327, arm-alternated against 0.0.319 to rule out the suspected commit"
16
+ },
17
+ "ticket": "TUN-560",
18
+ "notes": "bdd/README.md#known-intermittent-flows"
19
+ },
20
+ "knownIntermittent": []
21
+ }
@@ -29,12 +29,11 @@
29
29
  }
30
30
  },
31
31
  "coverageFloor": {
32
- "_comment": "Committed traceability floor per platform. A ratchet: may rise, may never fall. Seeded at 0 so adopting on a brownfield app never red-gates CI before any scenario exists. Lowering one requires a coverageFloorBaseline record naming the exact change AND the maintainer-applied `bdd-floor-baseline` pull-request label.",
32
+ "_comment": "Committed traceability floor per platform: an ABSOLUTE BAR answering `is this platform below it right now`, evaluated only in enforced mode. It is NOT a ratchet — set it once at adoption to the honest measured number (or leave it at 0) and stop touching it. Coverage already earned is protected separately and deterministically: an obligation covered at the base revision may not stop being covered, and new behavior arrives mapped or waived. Set a floor for EVERY platform above before flipping adoption.state to enforced.",
33
33
  "web": 0,
34
34
  "ios": 0,
35
35
  "android": 0
36
36
  },
37
- "coverageFloorBaseline": [],
38
37
  "retirements": [],
39
38
  "trackers": {
40
39
  "_comment": "Tracker-tag grammar is portfolio-wide; the allowed keys and repos are per-repo. Key style: @TUN-123, @SE-6833. Repo-issue style: @gh-2394 (this repo) or @gh-wiki-124 (a sibling repo). Tags are validated for syntax and membership only — the gate never contacts a tracker, so an unreachable issue can never block a merge.",
@@ -11,6 +11,7 @@
11
11
  "maestro:test:android": "maestro test -p android .maestro/flows",
12
12
  "maestro:test:smoke": "maestro test -p ios --include-tags=smoke .maestro/flows",
13
13
  "maestro:studio": "maestro studio",
14
+ "maestro:classify": "node scripts/classify-maestro-failures.mjs",
14
15
  "bdd:coverage": "node scripts/check-bdd-coverage.mjs",
15
16
  "bdd:coverage:write": "node scripts/check-bdd-coverage.mjs --write",
16
17
  "bdd:matrix": "node scripts/bdd-matrix.mjs --write",
package/package.json CHANGED
@@ -120,7 +120,7 @@
120
120
  }
121
121
  },
122
122
  "name": "@codyswann/lisa",
123
- "version": "3.0.0",
123
+ "version": "3.2.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": "3.0.0",
3
+ "version": "3.2.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": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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
 
@@ -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.
@@ -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.
@@ -38,20 +38,15 @@ DB_ID=$(jq -r '.notion.prdDatabaseId // empty' .lisa.config.json)
38
38
  [ -z "$DB_ID" ] && { echo "Error: notion.prdDatabaseId not set. Run /lisa:setup:notion." >&2; exit 1; }
39
39
  ```
40
40
 
41
- 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 workspace are skipped, not used.
41
+ 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, the interactive MCP is the fallback — not a Notion-local choice. Identity-match is verified before any operation; substrates authenticated as a different workspace are skipped, not used, at **every** tier.
42
42
 
43
43
  ```bash
44
44
  substrate=""
45
45
 
46
- # Tier 1: Notion MCP (identity-matched by fetching the configured PRD database)
47
- # Pseudo-code; actual call is the MCP tool invocation.
48
- # Try to fetch DB_ID through the MCP. Success MCP is authed to the right workspace.
49
- # 404 / object_not_found MCP is authed elsewhere (or unauthenticated). Skip.
50
- if mcp_notion_can_fetch_database "$DB_ID"; then
51
- substrate="mcp"
52
- fi
53
-
54
- # Tier 2: curl + API token
46
+ # Tier 1: curl + API token the configured-provider substrate, resolved through
47
+ # lisa-secrets-access. Leads because it is identical on a laptop, in CI, in a cloud
48
+ # routine, and in a subagent, and because its workspace binding travels with the
49
+ # request instead of coming from ambient browser-session state.
55
50
  read_notion_token() {
56
51
  local workspace="$1"
57
52
  [ -n "$NOTION_API_TOKEN" ] && { echo "$NOTION_API_TOKEN"; return; }
@@ -120,12 +115,28 @@ if [ -n "$TOKEN" ]; then
120
115
  "https://api.notion.com/v1/users/me")
121
116
  me_workspace=$(echo "$me" | jq -r '.bot.workspace_name // .bot.workspace_id // empty')
122
117
  if [ -n "$me_workspace" ] && [ "$me_workspace" = "$WORKSPACE" ]; then
123
- : ${substrate:=curl}
118
+ substrate="curl"
124
119
  elif [ -n "$me_workspace" ]; then
120
+ # A present-but-wrong token fails the gate rather than deferring to the MCP.
121
+ # Silently succeeding through an MCP authenticated elsewhere is the exact bug
122
+ # the precedence contract exists to surface.
125
123
  echo "Warning: Notion token belongs to workspace '$me_workspace' but config declares '$WORKSPACE'. Skipping curl tier." >&2
126
124
  fi
127
125
  fi
128
126
 
127
+ # Tier 2: Notion MCP — first-class fallback, used when tier 1 is genuinely
128
+ # unavailable (no token, no curl adapter for the operation, or Notion API outage).
129
+ # Identity-matched by fetching the configured PRD database.
130
+ # Pseudo-code; actual call is the MCP tool invocation.
131
+ # Try to fetch DB_ID through the MCP. Success → MCP is authed to the right workspace.
132
+ # 404 / object_not_found → MCP is authed elsewhere (or unauthenticated). Skip.
133
+ if mcp_notion_can_fetch_database "$DB_ID"; then
134
+ : ${substrate:=mcp}
135
+ # Mark the MCP available even when curl already won tier 1 — the dispatch table
136
+ # falls through to it for operations curl has no adapter for.
137
+ mcp_available=true
138
+ fi
139
+
129
140
  # Fail loudly with actionable remediation if nothing works.
130
141
  if [ -z "$substrate" ]; then
131
142
  # Detect plugin enablement state for the suggestion.
@@ -136,14 +147,19 @@ if [ -z "$substrate" ]; then
136
147
  cat >&2 <<EOF
137
148
  Error: no Notion access substrate available for workspace '$WORKSPACE'.
138
149
 
139
- Attempted:
140
- MCP — $([ "$plugin_enabled_global" = "true" ] || [ "$plugin_enabled_project" = "true" ] || [ "$plugin_enabled_local" = "true" ] && echo "plugin enabled but not authenticated or cannot fetch configured prdDatabaseId" || echo "plugin not enabled in any settings.json scope")
150
+ Attempted (in credential-substrate-precedence order):
141
151
  curl — no NOTION_API_TOKEN found for $WORKSPACE (env, slug-suffixed env, or keychain) OR token belongs to a different workspace
152
+ MCP — $([ "$plugin_enabled_global" = "true" ] || [ "$plugin_enabled_project" = "true" ] || [ "$plugin_enabled_local" = "true" ] && echo "plugin enabled but not authenticated or cannot fetch configured prdDatabaseId" || echo "plugin not enabled in any settings.json scope")
153
+
154
+ Remediation paths (the first is the contract's primary path):
142
155
 
143
- Remediation paths (pick one):
156
+ 1. Provision an internal-integration API token — works headless, in CI, and in
157
+ multi-workspace setups, and is the substrate this project resolves first.
144
158
 
145
- 1. Install the Notion MCP plugin (local scope — per-developer, gitignored).
146
- This is the simplest path for single-workspace developers.
159
+ Run /lisa:setup:notion guided flow with clipboard-piped keychain store.
160
+
161
+ 2. Install the Notion MCP plugin (local scope — per-developer, gitignored).
162
+ The supported fallback when no credentials provider is configured.
147
163
 
148
164
  Run in your terminal:
149
165
 
@@ -157,10 +173,6 @@ Remediation paths (pick one):
157
173
  Also share the configured prdDatabaseId with the integration via
158
174
  the page's '•••' menu → Connections.
159
175
 
160
- 2. Provision an internal-integration API token (headless / CI / multi-workspace).
161
-
162
- Run /lisa:setup:notion — guided flow with clipboard-piped keychain store.
163
-
164
176
  EOF
165
177
  exit 1
166
178
  fi
@@ -225,14 +237,15 @@ exec_op() {
225
237
  ## Invariants
226
238
 
227
239
  - Caller skills never call `curl https://api.notion.com/...` or any `mcp__*notion*` tool directly. They invoke this skill via the Skill tool with an operation name and arguments.
228
- - Substrate is selected per skill invocation following the tier ladder. The first tier that's available AND identity-matches `notion.workspaceId` wins.
229
- - The connection-match check is mandatory at every tier. Skipping it (because "the user obviously meant this workspace") is forbidden silent cross-workspace operations are exactly the multi-account hazard this design exists to prevent.
240
+ - Substrate is selected per skill invocation following the tier ladder defined by the shared `credential-substrate-precedence` contract — internal-integration token first, Notion MCP as fallback. The first tier that's available AND identity-matches `notion.workspaceId` wins. Do not restate or locally override the ordering here.
241
+ - The Notion MCP stays a first-class fallback, not a removed tier: it is the substrate whenever no token is configured, the operation has no curl adapter, or the Notion API is failing.
242
+ - The connection-match check is mandatory at every tier. Skipping it (because "the user obviously meant this workspace") is forbidden — silent cross-workspace operations are exactly the multi-account hazard this design exists to prevent. A present-but-wrong token fails the gate rather than deferring to an MCP authenticated somewhere else.
230
243
  - API tokens never mutate. If the configured workspace's token is wrong or missing, fail loudly and tell the user to run `/lisa:setup:notion`.
231
244
  - `Notion-Version` is pinned to `2022-06-28` — the version every existing notion-* skill targets. Bumping it is a coordinated change across the access skill and all callers.
232
245
 
233
246
  ## Headless behavior
234
247
 
235
- In a headless / non-interactive context (no TTY, `CI=true`, or `-p` mode), the MCP tier is unavailable (its OAuth flow needs a browser). The ladder collapses to curl + `NOTION_API_TOKEN`. Same skill code runs identically; only the substrate changes.
248
+ In a headless / non-interactive context (no TTY, `CI=true`, or `-p` mode), the MCP tier is unavailable (its OAuth flow needs a browser) and the ladder collapses to curl + `NOTION_API_TOKEN` — which is already tier 1 interactively. That is the point of the ordering: headless and interactive sessions take the **same primary path**, so a credential problem reproduces on a laptop instead of only in cron (`credential-substrate-precedence`, "headless parity"). Same skill code runs identically; only the availability of the fallback changes.
236
249
 
237
250
  ## Per-page sharing prerequisite
238
251
 
@@ -22,13 +22,21 @@ Return parsed JSON in a `<result>` block.
22
22
 
23
23
  ## Substrate Selection
24
24
 
25
- Probe in order:
25
+ Probe in order — the ordering is the shared `credential-substrate-precedence`
26
+ contract, not a PostHog-local choice. The first tier that is ready **and**
27
+ identity-matches the configured project is used; a substrate authenticated against
28
+ a different project is skipped, never used.
26
29
 
27
- 1. PostHog MCP, if available and authenticated.
28
- 2. `POSTHOG_PERSONAL_API_KEY` bearer token against the configured PostHog host.
30
+ 1. **Tier 1 configured-provider substrate: `POSTHOG_PERSONAL_API_KEY`** bearer
31
+ token against the configured PostHog host, resolved through
32
+ `lisa-secrets-access`.
33
+ 2. **Tier 2 — interactive MCP fallback: PostHog MCP**, if available and
34
+ authenticated. Used when tier 1 is genuinely unavailable: no
35
+ `POSTHOG_PERSONAL_API_KEY`, no REST adapter for the operation, or a PostHog
36
+ outage.
29
37
 
30
- PostHog documents personal API keys and bearer authentication. The headless REST
31
- tier uses:
38
+ PostHog documents personal API keys and bearer authentication, and the same key
39
+ works interactively and headlessly — which is why it leads. The REST tier uses:
32
40
 
33
41
  ```bash
34
42
  POSTHOG_HOST=${POSTHOG_HOST:-https://app.posthog.com}
@@ -54,7 +62,9 @@ Error: no PostHog access substrate available. Authenticate the PostHog MCP or se
54
62
 
55
63
  ## Invariants
56
64
 
57
- - Fallback is gated on `POSTHOG_PERSONAL_API_KEY`.
65
+ - Tier order is `credential-substrate-precedence`: `POSTHOG_PERSONAL_API_KEY`
66
+ first, the PostHog MCP as a preserved first-class fallback. Identity-match
67
+ against the configured project is mandatory on every tier.
58
68
  - `POSTHOG_HOST` defaults to PostHog Cloud but can point at a self-hosted
59
69
  deployment.
60
70
  - Consumer skills do not embed PostHog REST paths.