@codyswann/lisa 4.13.0 → 4.13.2

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 (125) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +7 -1
  2. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.js +3 -0
  4. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +16 -14
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/dist/opencode/plugin-templates/parity-safety-net.sh +7 -1
  9. package/package.json +1 -1
  10. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +48 -6
  13. package/plugins/lisa/.codex-plugin/skills/lisa-jam-access/SKILL.md +38 -2
  14. package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +47 -6
  15. package/plugins/lisa/.codex-plugin/skills/lisa-posthog-access/SKILL.md +38 -2
  16. package/plugins/lisa/.codex-plugin/skills/lisa-sentry-access/SKILL.md +38 -2
  17. package/plugins/lisa/.codex-plugin/skills/lisa-setup-atlassian/SKILL.md +53 -7
  18. package/plugins/lisa/.codex-plugin/skills/lisa-setup-notion/SKILL.md +53 -7
  19. package/plugins/lisa/.codex-plugin/skills/lisa-sonarcloud-access/SKILL.md +49 -7
  20. package/plugins/lisa/hooks/parity-safety-net.sh +7 -1
  21. package/plugins/lisa/rules/eager/falsifiable-checks.md +1 -1
  22. package/plugins/lisa/rules/reference/config-resolution.md +43 -6
  23. package/plugins/lisa/rules/reference/falsifiable-checks.md +1 -1
  24. package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +48 -6
  25. package/plugins/lisa/skills/lisa-jam-access/SKILL.md +38 -2
  26. package/plugins/lisa/skills/lisa-notion-access/SKILL.md +47 -6
  27. package/plugins/lisa/skills/lisa-posthog-access/SKILL.md +38 -2
  28. package/plugins/lisa/skills/lisa-sentry-access/SKILL.md +38 -2
  29. package/plugins/lisa/skills/lisa-setup-atlassian/SKILL.md +53 -7
  30. package/plugins/lisa/skills/lisa-setup-notion/SKILL.md +53 -7
  31. package/plugins/lisa/skills/lisa-sonarcloud-access/SKILL.md +49 -7
  32. package/plugins/lisa-agy/hooks/parity-safety-net.sh +7 -1
  33. package/plugins/lisa-agy/plugin.json +1 -1
  34. package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +48 -6
  35. package/plugins/lisa-agy/skills/lisa-jam-access/SKILL.md +38 -2
  36. package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +47 -6
  37. package/plugins/lisa-agy/skills/lisa-posthog-access/SKILL.md +38 -2
  38. package/plugins/lisa-agy/skills/lisa-sentry-access/SKILL.md +38 -2
  39. package/plugins/lisa-agy/skills/lisa-setup-atlassian/SKILL.md +53 -7
  40. package/plugins/lisa-agy/skills/lisa-setup-notion/SKILL.md +53 -7
  41. package/plugins/lisa-agy/skills/lisa-sonarcloud-access/SKILL.md +49 -7
  42. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  45. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +7 -1
  49. package/plugins/lisa-copilot/rules/eager/falsifiable-checks.md +1 -1
  50. package/plugins/lisa-copilot/rules/reference/config-resolution.md +43 -6
  51. package/plugins/lisa-copilot/rules/reference/falsifiable-checks.md +1 -1
  52. package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +48 -6
  53. package/plugins/lisa-copilot/skills/lisa-jam-access/SKILL.md +38 -2
  54. package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +47 -6
  55. package/plugins/lisa-copilot/skills/lisa-posthog-access/SKILL.md +38 -2
  56. package/plugins/lisa-copilot/skills/lisa-sentry-access/SKILL.md +38 -2
  57. package/plugins/lisa-copilot/skills/lisa-setup-atlassian/SKILL.md +53 -7
  58. package/plugins/lisa-copilot/skills/lisa-setup-notion/SKILL.md +53 -7
  59. package/plugins/lisa-copilot/skills/lisa-sonarcloud-access/SKILL.md +49 -7
  60. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +7 -1
  62. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +43 -6
  63. package/plugins/lisa-cursor/rules/falsifiable-checks-reference.mdc +1 -1
  64. package/plugins/lisa-cursor/rules/falsifiable-checks.mdc +1 -1
  65. package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +48 -6
  66. package/plugins/lisa-cursor/skills/lisa-jam-access/SKILL.md +38 -2
  67. package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +47 -6
  68. package/plugins/lisa-cursor/skills/lisa-posthog-access/SKILL.md +38 -2
  69. package/plugins/lisa-cursor/skills/lisa-sentry-access/SKILL.md +38 -2
  70. package/plugins/lisa-cursor/skills/lisa-setup-atlassian/SKILL.md +53 -7
  71. package/plugins/lisa-cursor/skills/lisa-setup-notion/SKILL.md +53 -7
  72. package/plugins/lisa-cursor/skills/lisa-sonarcloud-access/SKILL.md +49 -7
  73. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-expo-agy/plugin.json +1 -1
  76. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  81. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  86. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  91. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  96. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-rails-agy/plugin.json +1 -1
  101. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  106. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  111. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/src/base/hooks/parity-safety-net.sh +7 -1
  114. package/plugins/src/base/rules/eager/falsifiable-checks.md +1 -1
  115. package/plugins/src/base/rules/reference/config-resolution.md +43 -6
  116. package/plugins/src/base/rules/reference/falsifiable-checks.md +1 -1
  117. package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +48 -6
  118. package/plugins/src/base/skills/lisa-jam-access/SKILL.md +38 -2
  119. package/plugins/src/base/skills/lisa-notion-access/SKILL.md +47 -6
  120. package/plugins/src/base/skills/lisa-posthog-access/SKILL.md +38 -2
  121. package/plugins/src/base/skills/lisa-sentry-access/SKILL.md +38 -2
  122. package/plugins/src/base/skills/lisa-setup-atlassian/SKILL.md +53 -7
  123. package/plugins/src/base/skills/lisa-setup-notion/SKILL.md +53 -7
  124. package/plugins/src/base/skills/lisa-sonarcloud-access/SKILL.md +49 -7
  125. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +19 -20
@@ -62,9 +62,38 @@ not the only one:
62
62
  read_sonar_secret() {
63
63
  local name="$1"
64
64
  [ -n "${!name:-}" ] && { echo "${!name}"; return; }
65
+ #
66
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
67
+ # lead because a project that vendors the resolver has declared which copy it
68
+ # wants used, and that decision must survive this change untouched. The plugin
69
+ # rungs are the floor: `resolve-secret.mjs` ships beside this skill, so a rung
70
+ # pointing at it is reachable from anywhere the plugin itself is installed.
71
+ # Without one, a consumer repository that vendors none of the leading paths
72
+ # never reaches a resolver at all — the ladder exits without having asked
73
+ # anything, which is what pushed agents into improvising their own credential
74
+ # lookups. This LADDER is identical in every skill that resolves a credential
75
+ # and `credential-resolver-ladder` fails if any copy diverges. Only what
76
+ # happens AFTER the ladder may differ between them.
77
+ local candidates=(
78
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
79
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
80
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
81
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
82
+ )
83
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
84
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
85
+ fi
86
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
87
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
88
+ fi
89
+ # Last rung deliberately needs no environment variable: an agent that was
90
+ # never handed a plugin root still has the installed package to fall back on.
91
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
92
+
65
93
  local resolver
66
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
67
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
94
+ local tried=()
95
+ for resolver in "${candidates[@]}"; do
96
+ tried+=("$resolver")
68
97
  if [ -f "$resolver" ]; then
69
98
  local via_lisa
70
99
  via_lisa=$(node "$resolver" get "$name" 2>/dev/null) \
@@ -72,6 +101,13 @@ read_sonar_secret() {
72
101
  break
73
102
  fi
74
103
  done
104
+ # Name every path. A bare `return 1` sends the next reader hunting for a
105
+ # resolver they cannot see the absence of; the enumeration turns that into a
106
+ # seconds-long diagnosis. Paths and store coordinates only — never any
107
+ # resolved value, on any path.
108
+ echo "Error: could not resolve $name through lisa-secrets-access." >&2
109
+ echo "Tried, in order (relative paths are from $PWD):" >&2
110
+ printf ' %s\n' "${tried[@]}" >&2
75
111
  return 1
76
112
  }
77
113
 
@@ -81,11 +117,17 @@ SONARQUBE_CLI_TOKEN=$(read_sonar_secret SONARQUBE_CLI_TOKEN) && export SONARQUBE
81
117
  SONARQUBE_CLI_ORG=$(read_sonar_secret SONARQUBE_CLI_ORG) && export SONARQUBE_CLI_ORG
82
118
  ```
83
119
 
84
- `sonar-secrets.sh` runs the same resolution with a longer search path (it also
85
- probes `.codex/skills/…` and `node_modules/@codyswann/lisa/plugins/lisa/…`,
86
- because a hook fires in checkouts that never installed an agent plugin) and with
87
- a timeout, because it sits in front of every prompt. Use its search order
88
- verbatim when this skill runs inside a hook.
120
+ `sonar-secrets.sh` runs the same resolution with two differences that belong to
121
+ a hook rather than to a skill: it anchors every candidate at `$repo_root` (a
122
+ hook fires from whatever directory the agent happens to be in) and it wraps the
123
+ `resolve-secret.mjs` call in a timeout, because it sits in front of every
124
+ prompt. Use its search order verbatim when this skill runs inside a hook.
125
+
126
+ This paragraph used to claim the hook searched a *longer* path than the ladder
127
+ above. It did — the ladder above stopped after `.agents/`, so in a checkout
128
+ that vendored neither leading path it reached no resolver at all and returned
129
+ without a word. The two now cover the same layouts, and the fix that closed
130
+ that gap is guarded by `credential-resolver-ladder`.
89
131
 
90
132
  Wiring is performed once by `/lisa:setup:sonar` (which drives `sonar integrate
91
133
  <agent>`); this access layer assumes the MCP is already wired. This is distinct
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.13.0",
3
+ "version": "4.13.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.13.0",
3
+ "version": "4.13.2",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.13.0",
3
+ "version": "4.13.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.13.0",
3
+ "version": "4.13.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.13.0",
3
+ "version": "4.13.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.13.0",
3
+ "version": "4.13.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -752,7 +752,13 @@ while IFS= read -r rm_stmt; do
752
752
  # arm A). Unquoted and run-leading invocations get the whole statement back,
753
753
  # so this is a no-op for every real delete.
754
754
  rm_assignment_stmt="$rm_stmt"
755
- rm_walk_text="$(rm_scan_scope "$rm_stmt")"
755
+ rm_walk_status=0
756
+ rm_walk_text="$(rm_scan_scope "$rm_stmt")" || rm_walk_status=$?
757
+ if [ "$rm_walk_status" -ne 0 ]; then
758
+ # The substitution above runs in a subshell. A denial inside it cannot stop
759
+ # this parent hook, so carry the failure out explicitly and deny here.
760
+ scan_failed "$rm_walk_status"
761
+ fi
756
762
  while IFS= read -r rm_scope; do
757
763
  set -f
758
764
  seen_rm=0
@@ -34,7 +34,7 @@ Each has been observed in real runs; each reported success while asserting nothi
34
34
  This form is preferred over the alternatives because it is the only one that both survives `sh` and keeps the full output, which is what you need the moment the status says the gate failed. `set -o pipefail` is correct where it exists but is **not POSIX `sh`** — dash has no such option, and this repository runs hooks under `sh`. `${PIPESTATUS[0]}` is **bash-only** (zsh spells it `$pipestatus[1]`, 1-indexed) and is overwritten by the very next command, so it must be read immediately. Never report a gate as green from a status read through `| tail`, `| head`, or `| tee`.
35
35
  - **Falsify before reporting.** No clean result is reportable until the check has been shown to fail on a deliberate break. **"Mentally reverting" does not count** — reasoning that the assertion *would* fail is precisely the step that lets a non-functional guard ship, because the author already believes it is load-bearing. Run the break.
36
36
  - **Mutation-prove the guard, then read the cardinality.** Neuter the protection **in production code**, run the **whole suite**, count the failures and **read their names**, then revert. **Zero** ⇒ the guard is inert (one pinning a single field shipped with all 50 tests green while the regression walked through). **Many, unrelated** ⇒ over-broad; its next failure will not name the cause. **Exactly one, or several all named for the same regression** ⇒ load-bearing and correctly scoped. A **zero is robust to contention** — load only ever adds failures — so only the non-zero counts need a quiet machine.
37
- - **The automated mutation gate is language-scoped; the manual probe above is not.** Stryker instruments the JS/TS family and nothing else — there is **no shell parser**, so a guard whose logic lives in `.sh` yields no mutants under **any** `mutate` list. Adding one does not measure it: it aborts the whole run (`No parser registered for .sh!` — measured, not inferred) and takes every other guard's score with it. So a mutation gate that stays grey or green on a shell-guard change **measured nothing about that guard**, and reading it as coverage is the fifth failure mode above wearing a config file. Shell guards are covered by **driving tests only**: execute the script against a payload table and assert the blocked/allowed verdict, with a control on **both** sides — a refusal case *and* an allowed case. A shell guard nothing executes has no bite evidence at all, whatever the gate printed.
37
+ - **The automated mutation gate is language-scoped; the manual probe above is not.** Stryker instruments the JavaScript and TypeScript family plus Vue, HTML and Svelte — there is **no shell parser**, so a guard whose logic lives in `.sh` yields no mutants under **any** `mutate` list. Adding one does not measure it: it aborts the whole run (`No parser registered for .sh!` — measured, not inferred) and takes every other guard's score with it. So a mutation gate that stays grey or green on a shell-guard change **measured nothing about that guard**, and reading it as coverage is the fifth failure mode above wearing a config file. Shell guards are covered by **driving tests only**: execute the script against a payload table and assert the blocked/allowed verdict, with a control on **both** sides — a refusal case *and* an allowed case. A shell guard nothing executes has no bite evidence at all, whatever the gate printed.
38
38
  - **Never scope the probe by filename, and never infer absent coverage from a grep.** Suites split at the line cap, so a protection's tests often sit in a sibling file: a filename-scoped probe reported cardinality 0 on a live security fix that a whole-suite run showed failing 4 tests, every one named for it.
39
39
  - **Assert that no copy is wrong, not that some copy is right.** An existential assertion over duplicated content is satisfied by the correct copy and cannot see the broken one — rewriting one suite's assertions to universal negatives took the same mutation from **0 to 18** failures. Enumerate the property (*every* marker uses an em-dash), never the one known-bad spelling.
40
40
  - **Reconstruct a red state; never narrate one you did not run.** Evidence describing a failing or dirty state must come from a run you actually observed — an evidence file once claimed "working tree clean" while listing two untracked scripts in the same file. To observe a state that no longer exists, check that commit out in a **throwaway detached-HEAD worktree** and run it there. Writing down what the output would have said is fabrication, however confident the reasoning.
@@ -871,9 +871,34 @@ read_notion_token() {
871
871
  # Preferred path: the single secrets chokepoint, which owns the one-store rule
872
872
  # and the surface ladder. An agent following this reference must try it before
873
873
  # any keychain — a second reader is how one credential ends up in two places.
874
+ #
875
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
876
+ # lead because a project that vendors the resolver has declared which copy it
877
+ # wants used. The plugin rungs are the floor: `resolve-secret.mjs` ships
878
+ # beside the skill, so a rung pointing at it is reachable from anywhere the
879
+ # plugin itself is installed. Without one, a consumer repository that vendors
880
+ # none of the leading paths never reaches a resolver at all — and an agent
881
+ # copying a two-rung ladder out of THIS reference reproduces that.
882
+ local candidates=(
883
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
884
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
885
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
886
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
887
+ )
888
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
889
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
890
+ fi
891
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
892
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
893
+ fi
894
+ # Last rung deliberately needs no environment variable: an agent that was
895
+ # never handed a plugin root still has the installed package to fall back on.
896
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
897
+
874
898
  local resolver
875
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
876
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
899
+ local tried=()
900
+ for resolver in "${candidates[@]}"; do
901
+ tried+=("$resolver")
877
902
  if [ -f "$resolver" ]; then
878
903
  local via_lisa
879
904
  via_lisa=$(node "$resolver" get NOTION_API_TOKEN 2>/dev/null) \
@@ -884,16 +909,17 @@ read_notion_token() {
884
909
  # Legacy fallback: the OS keychain written by the guided setup flow, for
885
910
  # projects with no credentials provider. Reached only when the chokepoint is
886
911
  # absent or has no entry.
912
+ local from_keychain=""
887
913
  case "$(uname -s)" in
888
- Darwin) security find-generic-password -s lisa-notion -a "$workspace" -w 2>/dev/null ;;
914
+ Darwin) from_keychain=$(security find-generic-password -s lisa-notion -a "$workspace" -w 2>/dev/null) ;;
889
915
  Linux) command -v secret-tool >/dev/null && \
890
- secret-tool lookup service lisa-notion account "$workspace" 2>/dev/null ;;
916
+ from_keychain=$(secret-tool lookup service lisa-notion account "$workspace" 2>/dev/null) ;;
891
917
  MINGW*|MSYS*|CYGWIN*)
892
918
  # `cmdkey /generic ... /pass:` stores the secret in Windows Credential Manager, but
893
919
  # `cmdkey /list` never prints stored passwords (by design). Read the CredentialBlob
894
920
  # back via the Win32 CredRead API through PowerShell; pass the target name via an env
895
921
  # var to dodge nested quoting, and strip the CRLF powershell.exe appends.
896
- LISA_CRED_TARGET="lisa-notion-${workspace}" powershell.exe -NoProfile -NonInteractive -Command '
922
+ from_keychain=$(LISA_CRED_TARGET="lisa-notion-${workspace}" powershell.exe -NoProfile -NonInteractive -Command '
897
923
  Add-Type -TypeDefinition @"
898
924
  using System;
899
925
  using System.Runtime.InteropServices;
@@ -919,8 +945,19 @@ public static class LisaCred {
919
945
  }
920
946
  }
921
947
  "@
922
- [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r' ;;
948
+ [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r') ;;
923
949
  esac
950
+ [ -n "$from_keychain" ] && { echo "$from_keychain"; return; }
951
+
952
+ # Name every path. A bare empty return sends the next reader hunting for a
953
+ # resolver they cannot see the absence of; the enumeration turns that into a
954
+ # seconds-long diagnosis. Paths and store coordinates only — never any
955
+ # resolved value, on any path.
956
+ echo "Error: could not resolve NOTION_API_TOKEN through lisa-secrets-access or the legacy keychain." >&2
957
+ echo "Tried, in order (relative paths are from $PWD):" >&2
958
+ printf ' %s\n' "${tried[@]}" >&2
959
+ echo " <OS keychain> service=lisa-notion account=$workspace" >&2
960
+ return 1
924
961
  }
925
962
  ```
926
963
 
@@ -170,7 +170,7 @@ Practical consequence: an inert-guard finding is safe to report from a busy mach
170
170
 
171
171
  Everything above is a *manual* probe, and it is language-agnostic: neuter a shell guard, run the suite, read the names, and the yardstick works exactly as it does for TypeScript.
172
172
 
173
- The **automated** mutation gate is not language-agnostic, and the difference is easy to miss because both end in a green result. Stryker's instrumenter is per-language — the JS/TS family, plus HTML and Svelte, which delegate to it. There is **no shell parser**. So:
173
+ The **automated** mutation gate is not language-agnostic, and the difference is easy to miss because both end in a green result. Stryker's instrumenter is per-language — the JavaScript and TypeScript family, plus Vue, HTML and Svelte, which delegate to it. There is **no shell parser**. So:
174
174
 
175
175
  - A guard whose logic lives in `.sh` produces **no mutants under any `mutate` list**. It is outside the gate by construction, not by a selection decision, and widening the patterns cannot bring it in.
176
176
  - Adding a `.sh` path to `mutate` does not measure it and does not quietly no-op. Stryker throws out of instrumentation — `Unable to parse …. No parser registered for .sh!` — and the run aborts before a single mutant is tried, so **one such entry destroys the score of every other guard in the list.** Measured, not inferred.
@@ -56,9 +56,38 @@ read_atlassian_token() {
56
56
  # and the surface ladder, so anything it can answer must not be read out of an
57
57
  # OS keychain here — a second reader is how the same credential ends up living
58
58
  # in two places and drifting.
59
+ #
60
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
61
+ # lead because a project that vendors the resolver has declared which copy it
62
+ # wants used, and that decision must survive this change untouched. The plugin
63
+ # rungs are the floor: `resolve-secret.mjs` ships beside this skill, so a rung
64
+ # pointing at it is reachable from anywhere the plugin itself is installed.
65
+ # Without one, a consumer repository that vendors none of the leading paths
66
+ # never reaches a resolver at all — the ladder exits without having asked
67
+ # anything, which is what pushed agents into improvising their own credential
68
+ # lookups. This LADDER is identical in every skill that resolves a credential
69
+ # and `credential-resolver-ladder` fails if any copy diverges. Only what
70
+ # happens AFTER the ladder may differ between them.
71
+ local candidates=(
72
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
73
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
74
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
75
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
76
+ )
77
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
78
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
79
+ fi
80
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
81
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
82
+ fi
83
+ # Last rung deliberately needs no environment variable: an agent that was
84
+ # never handed a plugin root still has the installed package to fall back on.
85
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
86
+
59
87
  local resolver
60
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
61
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
88
+ local tried=()
89
+ for resolver in "${candidates[@]}"; do
90
+ tried+=("$resolver")
62
91
  if [ -f "$resolver" ]; then
63
92
  local via_lisa
64
93
  via_lisa=$(node "$resolver" get ATLASSIAN_API_TOKEN 2>/dev/null) \
@@ -76,15 +105,17 @@ read_atlassian_token() {
76
105
  # surface can reach, so a project resting on it has no working tier 1 in cron,
77
106
  # CI, or a cloud session. Re-run /lisa:setup:atlassian before that date to
78
107
  # store ATLASSIAN_API_TOKEN through the chokepoint instead.
108
+ local from_keychain=""
79
109
  case "$(uname -s)" in
80
- Darwin) security find-generic-password -s lisa-atlassian -a "$email" -w 2>/dev/null ;;
81
- Linux) command -v secret-tool >/dev/null && secret-tool lookup service lisa-atlassian account "$email" 2>/dev/null ;;
110
+ Darwin) from_keychain=$(security find-generic-password -s lisa-atlassian -a "$email" -w 2>/dev/null) ;;
111
+ Linux) command -v secret-tool >/dev/null && \
112
+ from_keychain=$(secret-tool lookup service lisa-atlassian account "$email" 2>/dev/null) ;;
82
113
  MINGW*|MSYS*|CYGWIN*)
83
114
  # `cmdkey /generic ... /pass:` stores the secret in Windows Credential Manager, but
84
115
  # `cmdkey /list` never prints stored passwords (by design). Read the CredentialBlob
85
116
  # back via the Win32 CredRead API through PowerShell; pass the target name via an env
86
117
  # var to dodge nested quoting, and strip the CRLF powershell.exe appends.
87
- LISA_CRED_TARGET="lisa-atlassian-${email}" powershell.exe -NoProfile -NonInteractive -Command '
118
+ from_keychain=$(LISA_CRED_TARGET="lisa-atlassian-${email}" powershell.exe -NoProfile -NonInteractive -Command '
88
119
  Add-Type -TypeDefinition @"
89
120
  using System;
90
121
  using System.Runtime.InteropServices;
@@ -110,8 +141,19 @@ public static class LisaCred {
110
141
  }
111
142
  }
112
143
  "@
113
- [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r' ;;
144
+ [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r') ;;
114
145
  esac
146
+ [ -n "$from_keychain" ] && { echo "$from_keychain"; return; }
147
+
148
+ # Name every path. A bare `return 1` sends the next reader hunting for a
149
+ # resolver they cannot see the absence of; the enumeration turns that into a
150
+ # seconds-long diagnosis. Paths and store coordinates only — never any
151
+ # resolved value, on any path.
152
+ echo "Error: could not resolve ATLASSIAN_API_TOKEN through lisa-secrets-access or the legacy keychain." >&2
153
+ echo "Tried, in order (relative paths are from $PWD):" >&2
154
+ printf ' %s\n' "${tried[@]}" >&2
155
+ echo " <OS keychain> service=lisa-atlassian account=$email" >&2
156
+ return 1
115
157
  }
116
158
  TOKEN=$(read_atlassian_token "$EMAIL")
117
159
  if [ -n "$TOKEN" ]; then
@@ -47,9 +47,38 @@ export PATH="$HOME/.local/bin:$PATH"
47
47
  # divergence `credential-substrate-precedence` exists to remove.
48
48
  read_jam_pat() {
49
49
  [ -n "${JAM_PAT:-}" ] && { echo "$JAM_PAT"; return; }
50
+ #
51
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
52
+ # lead because a project that vendors the resolver has declared which copy it
53
+ # wants used, and that decision must survive this change untouched. The plugin
54
+ # rungs are the floor: `resolve-secret.mjs` ships beside this skill, so a rung
55
+ # pointing at it is reachable from anywhere the plugin itself is installed.
56
+ # Without one, a consumer repository that vendors none of the leading paths
57
+ # never reaches a resolver at all — the ladder exits without having asked
58
+ # anything, which is what pushed agents into improvising their own credential
59
+ # lookups. This LADDER is identical in every skill that resolves a credential
60
+ # and `credential-resolver-ladder` fails if any copy diverges. Only what
61
+ # happens AFTER the ladder may differ between them.
62
+ local candidates=(
63
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
64
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
65
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
66
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
67
+ )
68
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
69
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
70
+ fi
71
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
72
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
73
+ fi
74
+ # Last rung deliberately needs no environment variable: an agent that was
75
+ # never handed a plugin root still has the installed package to fall back on.
76
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
77
+
50
78
  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
79
+ local tried=()
80
+ for resolver in "${candidates[@]}"; do
81
+ tried+=("$resolver")
53
82
  if [ -f "$resolver" ]; then
54
83
  local via_lisa
55
84
  via_lisa=$(node "$resolver" get JAM_PAT 2>/dev/null) \
@@ -57,6 +86,13 @@ read_jam_pat() {
57
86
  break
58
87
  fi
59
88
  done
89
+ # Name every path. A bare `return 1` sends the next reader hunting for a
90
+ # resolver they cannot see the absence of; the enumeration turns that into a
91
+ # seconds-long diagnosis. Paths and store coordinates only — never any
92
+ # resolved value, on any path.
93
+ echo "Error: could not resolve JAM_PAT through lisa-secrets-access." >&2
94
+ echo "Tried, in order (relative paths are from $PWD):" >&2
95
+ printf ' %s\n' "${tried[@]}" >&2
60
96
  return 1
61
97
  }
62
98
 
@@ -57,9 +57,38 @@ read_notion_token() {
57
57
  # and the surface ladder, so anything it can answer must not be read out of an
58
58
  # OS keychain here — a second reader is how the same credential ends up living
59
59
  # in two places and drifting.
60
+ #
61
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
62
+ # lead because a project that vendors the resolver has declared which copy it
63
+ # wants used, and that decision must survive this change untouched. The plugin
64
+ # rungs are the floor: `resolve-secret.mjs` ships beside this skill, so a rung
65
+ # pointing at it is reachable from anywhere the plugin itself is installed.
66
+ # Without one, a consumer repository that vendors none of the leading paths
67
+ # never reaches a resolver at all — the ladder exits without having asked
68
+ # anything, which is what pushed agents into improvising their own credential
69
+ # lookups. This LADDER is identical in every skill that resolves a credential
70
+ # and `credential-resolver-ladder` fails if any copy diverges. Only what
71
+ # happens AFTER the ladder may differ between them.
72
+ local candidates=(
73
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
74
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
75
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
76
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
77
+ )
78
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
79
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
80
+ fi
81
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
82
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
83
+ fi
84
+ # Last rung deliberately needs no environment variable: an agent that was
85
+ # never handed a plugin root still has the installed package to fall back on.
86
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
87
+
60
88
  local resolver
61
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
62
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
89
+ local tried=()
90
+ for resolver in "${candidates[@]}"; do
91
+ tried+=("$resolver")
63
92
  if [ -f "$resolver" ]; then
64
93
  local via_lisa
65
94
  via_lisa=$(node "$resolver" get NOTION_API_TOKEN 2>/dev/null) \
@@ -77,16 +106,17 @@ read_notion_token() {
77
106
  # surface can reach, so a project resting on it has no working tier 1 in cron,
78
107
  # CI, or a cloud session. Re-run /lisa:setup:notion before that date to store
79
108
  # NOTION_API_TOKEN through the chokepoint instead.
109
+ local from_keychain=""
80
110
  case "$(uname -s)" in
81
- Darwin) security find-generic-password -s lisa-notion -a "$workspace" -w 2>/dev/null ;;
111
+ Darwin) from_keychain=$(security find-generic-password -s lisa-notion -a "$workspace" -w 2>/dev/null) ;;
82
112
  Linux) command -v secret-tool >/dev/null && \
83
- secret-tool lookup service lisa-notion account "$workspace" 2>/dev/null ;;
113
+ from_keychain=$(secret-tool lookup service lisa-notion account "$workspace" 2>/dev/null) ;;
84
114
  MINGW*|MSYS*|CYGWIN*)
85
115
  # `cmdkey /generic ... /pass:` stores the secret in Windows Credential Manager, but
86
116
  # `cmdkey /list` never prints stored passwords (by design). Read the CredentialBlob
87
117
  # back via the Win32 CredRead API through PowerShell; pass the target name via an env
88
118
  # var to dodge nested quoting, and strip the CRLF powershell.exe appends.
89
- LISA_CRED_TARGET="lisa-notion-${workspace}" powershell.exe -NoProfile -NonInteractive -Command '
119
+ from_keychain=$(LISA_CRED_TARGET="lisa-notion-${workspace}" powershell.exe -NoProfile -NonInteractive -Command '
90
120
  Add-Type -TypeDefinition @"
91
121
  using System;
92
122
  using System.Runtime.InteropServices;
@@ -112,8 +142,19 @@ public static class LisaCred {
112
142
  }
113
143
  }
114
144
  "@
115
- [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r' ;;
145
+ [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r') ;;
116
146
  esac
147
+ [ -n "$from_keychain" ] && { echo "$from_keychain"; return; }
148
+
149
+ # Name every path. A bare `return 1` sends the next reader hunting for a
150
+ # resolver they cannot see the absence of; the enumeration turns that into a
151
+ # seconds-long diagnosis. Paths and store coordinates only — never any
152
+ # resolved value, on any path.
153
+ echo "Error: could not resolve NOTION_API_TOKEN through lisa-secrets-access or the legacy keychain." >&2
154
+ echo "Tried, in order (relative paths are from $PWD):" >&2
155
+ printf ' %s\n' "${tried[@]}" >&2
156
+ echo " <OS keychain> service=lisa-notion account=$workspace" >&2
157
+ return 1
117
158
  }
118
159
  TOKEN=$(read_notion_token "$WORKSPACE")
119
160
  if [ -n "$TOKEN" ]; then
@@ -48,9 +48,38 @@ POSTHOG_HOST=${POSTHOG_HOST:-https://app.posthog.com}
48
48
  # MCP — the exact divergence `credential-substrate-precedence` exists to remove.
49
49
  read_posthog_key() {
50
50
  [ -n "${POSTHOG_PERSONAL_API_KEY:-}" ] && { echo "$POSTHOG_PERSONAL_API_KEY"; return; }
51
+ #
52
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
53
+ # lead because a project that vendors the resolver has declared which copy it
54
+ # wants used, and that decision must survive this change untouched. The plugin
55
+ # rungs are the floor: `resolve-secret.mjs` ships beside this skill, so a rung
56
+ # pointing at it is reachable from anywhere the plugin itself is installed.
57
+ # Without one, a consumer repository that vendors none of the leading paths
58
+ # never reaches a resolver at all — the ladder exits without having asked
59
+ # anything, which is what pushed agents into improvising their own credential
60
+ # lookups. This LADDER is identical in every skill that resolves a credential
61
+ # and `credential-resolver-ladder` fails if any copy diverges. Only what
62
+ # happens AFTER the ladder may differ between them.
63
+ local candidates=(
64
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
65
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
66
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
67
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
68
+ )
69
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
70
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
71
+ fi
72
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
73
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
74
+ fi
75
+ # Last rung deliberately needs no environment variable: an agent that was
76
+ # never handed a plugin root still has the installed package to fall back on.
77
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
78
+
51
79
  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
80
+ local tried=()
81
+ for resolver in "${candidates[@]}"; do
82
+ tried+=("$resolver")
54
83
  if [ -f "$resolver" ]; then
55
84
  local via_lisa
56
85
  via_lisa=$(node "$resolver" get POSTHOG_PERSONAL_API_KEY 2>/dev/null) \
@@ -58,6 +87,13 @@ read_posthog_key() {
58
87
  break
59
88
  fi
60
89
  done
90
+ # Name every path. A bare `return 1` sends the next reader hunting for a
91
+ # resolver they cannot see the absence of; the enumeration turns that into a
92
+ # seconds-long diagnosis. Paths and store coordinates only — never any
93
+ # resolved value, on any path.
94
+ echo "Error: could not resolve POSTHOG_PERSONAL_API_KEY through lisa-secrets-access." >&2
95
+ echo "Tried, in order (relative paths are from $PWD):" >&2
96
+ printf ' %s\n' "${tried[@]}" >&2
61
97
  return 1
62
98
  }
63
99
 
@@ -47,9 +47,38 @@ interactively and headlessly — which is why it leads. The REST tier uses:
47
47
  # Mirrors `linear-access`, `atlassian-access`, and `notion-access`.
48
48
  read_sentry_token() {
49
49
  [ -n "${SENTRY_AUTH_TOKEN:-}" ] && { echo "$SENTRY_AUTH_TOKEN"; return; }
50
+ #
51
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
52
+ # lead because a project that vendors the resolver has declared which copy it
53
+ # wants used, and that decision must survive this change untouched. The plugin
54
+ # rungs are the floor: `resolve-secret.mjs` ships beside this skill, so a rung
55
+ # pointing at it is reachable from anywhere the plugin itself is installed.
56
+ # Without one, a consumer repository that vendors none of the leading paths
57
+ # never reaches a resolver at all — the ladder exits without having asked
58
+ # anything, which is what pushed agents into improvising their own credential
59
+ # lookups. This LADDER is identical in every skill that resolves a credential
60
+ # and `credential-resolver-ladder` fails if any copy diverges. Only what
61
+ # happens AFTER the ladder may differ between them.
62
+ local candidates=(
63
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
64
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
65
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
66
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
67
+ )
68
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
69
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
70
+ fi
71
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
72
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
73
+ fi
74
+ # Last rung deliberately needs no environment variable: an agent that was
75
+ # never handed a plugin root still has the installed package to fall back on.
76
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
77
+
50
78
  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
79
+ local tried=()
80
+ for resolver in "${candidates[@]}"; do
81
+ tried+=("$resolver")
53
82
  if [ -f "$resolver" ]; then
54
83
  local via_lisa
55
84
  via_lisa=$(node "$resolver" get SENTRY_AUTH_TOKEN 2>/dev/null) \
@@ -57,6 +86,13 @@ read_sentry_token() {
57
86
  break
58
87
  fi
59
88
  done
89
+ # Name every path. A bare `return 1` sends the next reader hunting for a
90
+ # resolver they cannot see the absence of; the enumeration turns that into a
91
+ # seconds-long diagnosis. Paths and store coordinates only — never any
92
+ # resolved value, on any path.
93
+ echo "Error: could not resolve SENTRY_AUTH_TOKEN through lisa-secrets-access." >&2
94
+ echo "Tried, in order (relative paths are from $PWD):" >&2
95
+ printf ' %s\n' "${tried[@]}" >&2
60
96
  return 1
61
97
  }
62
98