@codyswann/lisa 3.4.0 → 3.5.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 (206) hide show
  1. package/README.md +22 -0
  2. package/all/copy-contents/gitignore +7 -0
  3. package/all/copy-overwrite/scripts/lisa-command-envelope.mjs +44 -4
  4. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +31 -0
  5. package/all/copy-overwrite/scripts/lisa-schema-validate.mjs +125 -47
  6. package/all/copy-overwrite/scripts/lisa-work-item.mjs +24 -2
  7. package/cdk/merge/.oxlintrc.json +1 -1
  8. package/dist/cli/apply.d.ts.map +1 -1
  9. package/dist/cli/apply.js +48 -13
  10. package/dist/cli/apply.js.map +1 -1
  11. package/dist/cli/doctor-apply-freshness.d.ts +29 -0
  12. package/dist/cli/doctor-apply-freshness.d.ts.map +1 -0
  13. package/dist/cli/doctor-apply-freshness.js +194 -0
  14. package/dist/cli/doctor-apply-freshness.js.map +1 -0
  15. package/dist/cli/doctor-learnings-merge-driver.d.ts +23 -0
  16. package/dist/cli/doctor-learnings-merge-driver.d.ts.map +1 -0
  17. package/dist/cli/doctor-learnings-merge-driver.js +73 -0
  18. package/dist/cli/doctor-learnings-merge-driver.js.map +1 -0
  19. package/dist/cli/doctor-readiness-workflows.js +2 -2
  20. package/dist/cli/doctor-readiness-workflows.js.map +1 -1
  21. package/dist/cli/doctor.d.ts.map +1 -1
  22. package/dist/cli/doctor.js +13 -0
  23. package/dist/cli/doctor.js.map +1 -1
  24. package/dist/cli/ui-ci-quality-jobs-parse.js +2 -2
  25. package/dist/cli/ui-ci-quality-jobs-parse.js.map +1 -1
  26. package/dist/cli/ui-deploy-pipeline-model.d.ts +4 -0
  27. package/dist/cli/ui-deploy-pipeline-model.d.ts.map +1 -1
  28. package/dist/cli/ui-deploy-pipeline-model.js +2 -2
  29. package/dist/cli/ui-deploy-pipeline-model.js.map +1 -1
  30. package/dist/codex/agent-transformer.js +2 -2
  31. package/dist/codex/agent-transformer.js.map +1 -1
  32. package/dist/codex/command-skill-transformer.js +3 -3
  33. package/dist/codex/command-skill-transformer.js.map +1 -1
  34. package/dist/codex/scripts/block-no-verify.sh +53 -4
  35. package/dist/core/apply-receipt.d.ts +68 -0
  36. package/dist/core/apply-receipt.d.ts.map +1 -0
  37. package/dist/core/apply-receipt.js +107 -0
  38. package/dist/core/apply-receipt.js.map +1 -0
  39. package/dist/core/learnings-merge-driver-install.d.ts +34 -0
  40. package/dist/core/learnings-merge-driver-install.d.ts.map +1 -1
  41. package/dist/core/learnings-merge-driver-install.js +36 -0
  42. package/dist/core/learnings-merge-driver-install.js.map +1 -1
  43. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  44. package/dist/core/upstream-evidence-manifest.js +62 -39
  45. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  46. package/dist/migrations/ensure-lisa-postinstall.d.ts +33 -0
  47. package/dist/migrations/ensure-lisa-postinstall.d.ts.map +1 -1
  48. package/dist/migrations/ensure-lisa-postinstall.js +47 -5
  49. package/dist/migrations/ensure-lisa-postinstall.js.map +1 -1
  50. package/dist/migrations/ensure-oxlint-base-configs.d.ts +42 -0
  51. package/dist/migrations/ensure-oxlint-base-configs.d.ts.map +1 -0
  52. package/dist/migrations/ensure-oxlint-base-configs.js +212 -0
  53. package/dist/migrations/ensure-oxlint-base-configs.js.map +1 -0
  54. package/dist/migrations/index.d.ts +1 -0
  55. package/dist/migrations/index.d.ts.map +1 -1
  56. package/dist/migrations/index.js +3 -0
  57. package/dist/migrations/index.js.map +1 -1
  58. package/dist/opencode/command-transformer.js +3 -3
  59. package/dist/opencode/command-transformer.js.map +1 -1
  60. package/dist/utils/yaml.d.ts +54 -0
  61. package/dist/utils/yaml.d.ts.map +1 -0
  62. package/dist/utils/yaml.js +124 -0
  63. package/dist/utils/yaml.js.map +1 -0
  64. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +104 -19
  65. package/expo/copy-overwrite/scripts/bdd/contract.mjs +20 -1
  66. package/expo/copy-overwrite/scripts/bdd/discover.mjs +53 -10
  67. package/expo/copy-overwrite/scripts/bdd/parse.mjs +25 -3
  68. package/expo/copy-overwrite/scripts/bdd/render.mjs +2 -1
  69. package/expo/copy-overwrite/scripts/bdd/report.mjs +26 -12
  70. package/expo/copy-overwrite/scripts/bdd/validate.mjs +2 -2
  71. package/expo/copy-overwrite/scripts/bdd/waivers.mjs +19 -4
  72. package/expo/copy-overwrite/scripts/bdd-matrix.mjs +2 -2
  73. package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +58 -16
  74. package/expo/create-only/.github/required-checks.json +10 -4
  75. package/expo/merge/.oxlintrc.json +1 -1
  76. package/harper-fabric/merge/.oxlintrc.json +1 -1
  77. package/nestjs/create-only/.github/required-checks.json +57 -0
  78. package/nestjs/merge/.oxlintrc.json +1 -1
  79. package/package.json +1 -1
  80. package/phaser/merge/.oxlintrc.json +1 -1
  81. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +7 -0
  84. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +1 -1
  85. package/plugins/lisa/.codex-plugin/skills/lisa-jam-access/SKILL.md +37 -2
  86. package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +7 -0
  87. package/plugins/lisa/.codex-plugin/skills/lisa-posthog-access/SKILL.md +40 -3
  88. package/plugins/lisa/.codex-plugin/skills/lisa-sentry-access/SKILL.md +44 -3
  89. package/plugins/lisa/.codex-plugin/skills/lisa-sonarcloud-access/SKILL.md +56 -0
  90. package/plugins/lisa/.codex-plugin/skills/lisa-track/SKILL.md +1 -1
  91. package/plugins/lisa/hooks/block-no-verify.agy.sh +53 -4
  92. package/plugins/lisa/hooks/block-no-verify.sh +31 -0
  93. package/plugins/lisa/rules/eager/credential-substrate-precedence.md +3 -1
  94. package/plugins/lisa/rules/eager/tracked-work.md +1 -1
  95. package/plugins/lisa/rules/reference/credential-substrate-precedence.md +20 -0
  96. package/plugins/lisa/rules/reference/tracked-work.md +4 -2
  97. package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +7 -0
  98. package/plugins/lisa/skills/lisa-implement/SKILL.md +1 -1
  99. package/plugins/lisa/skills/lisa-jam-access/SKILL.md +37 -2
  100. package/plugins/lisa/skills/lisa-notion-access/SKILL.md +7 -0
  101. package/plugins/lisa/skills/lisa-posthog-access/SKILL.md +40 -3
  102. package/plugins/lisa/skills/lisa-sentry-access/SKILL.md +44 -3
  103. package/plugins/lisa/skills/lisa-sonarcloud-access/SKILL.md +56 -0
  104. package/plugins/lisa/skills/lisa-track/SKILL.md +1 -1
  105. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +53 -4
  106. package/plugins/lisa-agy/plugin.json +1 -1
  107. package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +7 -0
  108. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +1 -1
  109. package/plugins/lisa-agy/skills/lisa-jam-access/SKILL.md +37 -2
  110. package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +7 -0
  111. package/plugins/lisa-agy/skills/lisa-posthog-access/SKILL.md +40 -3
  112. package/plugins/lisa-agy/skills/lisa-sentry-access/SKILL.md +44 -3
  113. package/plugins/lisa-agy/skills/lisa-sonarcloud-access/SKILL.md +56 -0
  114. package/plugins/lisa-agy/skills/lisa-track/SKILL.md +1 -1
  115. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  117. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  118. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  120. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-copilot/hooks/block-no-verify.sh +31 -0
  122. package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +3 -1
  123. package/plugins/lisa-copilot/rules/eager/tracked-work.md +1 -1
  124. package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +20 -0
  125. package/plugins/lisa-copilot/rules/reference/tracked-work.md +4 -2
  126. package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +7 -0
  127. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +1 -1
  128. package/plugins/lisa-copilot/skills/lisa-jam-access/SKILL.md +37 -2
  129. package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +7 -0
  130. package/plugins/lisa-copilot/skills/lisa-posthog-access/SKILL.md +40 -3
  131. package/plugins/lisa-copilot/skills/lisa-sentry-access/SKILL.md +44 -3
  132. package/plugins/lisa-copilot/skills/lisa-sonarcloud-access/SKILL.md +56 -0
  133. package/plugins/lisa-copilot/skills/lisa-track/SKILL.md +1 -1
  134. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  135. package/plugins/lisa-cursor/hooks/block-no-verify.sh +31 -0
  136. package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +20 -0
  137. package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +3 -1
  138. package/plugins/lisa-cursor/rules/tracked-work-reference.mdc +4 -2
  139. package/plugins/lisa-cursor/rules/tracked-work.mdc +1 -1
  140. package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +7 -0
  141. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +1 -1
  142. package/plugins/lisa-cursor/skills/lisa-jam-access/SKILL.md +37 -2
  143. package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +7 -0
  144. package/plugins/lisa-cursor/skills/lisa-posthog-access/SKILL.md +40 -3
  145. package/plugins/lisa-cursor/skills/lisa-sentry-access/SKILL.md +44 -3
  146. package/plugins/lisa-cursor/skills/lisa-sonarcloud-access/SKILL.md +56 -0
  147. package/plugins/lisa-cursor/skills/lisa-track/SKILL.md +1 -1
  148. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  149. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  150. package/plugins/lisa-expo-agy/plugin.json +1 -1
  151. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  152. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  154. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  155. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  156. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  157. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  158. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  159. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  160. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  161. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  162. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  163. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  164. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  165. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  166. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  167. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  168. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  169. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  170. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  171. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  172. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  173. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  174. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  175. package/plugins/lisa-rails-agy/plugin.json +1 -1
  176. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  177. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  178. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  179. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  180. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  181. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  182. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  183. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  184. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  185. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  186. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  187. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  188. package/plugins/src/base/hooks/block-no-verify.agy.sh +53 -4
  189. package/plugins/src/base/hooks/block-no-verify.sh +31 -0
  190. package/plugins/src/base/rules/eager/credential-substrate-precedence.md +3 -1
  191. package/plugins/src/base/rules/eager/tracked-work.md +1 -1
  192. package/plugins/src/base/rules/reference/credential-substrate-precedence.md +20 -0
  193. package/plugins/src/base/rules/reference/tracked-work.md +4 -2
  194. package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +7 -0
  195. package/plugins/src/base/skills/lisa-implement/SKILL.md +1 -1
  196. package/plugins/src/base/skills/lisa-jam-access/SKILL.md +37 -2
  197. package/plugins/src/base/skills/lisa-notion-access/SKILL.md +7 -0
  198. package/plugins/src/base/skills/lisa-posthog-access/SKILL.md +40 -3
  199. package/plugins/src/base/skills/lisa-sentry-access/SKILL.md +44 -3
  200. package/plugins/src/base/skills/lisa-sonarcloud-access/SKILL.md +56 -0
  201. package/plugins/src/base/skills/lisa-track/SKILL.md +1 -1
  202. package/scripts/install-claude-plugins.sh +5 -1
  203. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +298 -26
  204. package/typescript/create-only/.github/required-checks.json +11 -6
  205. package/typescript/create-only/.github/workflows/required-checks-drift.yml +84 -0
  206. package/typescript/merge/.oxlintrc.json +1 -1
@@ -40,15 +40,38 @@ works interactively and headlessly — which is why it leads. The REST tier uses
40
40
 
41
41
  ```bash
42
42
  POSTHOG_HOST=${POSTHOG_HOST:-https://app.posthog.com}
43
+
44
+ # Resolve the key through the chokepoint before giving up on the environment.
45
+ # `$POSTHOG_PERSONAL_API_KEY` is the documented fallback, not the only rung:
46
+ # without this, a project that keeps its credentials in Bitwarden, Doppler, or
47
+ # AWS has no tier 1 path at all and silently resolves through the interactive
48
+ # MCP — the exact divergence `credential-substrate-precedence` exists to remove.
49
+ read_posthog_key() {
50
+ [ -n "${POSTHOG_PERSONAL_API_KEY:-}" ] && { echo "$POSTHOG_PERSONAL_API_KEY"; return; }
51
+ local resolver
52
+ for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
53
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
54
+ if [ -f "$resolver" ]; then
55
+ local via_lisa
56
+ via_lisa=$(node "$resolver" get POSTHOG_PERSONAL_API_KEY 2>/dev/null) \
57
+ && [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
58
+ break
59
+ fi
60
+ done
61
+ return 1
62
+ }
63
+
43
64
  posthog_api() {
44
65
  local path="$1"
45
66
  local method="${2:-GET}"
46
67
  local body="${3:-}"
47
- [ -n "$POSTHOG_PERSONAL_API_KEY" ] || {
48
- echo "Error: POSTHOG_PERSONAL_API_KEY is not set." >&2
68
+ local key
69
+ key=$(read_posthog_key) || {
70
+ echo "Error: no PostHog key. Set POSTHOG_PERSONAL_API_KEY, or store it as" >&2
71
+ echo "POSTHOG_PERSONAL_API_KEY in this project's secrets provider." >&2
49
72
  return 1
50
73
  }
51
- local args=(-sS -X "$method" -H "Authorization: Bearer $POSTHOG_PERSONAL_API_KEY")
74
+ local args=(-sS -X "$method" -H "Authorization: Bearer $key")
52
75
  [ -n "$body" ] && args+=(-H "Content-Type: application/json" --data-binary "$body")
53
76
  curl "${args[@]}" "${POSTHOG_HOST%/}/api${path}"
54
77
  }
@@ -60,11 +83,25 @@ If neither tier works, fail with:
60
83
  Error: no PostHog access substrate available. Authenticate the PostHog MCP or set POSTHOG_PERSONAL_API_KEY.
61
84
  ```
62
85
 
86
+ ## Mutation boundary
87
+
88
+ Every operation in the Invocation Contract is **read-only** — analytics
89
+ retrieval. `query` is an HTTP POST, but it reads: it submits a query body and
90
+ changes no PostHog state. So the `credential-substrate-precedence` guarded
91
+ fallback for mutating operations (write, read back, assert the tenant from the
92
+ response, roll back on mismatch) is not engaged here, and a failed tier is
93
+ simply skipped. Adding a genuinely mutating operation — creating an insight,
94
+ editing a feature flag — pulls that protocol in: a write of unknown outcome MUST
95
+ reconcile by read-back before any retry.
96
+
63
97
  ## Invariants
64
98
 
65
99
  - Tier order is `credential-substrate-precedence`: `POSTHOG_PERSONAL_API_KEY`
66
100
  first, the PostHog MCP as a preserved first-class fallback. Identity-match
67
101
  against the configured project is mandatory on every tier.
102
+ - The key is resolved through `lisa-secrets-access`, with the bare
103
+ `POSTHOG_PERSONAL_API_KEY` environment variable as the documented fallback.
104
+ Never read a second credential store directly.
68
105
  - `POSTHOG_HOST` defaults to PostHog Cloud but can point at a self-hosted
69
106
  deployment.
70
107
  - Consumer skills do not embed PostHog REST paths.
@@ -39,28 +39,69 @@ Sentry documents API auth tokens for REST API calls, and the same token works
39
39
  interactively and headlessly — which is why it leads. The REST tier uses:
40
40
 
41
41
  ```bash
42
+ # Resolve the token through the chokepoint before giving up on the environment.
43
+ # `$SENTRY_AUTH_TOKEN` is the documented fallback, not the only rung: without
44
+ # this, a project that keeps its credentials in Bitwarden, Doppler, or AWS has
45
+ # no tier 1 path at all and silently resolves through the interactive MCP —
46
+ # the exact divergence `credential-substrate-precedence` exists to remove.
47
+ # Mirrors `linear-access`, `atlassian-access`, and `notion-access`.
48
+ read_sentry_token() {
49
+ [ -n "${SENTRY_AUTH_TOKEN:-}" ] && { echo "$SENTRY_AUTH_TOKEN"; return; }
50
+ local resolver
51
+ for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
52
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
53
+ if [ -f "$resolver" ]; then
54
+ local via_lisa
55
+ via_lisa=$(node "$resolver" get SENTRY_AUTH_TOKEN 2>/dev/null) \
56
+ && [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
57
+ break
58
+ fi
59
+ done
60
+ return 1
61
+ }
62
+
42
63
  sentry_api() {
43
64
  local path="$1"
44
- [ -n "$SENTRY_AUTH_TOKEN" ] || {
45
- echo "Error: SENTRY_AUTH_TOKEN is not set." >&2
65
+ local token
66
+ token=$(read_sentry_token) || {
67
+ echo "Error: no Sentry auth token. Set SENTRY_AUTH_TOKEN, or store it as" >&2
68
+ echo "SENTRY_AUTH_TOKEN in this project's secrets provider." >&2
46
69
  return 1
47
70
  }
48
71
  curl -sS "https://sentry.io/api/0${path}" \
49
- -H "Authorization: Bearer $SENTRY_AUTH_TOKEN"
72
+ -H "Authorization: Bearer $token"
50
73
  }
51
74
  ```
52
75
 
76
+ Tier 1a authenticates `sentry-cli` from the **same resolved token**
77
+ (`SENTRY_AUTH_TOKEN=$(read_sentry_token) sentry-cli …`) — never from a second
78
+ credential store, and never from an interactive `sentry-cli login` keychain.
79
+
53
80
  If neither tier works, fail with:
54
81
 
55
82
  ```text
56
83
  Error: no Sentry access substrate available. Authenticate Sentry MCP/CLI or set SENTRY_AUTH_TOKEN.
57
84
  ```
58
85
 
86
+ ## Mutation boundary
87
+
88
+ Every operation in the Invocation Contract is **read-only** — issue, event, and
89
+ release retrieval. So the `credential-substrate-precedence` guarded fallback for
90
+ mutating operations (write, read back, assert the tenant from the response, roll
91
+ back on mismatch) is not engaged here, and a failed tier is simply skipped. Any
92
+ future operation that mutates Sentry state — resolving an issue, editing a
93
+ release — is a write and MUST reconcile by read-back under that protocol before
94
+ any retry; do not add one to the contract without it.
95
+
59
96
  ## Invariants
60
97
 
61
98
  - Tier order is `credential-substrate-precedence`: `SENTRY_AUTH_TOKEN` first, the
62
99
  Sentry MCP as a preserved first-class fallback. Identity-match against the
63
100
  configured org/project is mandatory on every tier.
101
+ - The token is resolved through `lisa-secrets-access`, with the bare
102
+ `SENTRY_AUTH_TOKEN` environment variable as the documented fallback. Never read
103
+ a second credential store (OS keychain, `~/.sentryclirc` token) directly — the
104
+ one-store rule lives at the chokepoint.
64
105
  - Org/project come from `.sentryclirc`, `.lisa.config.json`, or explicit
65
106
  operation args; never infer by searching all accessible orgs.
66
107
  - Consumer skills do not embed Sentry REST paths.
@@ -34,6 +34,49 @@ SonarQube Cloud org. Do **not** substitute the raw MCP-image names
34
34
  running the Docker image directly, and the `sonar` CLI ignores them (auth exits
35
35
  non-zero). The CI scan gate's `SONAR_TOKEN` is a third, separate name.
36
36
 
37
+ ### Where those variables come from
38
+
39
+ The `sonar` CLI reads them **from the environment only** — it has no provider
40
+ integration of its own — so on a surface that deliberately materializes nothing
41
+ to disk the credential can be provisioned and unreachable at the same time.
42
+ Resolve it through `lisa-secrets-access` and export it into the launching
43
+ process, exactly as the shared `sonar-secrets.sh` hook already does. The bare
44
+ `$SONARQUBE_CLI_TOKEN` in the environment is the documented **fallback** rung,
45
+ not the only one:
46
+
47
+ ```bash
48
+ # Preferred: the one sanctioned reader. Without this rung a project keeping its
49
+ # credentials in Bitwarden, Doppler, or AWS cannot authenticate the MCP at all,
50
+ # and Sonar access degrades to "run `sonar auth login` in a browser" — dead in
51
+ # cron, CI, and cloud sessions. See `credential-substrate-precedence`.
52
+ read_sonar_secret() {
53
+ local name="$1"
54
+ [ -n "${!name:-}" ] && { echo "${!name}"; return; }
55
+ local resolver
56
+ for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
57
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
58
+ if [ -f "$resolver" ]; then
59
+ local via_lisa
60
+ via_lisa=$(node "$resolver" get "$name" 2>/dev/null) \
61
+ && [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
62
+ break
63
+ fi
64
+ done
65
+ return 1
66
+ }
67
+
68
+ # Exported into this process only. Writing it anywhere durable would create a
69
+ # second live copy of a credential whose single store is the provider.
70
+ SONARQUBE_CLI_TOKEN=$(read_sonar_secret SONARQUBE_CLI_TOKEN) && export SONARQUBE_CLI_TOKEN
71
+ SONARQUBE_CLI_ORG=$(read_sonar_secret SONARQUBE_CLI_ORG) && export SONARQUBE_CLI_ORG
72
+ ```
73
+
74
+ `sonar-secrets.sh` runs the same resolution with a longer search path (it also
75
+ probes `.codex/skills/…` and `node_modules/@codyswann/lisa/plugins/lisa/…`,
76
+ because a hook fires in checkouts that never installed an agent plugin) and with
77
+ a timeout, because it sits in front of every prompt. Use its search order
78
+ verbatim when this skill runs inside a hook.
79
+
37
80
  Wiring is performed once by `/lisa:setup:sonar` (which drives `sonar integrate
38
81
  <agent>`); this access layer assumes the MCP is already wired. This is distinct
39
82
  from the CI `SONAR_TOKEN` secret that authenticates the SonarCloud scan job in
@@ -79,9 +122,22 @@ Pass the project key through the tool's `projectKey` argument (or rely on a
79
122
  server-configured `SONARQUBE_PROJECT_KEY`); pass `branch` / `pullRequest` where the
80
123
  tool accepts them.
81
124
 
125
+ ## Mutation boundary
126
+
127
+ Every operation in the map above is **read-only** — quality, coverage, and
128
+ security data. So the `credential-substrate-precedence` guarded fallback for
129
+ mutating operations (write, read back, assert the tenant from the response, roll
130
+ back on mismatch) is not engaged here; with one substrate there is nothing to
131
+ fall back to in any case. A future operation that mutates Sonar state — marking
132
+ a hotspot safe, changing an issue's status — is a write and MUST reconcile by
133
+ read-back before any retry.
134
+
82
135
  ## Invariants
83
136
 
84
137
  - The official SonarQube MCP is the only substrate; there is no REST fallback.
138
+ - `SONARQUBE_CLI_*` values are resolved through `lisa-secrets-access` and
139
+ exported in-process, with the bare environment variables as the documented
140
+ fallback. Never write them to a dotfile or a `.env` on a local surface.
85
141
  - Auth is env-var only (`SONARQUBE_CLI_TOKEN` [+ `SONARQUBE_CLI_ORG` | `SONARQUBE_CLI_SERVER`]);
86
142
  never the interactive `sonar auth login` keychain flow inside a factory.
87
143
  - Sonar host access requires the host (`sonarcloud.io`, `sonarqube.us`, or the
@@ -47,7 +47,7 @@ This is intentionally conservative: ambiguity creates one explicit work item ins
47
47
  2. Persist only the canonical reference in worktree-local machine state:
48
48
 
49
49
  ```bash
50
- node scripts/lisa-work-item.mjs bind <canonical-ref>
50
+ node scripts/lisa-work-item.mjs link <canonical-ref>
51
51
  ```
52
52
 
53
53
  3. Read the binding back through `node scripts/lisa-work-item.mjs current` and require it to equal the canonical reference. If binding fails, stop before durable project work.
@@ -102,16 +102,65 @@ def disables_verification(token):
102
102
  and NO_VERIFY.startswith(token)
103
103
  )
104
104
 
105
+ # The only relocations that keep a repo's own hooks in play. Anything else —
106
+ # including "" and /dev/null, which are simply the two most obvious members of
107
+ # the blocked set rather than special cases — is refused.
108
+ PERMITTED_HOOKS_PATHS = {".husky", ".githooks"}
109
+
110
+
111
+ def is_permitted_hooks_path(value):
112
+ """Whether a core.hooksPath value relocates hooks rather than disabling them.
113
+
114
+ Args:
115
+ value: The raw core.hooksPath value as it appeared on the command line.
116
+
117
+ Returns:
118
+ True if the path is an established in-repo hooks directory.
119
+ """
120
+ cleaned = value.strip().strip("'\"")
121
+ if cleaned.startswith("./"):
122
+ cleaned = cleaned[2:]
123
+ return cleaned.rstrip("/") in PERMITTED_HOOKS_PATHS
124
+
125
+
105
126
  for i, token in enumerate(normalized_tokens):
106
127
  if disables_verification(token):
107
128
  sys.exit(1)
108
129
  if token == "HUSKY=0" or token.startswith("HUSKY_SKIP_HOOKS="):
109
130
  sys.exit(1)
110
- if token.startswith("core.hooksPath="):
111
- value = token.split("=", 1)[1]
112
- if value in ("", "/dev/null"):
131
+ # Allowlist the destinations, do not denylist the disabling ones: hooks are
132
+ # disabled just as completely by any directory that happens to contain none
133
+ # (`-c core.hooksPath=/tmp/empty`), so the set that DISABLES hooks is
134
+ # unbounded while the set that legitimately relocates them is tiny. Matched
135
+ # case-insensitively because git config names are.
136
+ lowered = token.lower()
137
+ if lowered.startswith("core.hookspath="):
138
+ if not is_permitted_hooks_path(token.split("=", 1)[1]):
139
+ sys.exit(1)
140
+ if lowered == "core.hookspath" and i + 1 < len(normalized_tokens):
141
+ if not is_permitted_hooks_path(normalized_tokens[i + 1]):
142
+ sys.exit(1)
143
+ # `--config-env=core.hooksPath=SOMEVAR` reads the path out of the named env
144
+ # var, so it is not in the command at all and cannot be allowlisted.
145
+ if lowered.startswith("--config-env="):
146
+ spec = token.split("=", 1)[1]
147
+ if spec.split("=", 1)[0].strip().lower() == "core.hookspath":
148
+ sys.exit(1)
149
+ # git also accepts `--config-env <name>=<envvar>` as two tokens. Guarding
150
+ # only the `=` spelling let the trailing `core.hooksPath=.husky` fall
151
+ # through to the allowlist above, which reads `.husky` as a path — but here
152
+ # it names an ENVIRONMENT VARIABLE, which can hold /dev/null. Checked at the
153
+ # `--config-env` token, which the loop reaches first.
154
+ if lowered == "--config-env" and i + 1 < len(normalized_tokens):
155
+ spec = normalized_tokens[i + 1]
156
+ if spec.split("=", 1)[0].strip().strip("'\"").lower() == "core.hookspath":
113
157
  sys.exit(1)
114
- if token == "core.hooksPath" and i + 1 < len(normalized_tokens) and normalized_tokens[i + 1] in ("", "/dev/null"):
158
+ # `GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath
159
+ # GIT_CONFIG_VALUE_0=/dev/null git ...` sets the same command-scope config
160
+ # via env-var-style assignments. The index is arbitrary below
161
+ # GIT_CONFIG_COUNT, so it is matched as `\d+` rather than pinned to 0.
162
+ key_match = re.match(r"git_config_key_\d+=(.*)$", lowered, re.DOTALL)
163
+ if key_match and key_match.group(1).strip().strip("'\"") == "core.hookspath":
115
164
  sys.exit(1)
116
165
 
117
166
  sys.exit(0)
@@ -186,6 +186,37 @@ for i, token in enumerate(normalized_tokens):
186
186
  spec = token.split("=", 1)[1]
187
187
  if spec.split("=", 1)[0].strip().lower() == "core.hookspath":
188
188
  sys.exit(1)
189
+ # git accepts `--config-env <name>=<envvar>` as TWO tokens as well as one,
190
+ # and guarding only the `=` spelling was worse than missing the separate
191
+ # form outright: the trailing `core.hooksPath=.husky` then fell through to
192
+ # the allowlist above, which reads `.husky` as a PATH and permits it. But
193
+ # here it is an ENVIRONMENT VARIABLE NAME, and `env '.husky=/dev/null' git
194
+ # --config-env core.hooksPath=.husky` really does resolve hooksPath to
195
+ # /dev/null. The allowlist was being used as the bypass.
196
+ #
197
+ # Checked at the `--config-env` token, which the loop reaches first, so the
198
+ # refusal happens before the value token can be mistaken for a path.
199
+ if lowered == "--config-env" and i + 1 < len(normalized_tokens):
200
+ spec = normalized_tokens[i + 1]
201
+ if spec.split("=", 1)[0].strip().strip("'\"").lower() == "core.hookspath":
202
+ sys.exit(1)
203
+ # `GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath
204
+ # GIT_CONFIG_VALUE_0=/dev/null git commit` sets command-scope config the
205
+ # same way `-c core.hooksPath=...` does — env-var-style assignments ahead of
206
+ # the invocation instead of a flag — so it disables every hook just as
207
+ # completely while matching none of the token shapes above. Upstream missed
208
+ # this until a downstream fork hardened its own copy against it, which is
209
+ # the one direction a guard must never be caught in.
210
+ #
211
+ # The index is matched as `\d+` rather than pinned to 0: git accepts any
212
+ # index below GIT_CONFIG_COUNT, so a single-index check is evaded by typing
213
+ # a 1. Refused outright, like --config-env=, because the path lives in a
214
+ # separate GIT_CONFIG_VALUE_<n> token that can be exported earlier,
215
+ # reordered, or left out entirely — there is nothing here to allowlist
216
+ # against.
217
+ key_match = re.match(r"git_config_key_\d+=(.*)$", lowered, re.DOTALL)
218
+ if key_match and key_match.group(1).strip().strip("'\"") == "core.hookspath":
219
+ sys.exit(1)
189
220
 
190
221
  sys.exit(0)
191
222
  PY
@@ -20,7 +20,9 @@ scope for that skill's work.
20
20
  `lisa-secrets-access`, chosen whenever its bootstrap credential is available **and**
21
21
  the resolved substrate identity-matches the configured tenant/workspace/site.
22
22
  `lisa-secrets-access` is the single chokepoint — never read an OS keychain a second
23
- time.
23
+ time. The two legacy OS-keychain rungs that remain (`lisa-atlassian-access`,
24
+ `lisa-notion-access`) are a dated migration ramp with a **removal date of
25
+ 2026-11-01**, not a standing exemption; no new access skill may add one.
24
26
  2. **Tier 2 — interactive MCP**, used only when tier 1 is *genuinely* unavailable:
25
27
  no bootstrap, no adapter for the operation (per-operation, not per-session), or a
26
28
  provider outage. "The MCP happens to be authenticated" and "tier 1 is slower" are
@@ -2,6 +2,6 @@
2
2
 
3
3
  Before the first durable project mutation (code, tests, config, docs, committed research/plans/findings, commits, or PRs), establish exactly one live tracker leaf through `lisa-track`. Read-only discussion and orientation are exempt only while they produce no durable artifact.
4
4
 
5
- The mandatory order is: live-validate an explicit ref, or conservatively search and create exactly one valid leaf through `lisa-tracker-write` when no unique match exists; idempotently claim it through `lisa-tracker-claim`; then persist and verify the worktree-local binding with `node scripts/lisa-work-item.mjs bind <ref>`. A tracker that answers no, and any claim or binding failure, blocks durable work; a tracker that cannot be reached does not block a commit — the offline checks still run, the skip is loud on stderr, and the required `Work-Item Traceability` check re-runs the live checks with credentials before anything merges.
5
+ The mandatory order is: live-validate an explicit ref, or conservatively search and create exactly one valid leaf through `lisa-tracker-write` when no unique match exists; idempotently claim it through `lisa-tracker-claim`; then persist and verify the worktree-local binding with `node scripts/lisa-work-item.mjs link <ref>`. A tracker that answers no, and any claim or binding failure, blocks durable work; a tracker that cannot be reached does not block a commit — the offline checks still run, the skip is loud on stderr, and the required `Work-Item Traceability` check re-runs the live checks with credentials before anything merges.
6
6
 
7
7
  Carry that canonical ref through the branch, every ordinary commit's `Work-Item:` trailer, the PR, usage/evidence, and `lisa-tracker-sync`. Hooks and CI never create tickets. Keep the binding through interruptions or blocked outcomes; run `node scripts/lisa-work-item.mjs clear` only after merge/deploy/verification, two-way linkage/evidence, and the tracker item have all reached true terminal completion.
@@ -139,6 +139,26 @@ operation, the fallback is guarded — never the normal path:
139
139
  A successful pre-flight switch is not sufficient for tenant safety — another process can
140
140
  mutate global state between the check and the write.
141
141
 
142
+ ## Legacy OS-keychain fallback — removal date 2026-11-01
143
+
144
+ Two access skills (`lisa-atlassian-access`, `lisa-notion-access`) still read an
145
+ OS keychain as a last rung below the chokepoint, because the guided
146
+ `/lisa:setup:atlassian` and `/lisa:setup:notion` flows wrote credentials there
147
+ before `lisa-secrets-access` existed. That rung is a **migration ramp with a
148
+ removal date, not a standing exemption**: it is a second reader of the same
149
+ credential, which is exactly how one credential ends up living in two places and
150
+ drifting, and a keychain entry is machine-local ambient state that no headless
151
+ surface can reach — so a project resting on it has no working tier 1 in cron, CI,
152
+ or a cloud session.
153
+
154
+ **Both rungs are deleted on 2026-11-01.** Before then, projects still on the
155
+ keychain path move the credential into their configured provider — re-running
156
+ `/lisa:setup:<vendor>` stores it through the chokepoint. After removal, a
157
+ keychain-only project fails loudly with the exact variable named (tier 3), which
158
+ is the intended outcome: it can never silently resolve through a substrate
159
+ authenticated elsewhere. No new access skill may add a keychain rung; a credential
160
+ the chokepoint cannot answer for is a setup gap to fix, not a store to add.
161
+
142
162
  ## Consequences to expect
143
163
 
144
164
  - **A stale or wrong token now fails identity-match instead of silently succeeding
@@ -9,9 +9,11 @@ Use `lisa-track` as the single entry point:
9
9
  1. An explicit ticket is live-read through `lisa-tracker-read` and rejected if it is missing, inaccessible, terminal, a container, outside the configured project, or outside the current repository.
10
10
  2. A plain-text request or specification file is searched conservatively within the configured project. Reuse only one uniquely high-confidence matching live leaf. If no unique match exists, create exactly one complete single-repository leaf through `lisa-tracker-write`; never create a thin placeholder or a container.
11
11
  3. Idempotently claim the resolved leaf through `lisa-tracker-claim`, which reuses the vendor build-intake claim semantics and post-read verifies the claimed-or-later state.
12
- 4. Before any durable repository work, persist the canonical reference with `node scripts/lisa-work-item.mjs bind <ref>` and verify the worktree-local binding.
12
+ 4. Before any durable repository work, persist the canonical reference with `node scripts/lisa-work-item.mjs link <ref>` and verify the worktree-local binding.
13
13
 
14
- The sequence is strict: **live validate/create -> claim -> bind -> durable work**. A tracker answer of no, and any claim or binding failure, blocks the work. A tracker that cannot be reached `gh` absent, or its credential refused does not block a commit: the offline checks still run, the skip is loud on stderr, and the required `Work-Item Traceability` check re-runs the live checks with credentials before anything merges. Any other tracker failure still blocks. Tool presence or stale session text is not access.
14
+ `link` is the spelling to use. `bind` is accepted as a permanent alias for the identical operation, but some agent harnesses Claude Code's worktree isolation among them refuse any command line containing the bare token `bind`, because it names a shell builtin that evaluates a string. Inside an isolated worktree that refusal makes the `bind` spelling unrunnable, so reach for `link` and never work around a blocked binding by hand-writing `Work-Item:` trailers.
15
+
16
+ The sequence is strict: **live validate/create -> claim -> link -> durable work**. A tracker answer of no, and any claim or binding failure, blocks the work. A tracker that cannot be reached — `gh` absent, or its credential refused — does not block a commit: the offline checks still run, the skip is loud on stderr, and the required `Work-Item Traceability` check re-runs the live checks with credentials before anything merges. Any other tracker failure still blocks. Tool presence or stale session text is not access.
15
17
 
16
18
  ## One canonical identity
17
19
 
@@ -69,6 +69,13 @@ read_atlassian_token() {
69
69
  # Legacy fallback: the OS keychain written by the guided /lisa:setup:atlassian
70
70
  # flow, for projects that have not adopted a credentials provider. Reached only
71
71
  # when the chokepoint is absent or has no entry.
72
+ #
73
+ # This rung is REMOVED ON 2026-11-01 — a dated migration ramp, not a standing
74
+ # exemption (see credential-substrate-precedence, "Legacy OS-keychain fallback
75
+ # — removal date"). A keychain entry is machine-local ambient state no headless
76
+ # surface can reach, so a project resting on it has no working tier 1 in cron,
77
+ # CI, or a cloud session. Re-run /lisa:setup:atlassian before that date to
78
+ # store ATLASSIAN_API_TOKEN through the chokepoint instead.
72
79
  case "$(uname -s)" in
73
80
  Darwin) security find-generic-password -s lisa-atlassian -a "$email" -w 2>/dev/null ;;
74
81
  Linux) command -v secret-tool >/dev/null && secret-tool lookup service lisa-atlassian account "$email" 2>/dev/null ;;
@@ -64,7 +64,7 @@ The input-resolver invokes `lisa-track $ARGUMENTS` and owns its complete resolve
64
64
  - **Bind before durable work:** only after the verified claim, run:
65
65
 
66
66
  ```bash
67
- node scripts/lisa-work-item.mjs bind <canonical-ref>
67
+ node scripts/lisa-work-item.mjs link <canonical-ref>
68
68
  ```
69
69
 
70
70
  Require a successful readback of that worktree-local binding. On detached HEAD, `branch: null` is the expected pending binding; after branch creation the mandatory `attach-branch` step below must replace it before any commit. Tracker or binding failure stops the flow; never continue untracked.
@@ -39,20 +39,55 @@ headlessly — which is why it leads. The CLI tier uses:
39
39
  ```bash
40
40
  curl -fsSL https://native.jam.dev/install | bash
41
41
  export PATH="$HOME/.local/bin:$PATH"
42
- printf '%s' "$JAM_PAT" | jam auth login --token
42
+
43
+ # Resolve the PAT through the chokepoint before giving up on the environment.
44
+ # `$JAM_PAT` is the documented fallback, not the only rung: without this, a
45
+ # project that keeps its credentials in Bitwarden, Doppler, or AWS has no tier 1
46
+ # path at all and silently resolves through the interactive MCP — the exact
47
+ # divergence `credential-substrate-precedence` exists to remove.
48
+ read_jam_pat() {
49
+ [ -n "${JAM_PAT:-}" ] && { echo "$JAM_PAT"; return; }
50
+ local resolver
51
+ for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
52
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
53
+ if [ -f "$resolver" ]; then
54
+ local via_lisa
55
+ via_lisa=$(node "$resolver" get JAM_PAT 2>/dev/null) \
56
+ && [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
57
+ break
58
+ fi
59
+ done
60
+ return 1
61
+ }
62
+
63
+ # Piped, never written to disk and never passed as an argv token: a PAT on a
64
+ # command line is readable from the process table by every user on the host.
65
+ read_jam_pat | jam auth login --token
43
66
  jam skills install
44
67
  ```
45
68
 
46
69
  If neither tier works, fail with:
47
70
 
48
71
  ```text
49
- Error: no Jam access substrate available. Authenticate the Jam MCP or set JAM_PAT.
72
+ Error: no Jam access substrate available. Authenticate the Jam MCP, set JAM_PAT, or store JAM_PAT in this project's secrets provider.
50
73
  ```
51
74
 
75
+ ## Mutation boundary
76
+
77
+ Every operation in the Invocation Contract is **read-only** — fetching a trace,
78
+ a recording, or a bug report. So the `credential-substrate-precedence` guarded
79
+ fallback for mutating operations (write, read back, assert the tenant from the
80
+ response, roll back on mismatch) is not engaged here, and a failed tier is
81
+ simply skipped. A future operation that mutates Jam state — commenting on or
82
+ deleting a Jam — is a write and MUST reconcile by read-back before any retry.
83
+
52
84
  ## Invariants
53
85
 
54
86
  - Tier order is `credential-substrate-precedence`: `JAM_PAT` CLI first, Jam MCP as
55
87
  a preserved first-class fallback. Do not retry a failed tier blindly.
88
+ - The PAT is resolved through `lisa-secrets-access`, with the bare `JAM_PAT`
89
+ environment variable as the documented fallback. Never read a second
90
+ credential store directly.
56
91
  - Never commit a Jam PAT into `.mcp.json` or any generated setup artifact.
57
92
  - Headless Jam access requires `native.jam.dev` for the installer and
58
93
  `api.jam.dev` for CLI/API calls in any custom remote network allowlist.
@@ -70,6 +70,13 @@ read_notion_token() {
70
70
  # Legacy fallback: the OS keychain written by the guided /lisa:setup:notion
71
71
  # flow, for projects that have not adopted a credentials provider. Reached only
72
72
  # when the chokepoint is absent or has no entry.
73
+ #
74
+ # This rung is REMOVED ON 2026-11-01 — a dated migration ramp, not a standing
75
+ # exemption (see credential-substrate-precedence, "Legacy OS-keychain fallback
76
+ # — removal date"). A keychain entry is machine-local ambient state no headless
77
+ # surface can reach, so a project resting on it has no working tier 1 in cron,
78
+ # CI, or a cloud session. Re-run /lisa:setup:notion before that date to store
79
+ # NOTION_API_TOKEN through the chokepoint instead.
73
80
  case "$(uname -s)" in
74
81
  Darwin) security find-generic-password -s lisa-notion -a "$workspace" -w 2>/dev/null ;;
75
82
  Linux) command -v secret-tool >/dev/null && \
@@ -40,15 +40,38 @@ works interactively and headlessly — which is why it leads. The REST tier uses
40
40
 
41
41
  ```bash
42
42
  POSTHOG_HOST=${POSTHOG_HOST:-https://app.posthog.com}
43
+
44
+ # Resolve the key through the chokepoint before giving up on the environment.
45
+ # `$POSTHOG_PERSONAL_API_KEY` is the documented fallback, not the only rung:
46
+ # without this, a project that keeps its credentials in Bitwarden, Doppler, or
47
+ # AWS has no tier 1 path at all and silently resolves through the interactive
48
+ # MCP — the exact divergence `credential-substrate-precedence` exists to remove.
49
+ read_posthog_key() {
50
+ [ -n "${POSTHOG_PERSONAL_API_KEY:-}" ] && { echo "$POSTHOG_PERSONAL_API_KEY"; return; }
51
+ local resolver
52
+ for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
53
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
54
+ if [ -f "$resolver" ]; then
55
+ local via_lisa
56
+ via_lisa=$(node "$resolver" get POSTHOG_PERSONAL_API_KEY 2>/dev/null) \
57
+ && [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
58
+ break
59
+ fi
60
+ done
61
+ return 1
62
+ }
63
+
43
64
  posthog_api() {
44
65
  local path="$1"
45
66
  local method="${2:-GET}"
46
67
  local body="${3:-}"
47
- [ -n "$POSTHOG_PERSONAL_API_KEY" ] || {
48
- echo "Error: POSTHOG_PERSONAL_API_KEY is not set." >&2
68
+ local key
69
+ key=$(read_posthog_key) || {
70
+ echo "Error: no PostHog key. Set POSTHOG_PERSONAL_API_KEY, or store it as" >&2
71
+ echo "POSTHOG_PERSONAL_API_KEY in this project's secrets provider." >&2
49
72
  return 1
50
73
  }
51
- local args=(-sS -X "$method" -H "Authorization: Bearer $POSTHOG_PERSONAL_API_KEY")
74
+ local args=(-sS -X "$method" -H "Authorization: Bearer $key")
52
75
  [ -n "$body" ] && args+=(-H "Content-Type: application/json" --data-binary "$body")
53
76
  curl "${args[@]}" "${POSTHOG_HOST%/}/api${path}"
54
77
  }
@@ -60,11 +83,25 @@ If neither tier works, fail with:
60
83
  Error: no PostHog access substrate available. Authenticate the PostHog MCP or set POSTHOG_PERSONAL_API_KEY.
61
84
  ```
62
85
 
86
+ ## Mutation boundary
87
+
88
+ Every operation in the Invocation Contract is **read-only** — analytics
89
+ retrieval. `query` is an HTTP POST, but it reads: it submits a query body and
90
+ changes no PostHog state. So the `credential-substrate-precedence` guarded
91
+ fallback for mutating operations (write, read back, assert the tenant from the
92
+ response, roll back on mismatch) is not engaged here, and a failed tier is
93
+ simply skipped. Adding a genuinely mutating operation — creating an insight,
94
+ editing a feature flag — pulls that protocol in: a write of unknown outcome MUST
95
+ reconcile by read-back before any retry.
96
+
63
97
  ## Invariants
64
98
 
65
99
  - Tier order is `credential-substrate-precedence`: `POSTHOG_PERSONAL_API_KEY`
66
100
  first, the PostHog MCP as a preserved first-class fallback. Identity-match
67
101
  against the configured project is mandatory on every tier.
102
+ - The key is resolved through `lisa-secrets-access`, with the bare
103
+ `POSTHOG_PERSONAL_API_KEY` environment variable as the documented fallback.
104
+ Never read a second credential store directly.
68
105
  - `POSTHOG_HOST` defaults to PostHog Cloud but can point at a self-hosted
69
106
  deployment.
70
107
  - Consumer skills do not embed PostHog REST paths.
@@ -39,28 +39,69 @@ Sentry documents API auth tokens for REST API calls, and the same token works
39
39
  interactively and headlessly — which is why it leads. The REST tier uses:
40
40
 
41
41
  ```bash
42
+ # Resolve the token through the chokepoint before giving up on the environment.
43
+ # `$SENTRY_AUTH_TOKEN` is the documented fallback, not the only rung: without
44
+ # this, a project that keeps its credentials in Bitwarden, Doppler, or AWS has
45
+ # no tier 1 path at all and silently resolves through the interactive MCP —
46
+ # the exact divergence `credential-substrate-precedence` exists to remove.
47
+ # Mirrors `linear-access`, `atlassian-access`, and `notion-access`.
48
+ read_sentry_token() {
49
+ [ -n "${SENTRY_AUTH_TOKEN:-}" ] && { echo "$SENTRY_AUTH_TOKEN"; return; }
50
+ local resolver
51
+ for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
52
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
53
+ if [ -f "$resolver" ]; then
54
+ local via_lisa
55
+ via_lisa=$(node "$resolver" get SENTRY_AUTH_TOKEN 2>/dev/null) \
56
+ && [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
57
+ break
58
+ fi
59
+ done
60
+ return 1
61
+ }
62
+
42
63
  sentry_api() {
43
64
  local path="$1"
44
- [ -n "$SENTRY_AUTH_TOKEN" ] || {
45
- echo "Error: SENTRY_AUTH_TOKEN is not set." >&2
65
+ local token
66
+ token=$(read_sentry_token) || {
67
+ echo "Error: no Sentry auth token. Set SENTRY_AUTH_TOKEN, or store it as" >&2
68
+ echo "SENTRY_AUTH_TOKEN in this project's secrets provider." >&2
46
69
  return 1
47
70
  }
48
71
  curl -sS "https://sentry.io/api/0${path}" \
49
- -H "Authorization: Bearer $SENTRY_AUTH_TOKEN"
72
+ -H "Authorization: Bearer $token"
50
73
  }
51
74
  ```
52
75
 
76
+ Tier 1a authenticates `sentry-cli` from the **same resolved token**
77
+ (`SENTRY_AUTH_TOKEN=$(read_sentry_token) sentry-cli …`) — never from a second
78
+ credential store, and never from an interactive `sentry-cli login` keychain.
79
+
53
80
  If neither tier works, fail with:
54
81
 
55
82
  ```text
56
83
  Error: no Sentry access substrate available. Authenticate Sentry MCP/CLI or set SENTRY_AUTH_TOKEN.
57
84
  ```
58
85
 
86
+ ## Mutation boundary
87
+
88
+ Every operation in the Invocation Contract is **read-only** — issue, event, and
89
+ release retrieval. So the `credential-substrate-precedence` guarded fallback for
90
+ mutating operations (write, read back, assert the tenant from the response, roll
91
+ back on mismatch) is not engaged here, and a failed tier is simply skipped. Any
92
+ future operation that mutates Sentry state — resolving an issue, editing a
93
+ release — is a write and MUST reconcile by read-back under that protocol before
94
+ any retry; do not add one to the contract without it.
95
+
59
96
  ## Invariants
60
97
 
61
98
  - Tier order is `credential-substrate-precedence`: `SENTRY_AUTH_TOKEN` first, the
62
99
  Sentry MCP as a preserved first-class fallback. Identity-match against the
63
100
  configured org/project is mandatory on every tier.
101
+ - The token is resolved through `lisa-secrets-access`, with the bare
102
+ `SENTRY_AUTH_TOKEN` environment variable as the documented fallback. Never read
103
+ a second credential store (OS keychain, `~/.sentryclirc` token) directly — the
104
+ one-store rule lives at the chokepoint.
64
105
  - Org/project come from `.sentryclirc`, `.lisa.config.json`, or explicit
65
106
  operation args; never infer by searching all accessible orgs.
66
107
  - Consumer skills do not embed Sentry REST paths.