@codyswann/lisa 3.54.2 → 3.54.4

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 (81) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +1 -0
  3. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  4. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.js +12 -7
  6. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  7. package/package.json +1 -1
  8. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  10. package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +37 -2
  11. package/plugins/lisa/.codex-plugin/skills/lisa-setup-linear/SKILL.md +53 -7
  12. package/plugins/lisa/hooks/threshold-ratchet.mjs +17 -0
  13. package/plugins/lisa/scripts/plugin-sync-explain.mjs +27 -1
  14. package/plugins/lisa/skills/lisa-linear-access/SKILL.md +37 -2
  15. package/plugins/lisa/skills/lisa-setup-linear/SKILL.md +53 -7
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-agy/scripts/plugin-sync-explain.mjs +27 -1
  18. package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +37 -2
  19. package/plugins/lisa-agy/skills/lisa-setup-linear/SKILL.md +53 -7
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +17 -0
  27. package/plugins/lisa-copilot/scripts/plugin-sync-explain.mjs +27 -1
  28. package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +37 -2
  29. package/plugins/lisa-copilot/skills/lisa-setup-linear/SKILL.md +53 -7
  30. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +17 -0
  32. package/plugins/lisa-cursor/scripts/plugin-sync-explain.mjs +27 -1
  33. package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +37 -2
  34. package/plugins/lisa-cursor/skills/lisa-setup-linear/SKILL.md +53 -7
  35. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo-agy/plugin.json +1 -1
  38. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  58. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails-agy/plugin.json +1 -1
  63. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  68. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  73. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/src/base/hooks/threshold-ratchet.mjs +17 -0
  76. package/plugins/src/base/scripts/plugin-sync-explain.mjs +27 -1
  77. package/plugins/src/base/skills/lisa-linear-access/SKILL.md +37 -2
  78. package/plugins/src/base/skills/lisa-setup-linear/SKILL.md +53 -7
  79. package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +17 -0
  80. package/scripts/check-security-floors.mjs +36 -10
  81. package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +17 -0
package/package.json CHANGED
@@ -141,7 +141,7 @@
141
141
  "zod-validation-error": "^4.0.0"
142
142
  },
143
143
  "name": "@codyswann/lisa",
144
- "version": "3.54.2",
144
+ "version": "3.54.4",
145
145
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
146
146
  "main": "dist/index.js",
147
147
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.54.2",
3
+ "version": "3.54.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.54.2",
3
+ "version": "3.54.4",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -84,9 +84,37 @@ All GraphQL calls use:
84
84
  # nowhere to migrate to. Mirrors `atlassian-access` and `notion-access`.
85
85
  read_linear_key() {
86
86
  [ -n "${LINEAR_API_KEY:-}" ] && { echo "$LINEAR_API_KEY"; return; }
87
+
88
+ # The ladder is ordered repo-first and ends at the plugin's own copy.
89
+ #
90
+ # Repo-relative rungs lead because a project that vendors the resolver has
91
+ # declared which copy it wants used, and that decision must survive this
92
+ # change untouched. The plugin rungs are the floor: `resolve-secret.mjs`
93
+ # ships beside this skill, so a rung pointing at it is reachable from
94
+ # anywhere the plugin itself is installed. Without one, a repo that vendors
95
+ # none of the leading paths has no route to the key at all — which is what
96
+ # every consumer of this skill in a `.opencode`-layout repository actually
97
+ # hit, and why agents started improvising their own key lookups.
98
+ local candidates=(
99
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
100
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
101
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
102
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
103
+ )
104
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
105
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
106
+ fi
107
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
108
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
109
+ fi
110
+ # Last rung deliberately needs no environment variable: an agent that was
111
+ # never handed a plugin root still has the installed package to fall back on.
112
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
113
+
87
114
  local resolver
88
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
89
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
115
+ local tried=()
116
+ for resolver in "${candidates[@]}"; do
117
+ tried+=("$resolver")
90
118
  if [ -f "$resolver" ]; then
91
119
  local via_lisa
92
120
  via_lisa=$(node "$resolver" get LINEAR_API_KEY 2>/dev/null) \
@@ -94,6 +122,13 @@ read_linear_key() {
94
122
  break
95
123
  fi
96
124
  done
125
+
126
+ # Name every path. A bare `return 1` sends the next reader hunting for a
127
+ # resolver they cannot see the absence of; the enumeration turns that into a
128
+ # seconds-long diagnosis. Paths only — never any resolved value.
129
+ echo "Error: could not resolve LINEAR_API_KEY through lisa-secrets-access." >&2
130
+ echo "Tried, in order (relative paths are from $PWD):" >&2
131
+ printf ' %s\n' "${tried[@]}" >&2
97
132
  return 1
98
133
  }
99
134
 
@@ -92,10 +92,43 @@ read_linear_key() { # $1=workspace slug
92
92
  # Preferred path: the single secrets chokepoint. It owns the one-store rule
93
93
  # and the surface ladder, so anything it can answer must not be read out of an
94
94
  # OS keychain here — a second reader is how the same credential ends up living
95
- # in two places and drifting. Keep this identical to `linear-access`.
95
+ # in two places and drifting.
96
+ #
97
+ # The CANDIDATE LADDER below must stay identical to `linear-access`. What may
98
+ # differ is only what happens after it: `linear-access` has nowhere else to
99
+ # go and fails loudly, whereas this skill falls through to the legacy keychain
100
+ # rung. Stating the invariant as "the ladder" rather than "this function" is
101
+ # deliberate — the previous wording said to keep the whole thing identical,
102
+ # which is not achievable, and a rule that cannot be followed is a rule that
103
+ # gets ignored. That is exactly how this copy kept the two-rung ladder while
104
+ # `linear-access` grew to seven, leaving `/lisa:setup:linear` unable to reach
105
+ # a key that `lisa-linear-access` could read from the same repository.
106
+ #
107
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
108
+ # lead because a project that vendors the resolver has declared which copy it
109
+ # wants used. The plugin rungs are the floor: `resolve-secret.mjs` ships
110
+ # beside this skill, so a rung pointing at it is reachable from anywhere the
111
+ # plugin itself is installed.
112
+ local candidates=(
113
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
114
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
115
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
116
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
117
+ )
118
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
119
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
120
+ fi
121
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
122
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
123
+ fi
124
+ # Last rung deliberately needs no environment variable: an agent that was
125
+ # never handed a plugin root still has the installed package to fall back on.
126
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
127
+
96
128
  local resolver
97
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
98
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
129
+ local tried=()
130
+ for resolver in "${candidates[@]}"; do
131
+ tried+=("$resolver")
99
132
  if [ -f "$resolver" ]; then
100
133
  local via_lisa
101
134
  via_lisa=$(node "$resolver" get LINEAR_API_KEY 2>/dev/null) \
@@ -106,15 +139,16 @@ read_linear_key() { # $1=workspace slug
106
139
  # Legacy fallback: the OS keychain written by the guided flow below, for
107
140
  # projects that have not adopted a credentials provider. Reached only when the
108
141
  # chokepoint is absent or has no entry.
142
+ local from_keychain=""
109
143
  case "$(uname -s)" in
110
- Darwin) security find-generic-password -s lisa-linear -a "$ws" -w 2>/dev/null ;;
111
- Linux) command -v secret-tool >/dev/null && secret-tool lookup service lisa-linear account "$ws" 2>/dev/null ;;
144
+ Darwin) from_keychain=$(security find-generic-password -s lisa-linear -a "$ws" -w 2>/dev/null) ;;
145
+ Linux) command -v secret-tool >/dev/null && from_keychain=$(secret-tool lookup service lisa-linear account "$ws" 2>/dev/null) ;;
112
146
  MINGW*|MSYS*|CYGWIN*)
113
147
  # `cmdkey /generic ... /pass:` stores the secret in Windows Credential Manager, but
114
148
  # `cmdkey /list` never prints stored passwords (by design). Read the CredentialBlob
115
149
  # back via the Win32 CredRead API through PowerShell; pass the target name via an env
116
150
  # var to dodge nested quoting, and strip the CRLF powershell.exe appends.
117
- LISA_CRED_TARGET="lisa-linear-${ws}" powershell.exe -NoProfile -NonInteractive -Command '
151
+ from_keychain=$(LISA_CRED_TARGET="lisa-linear-${ws}" powershell.exe -NoProfile -NonInteractive -Command '
118
152
  Add-Type -TypeDefinition @"
119
153
  using System;
120
154
  using System.Runtime.InteropServices;
@@ -140,8 +174,20 @@ public static class LisaCred {
140
174
  }
141
175
  }
142
176
  "@
143
- [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r' ;;
177
+ [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r') ;;
144
178
  esac
179
+ [ -n "$from_keychain" ] && { echo "$from_keychain"; return; }
180
+
181
+ # Name every path, the same way `linear-access` does — the diagnostics are
182
+ # part of the parity, not decoration. A silent empty return sends the next
183
+ # reader hunting for a resolver they cannot see the absence of; the
184
+ # enumeration turns that into a seconds-long diagnosis. Paths and store
185
+ # coordinates only — never any resolved value, on any path.
186
+ echo "Error: could not resolve LINEAR_API_KEY through lisa-secrets-access or the legacy keychain." >&2
187
+ echo "Tried, in order (relative paths are from $PWD):" >&2
188
+ printf ' %s\n' "${tried[@]}" >&2
189
+ echo " <OS keychain> service=lisa-linear account=$ws" >&2
190
+ return 1
145
191
  }
146
192
 
147
193
  KEY=$(read_linear_key "$WORKSPACE")
@@ -53,8 +53,25 @@ import {
53
53
  * Standard git locations, checked in order so the executable comes from a
54
54
  * fixed, unwriteable directory rather than a PATH lookup. The bare "git"
55
55
  * fallback keeps unusual layouts (e.g. Windows git-bash) working.
56
+ *
57
+ * Order within that constraint is set by measurement, not by convention. On
58
+ * macOS `/usr/bin/git` is not git: it is Apple's `xcrun` shim, which locates a
59
+ * developer directory and re-executes the real binary there. Dispatching
60
+ * through it costs a **median 33 ms against 15 ms** for either
61
+ * developer-directory git, and **100 ms against 21 ms at p90** — randomized
62
+ * call order, fixed inter-call gaps, `git rev-parse --show-toplevel`, n=30 each
63
+ * (lisa#2898). The call does no work at all; the difference is the dispatch.
64
+ *
65
+ * The two entries promoted ahead of it are the developer-directory gits the
66
+ * shim itself re-executes. Both are `root:wheel` files in system locations, so
67
+ * this is the same trust class as `/usr/bin/git` and not a relaxation: the
68
+ * user-writable `/usr/local` and Homebrew entries stay behind it, exactly where
69
+ * they already were. Neither promoted path exists on Linux, so every CI runner
70
+ * resolves precisely what it resolved before.
56
71
  */
57
72
  const GIT_LOCATIONS = [
73
+ "/Library/Developer/CommandLineTools/usr/bin/git",
74
+ "/Applications/Xcode.app/Contents/Developer/usr/bin/git",
58
75
  "/usr/bin/git",
59
76
  "/usr/local/bin/git",
60
77
  "/opt/homebrew/bin/git",
@@ -33,7 +33,33 @@ export const PLUGIN_SYNC_CLASSIFICATIONS = [
33
33
  const PLUGINS_DIR = "plugins";
34
34
  const SOURCE_ROOT = "plugins/src";
35
35
  const MARKETPLACE = ".claude-plugin/marketplace.json";
36
- const GIT_BIN = "/usr/bin/git";
36
+ /**
37
+ * Fixed absolute git locations, tried in order rather than a bare command name
38
+ * so a writeable directory early on `PATH` cannot decide which binary runs.
39
+ *
40
+ * Order within that constraint is set by measurement, not by convention. On
41
+ * macOS `/usr/bin/git` is not git: it is Apple's `xcrun` shim, which locates a
42
+ * developer directory and re-executes the real binary there. Dispatching
43
+ * through it costs a **median 33 ms against 15 ms** for either
44
+ * developer-directory git, and **100 ms against 21 ms at p90** — randomized
45
+ * call order, fixed inter-call gaps, `git rev-parse --show-toplevel`, n=30 each
46
+ * (lisa#2898). The call does no work at all; the difference is the dispatch.
47
+ *
48
+ * The two entries promoted ahead of it are the developer-directory gits the
49
+ * shim itself re-executes. Both are `root:wheel` files in system locations, so
50
+ * this is the same trust class as `/usr/bin/git` and not a relaxation: the
51
+ * user-writable `/usr/local` and Homebrew entries stay behind it, exactly where
52
+ * they already were. Neither promoted path exists on Linux, so every CI runner
53
+ * resolves precisely what it resolved before.
54
+ */
55
+ const GIT_LOCATIONS = [
56
+ "/Library/Developer/CommandLineTools/usr/bin/git",
57
+ "/Applications/Xcode.app/Contents/Developer/usr/bin/git",
58
+ "/usr/bin/git",
59
+ "/usr/local/bin/git",
60
+ "/opt/homebrew/bin/git",
61
+ ];
62
+ const GIT_BIN = GIT_LOCATIONS.find(candidate => existsSync(candidate)) ?? "git";
37
63
 
38
64
  /**
39
65
  * @typedef {{
@@ -84,9 +84,37 @@ All GraphQL calls use:
84
84
  # nowhere to migrate to. Mirrors `atlassian-access` and `notion-access`.
85
85
  read_linear_key() {
86
86
  [ -n "${LINEAR_API_KEY:-}" ] && { echo "$LINEAR_API_KEY"; return; }
87
+
88
+ # The ladder is ordered repo-first and ends at the plugin's own copy.
89
+ #
90
+ # Repo-relative rungs lead because a project that vendors the resolver has
91
+ # declared which copy it wants used, and that decision must survive this
92
+ # change untouched. The plugin rungs are the floor: `resolve-secret.mjs`
93
+ # ships beside this skill, so a rung pointing at it is reachable from
94
+ # anywhere the plugin itself is installed. Without one, a repo that vendors
95
+ # none of the leading paths has no route to the key at all — which is what
96
+ # every consumer of this skill in a `.opencode`-layout repository actually
97
+ # hit, and why agents started improvising their own key lookups.
98
+ local candidates=(
99
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
100
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
101
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
102
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
103
+ )
104
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
105
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
106
+ fi
107
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
108
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
109
+ fi
110
+ # Last rung deliberately needs no environment variable: an agent that was
111
+ # never handed a plugin root still has the installed package to fall back on.
112
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
113
+
87
114
  local resolver
88
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
89
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
115
+ local tried=()
116
+ for resolver in "${candidates[@]}"; do
117
+ tried+=("$resolver")
90
118
  if [ -f "$resolver" ]; then
91
119
  local via_lisa
92
120
  via_lisa=$(node "$resolver" get LINEAR_API_KEY 2>/dev/null) \
@@ -94,6 +122,13 @@ read_linear_key() {
94
122
  break
95
123
  fi
96
124
  done
125
+
126
+ # Name every path. A bare `return 1` sends the next reader hunting for a
127
+ # resolver they cannot see the absence of; the enumeration turns that into a
128
+ # seconds-long diagnosis. Paths only — never any resolved value.
129
+ echo "Error: could not resolve LINEAR_API_KEY through lisa-secrets-access." >&2
130
+ echo "Tried, in order (relative paths are from $PWD):" >&2
131
+ printf ' %s\n' "${tried[@]}" >&2
97
132
  return 1
98
133
  }
99
134
 
@@ -92,10 +92,43 @@ read_linear_key() { # $1=workspace slug
92
92
  # Preferred path: the single secrets chokepoint. It owns the one-store rule
93
93
  # and the surface ladder, so anything it can answer must not be read out of an
94
94
  # OS keychain here — a second reader is how the same credential ends up living
95
- # in two places and drifting. Keep this identical to `linear-access`.
95
+ # in two places and drifting.
96
+ #
97
+ # The CANDIDATE LADDER below must stay identical to `linear-access`. What may
98
+ # differ is only what happens after it: `linear-access` has nowhere else to
99
+ # go and fails loudly, whereas this skill falls through to the legacy keychain
100
+ # rung. Stating the invariant as "the ladder" rather than "this function" is
101
+ # deliberate — the previous wording said to keep the whole thing identical,
102
+ # which is not achievable, and a rule that cannot be followed is a rule that
103
+ # gets ignored. That is exactly how this copy kept the two-rung ladder while
104
+ # `linear-access` grew to seven, leaving `/lisa:setup:linear` unable to reach
105
+ # a key that `lisa-linear-access` could read from the same repository.
106
+ #
107
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
108
+ # lead because a project that vendors the resolver has declared which copy it
109
+ # wants used. The plugin rungs are the floor: `resolve-secret.mjs` ships
110
+ # beside this skill, so a rung pointing at it is reachable from anywhere the
111
+ # plugin itself is installed.
112
+ local candidates=(
113
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
114
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
115
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
116
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
117
+ )
118
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
119
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
120
+ fi
121
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
122
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
123
+ fi
124
+ # Last rung deliberately needs no environment variable: an agent that was
125
+ # never handed a plugin root still has the installed package to fall back on.
126
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
127
+
96
128
  local resolver
97
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
98
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
129
+ local tried=()
130
+ for resolver in "${candidates[@]}"; do
131
+ tried+=("$resolver")
99
132
  if [ -f "$resolver" ]; then
100
133
  local via_lisa
101
134
  via_lisa=$(node "$resolver" get LINEAR_API_KEY 2>/dev/null) \
@@ -106,15 +139,16 @@ read_linear_key() { # $1=workspace slug
106
139
  # Legacy fallback: the OS keychain written by the guided flow below, for
107
140
  # projects that have not adopted a credentials provider. Reached only when the
108
141
  # chokepoint is absent or has no entry.
142
+ local from_keychain=""
109
143
  case "$(uname -s)" in
110
- Darwin) security find-generic-password -s lisa-linear -a "$ws" -w 2>/dev/null ;;
111
- Linux) command -v secret-tool >/dev/null && secret-tool lookup service lisa-linear account "$ws" 2>/dev/null ;;
144
+ Darwin) from_keychain=$(security find-generic-password -s lisa-linear -a "$ws" -w 2>/dev/null) ;;
145
+ Linux) command -v secret-tool >/dev/null && from_keychain=$(secret-tool lookup service lisa-linear account "$ws" 2>/dev/null) ;;
112
146
  MINGW*|MSYS*|CYGWIN*)
113
147
  # `cmdkey /generic ... /pass:` stores the secret in Windows Credential Manager, but
114
148
  # `cmdkey /list` never prints stored passwords (by design). Read the CredentialBlob
115
149
  # back via the Win32 CredRead API through PowerShell; pass the target name via an env
116
150
  # var to dodge nested quoting, and strip the CRLF powershell.exe appends.
117
- LISA_CRED_TARGET="lisa-linear-${ws}" powershell.exe -NoProfile -NonInteractive -Command '
151
+ from_keychain=$(LISA_CRED_TARGET="lisa-linear-${ws}" powershell.exe -NoProfile -NonInteractive -Command '
118
152
  Add-Type -TypeDefinition @"
119
153
  using System;
120
154
  using System.Runtime.InteropServices;
@@ -140,8 +174,20 @@ public static class LisaCred {
140
174
  }
141
175
  }
142
176
  "@
143
- [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r' ;;
177
+ [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r') ;;
144
178
  esac
179
+ [ -n "$from_keychain" ] && { echo "$from_keychain"; return; }
180
+
181
+ # Name every path, the same way `linear-access` does — the diagnostics are
182
+ # part of the parity, not decoration. A silent empty return sends the next
183
+ # reader hunting for a resolver they cannot see the absence of; the
184
+ # enumeration turns that into a seconds-long diagnosis. Paths and store
185
+ # coordinates only — never any resolved value, on any path.
186
+ echo "Error: could not resolve LINEAR_API_KEY through lisa-secrets-access or the legacy keychain." >&2
187
+ echo "Tried, in order (relative paths are from $PWD):" >&2
188
+ printf ' %s\n' "${tried[@]}" >&2
189
+ echo " <OS keychain> service=lisa-linear account=$ws" >&2
190
+ return 1
145
191
  }
146
192
 
147
193
  KEY=$(read_linear_key "$WORKSPACE")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.54.2",
3
+ "version": "3.54.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -33,7 +33,33 @@ export const PLUGIN_SYNC_CLASSIFICATIONS = [
33
33
  const PLUGINS_DIR = "plugins";
34
34
  const SOURCE_ROOT = "plugins/src";
35
35
  const MARKETPLACE = ".claude-plugin/marketplace.json";
36
- const GIT_BIN = "/usr/bin/git";
36
+ /**
37
+ * Fixed absolute git locations, tried in order rather than a bare command name
38
+ * so a writeable directory early on `PATH` cannot decide which binary runs.
39
+ *
40
+ * Order within that constraint is set by measurement, not by convention. On
41
+ * macOS `/usr/bin/git` is not git: it is Apple's `xcrun` shim, which locates a
42
+ * developer directory and re-executes the real binary there. Dispatching
43
+ * through it costs a **median 33 ms against 15 ms** for either
44
+ * developer-directory git, and **100 ms against 21 ms at p90** — randomized
45
+ * call order, fixed inter-call gaps, `git rev-parse --show-toplevel`, n=30 each
46
+ * (lisa#2898). The call does no work at all; the difference is the dispatch.
47
+ *
48
+ * The two entries promoted ahead of it are the developer-directory gits the
49
+ * shim itself re-executes. Both are `root:wheel` files in system locations, so
50
+ * this is the same trust class as `/usr/bin/git` and not a relaxation: the
51
+ * user-writable `/usr/local` and Homebrew entries stay behind it, exactly where
52
+ * they already were. Neither promoted path exists on Linux, so every CI runner
53
+ * resolves precisely what it resolved before.
54
+ */
55
+ const GIT_LOCATIONS = [
56
+ "/Library/Developer/CommandLineTools/usr/bin/git",
57
+ "/Applications/Xcode.app/Contents/Developer/usr/bin/git",
58
+ "/usr/bin/git",
59
+ "/usr/local/bin/git",
60
+ "/opt/homebrew/bin/git",
61
+ ];
62
+ const GIT_BIN = GIT_LOCATIONS.find(candidate => existsSync(candidate)) ?? "git";
37
63
 
38
64
  /**
39
65
  * @typedef {{
@@ -84,9 +84,37 @@ All GraphQL calls use:
84
84
  # nowhere to migrate to. Mirrors `atlassian-access` and `notion-access`.
85
85
  read_linear_key() {
86
86
  [ -n "${LINEAR_API_KEY:-}" ] && { echo "$LINEAR_API_KEY"; return; }
87
+
88
+ # The ladder is ordered repo-first and ends at the plugin's own copy.
89
+ #
90
+ # Repo-relative rungs lead because a project that vendors the resolver has
91
+ # declared which copy it wants used, and that decision must survive this
92
+ # change untouched. The plugin rungs are the floor: `resolve-secret.mjs`
93
+ # ships beside this skill, so a rung pointing at it is reachable from
94
+ # anywhere the plugin itself is installed. Without one, a repo that vendors
95
+ # none of the leading paths has no route to the key at all — which is what
96
+ # every consumer of this skill in a `.opencode`-layout repository actually
97
+ # hit, and why agents started improvising their own key lookups.
98
+ local candidates=(
99
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
100
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
101
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
102
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
103
+ )
104
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
105
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
106
+ fi
107
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
108
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
109
+ fi
110
+ # Last rung deliberately needs no environment variable: an agent that was
111
+ # never handed a plugin root still has the installed package to fall back on.
112
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
113
+
87
114
  local resolver
88
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
89
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
115
+ local tried=()
116
+ for resolver in "${candidates[@]}"; do
117
+ tried+=("$resolver")
90
118
  if [ -f "$resolver" ]; then
91
119
  local via_lisa
92
120
  via_lisa=$(node "$resolver" get LINEAR_API_KEY 2>/dev/null) \
@@ -94,6 +122,13 @@ read_linear_key() {
94
122
  break
95
123
  fi
96
124
  done
125
+
126
+ # Name every path. A bare `return 1` sends the next reader hunting for a
127
+ # resolver they cannot see the absence of; the enumeration turns that into a
128
+ # seconds-long diagnosis. Paths only — never any resolved value.
129
+ echo "Error: could not resolve LINEAR_API_KEY through lisa-secrets-access." >&2
130
+ echo "Tried, in order (relative paths are from $PWD):" >&2
131
+ printf ' %s\n' "${tried[@]}" >&2
97
132
  return 1
98
133
  }
99
134
 
@@ -92,10 +92,43 @@ read_linear_key() { # $1=workspace slug
92
92
  # Preferred path: the single secrets chokepoint. It owns the one-store rule
93
93
  # and the surface ladder, so anything it can answer must not be read out of an
94
94
  # OS keychain here — a second reader is how the same credential ends up living
95
- # in two places and drifting. Keep this identical to `linear-access`.
95
+ # in two places and drifting.
96
+ #
97
+ # The CANDIDATE LADDER below must stay identical to `linear-access`. What may
98
+ # differ is only what happens after it: `linear-access` has nowhere else to
99
+ # go and fails loudly, whereas this skill falls through to the legacy keychain
100
+ # rung. Stating the invariant as "the ladder" rather than "this function" is
101
+ # deliberate — the previous wording said to keep the whole thing identical,
102
+ # which is not achievable, and a rule that cannot be followed is a rule that
103
+ # gets ignored. That is exactly how this copy kept the two-rung ladder while
104
+ # `linear-access` grew to seven, leaving `/lisa:setup:linear` unable to reach
105
+ # a key that `lisa-linear-access` could read from the same repository.
106
+ #
107
+ # Ordered repo-first, ending at the plugin's own copy. Repo-relative rungs
108
+ # lead because a project that vendors the resolver has declared which copy it
109
+ # wants used. The plugin rungs are the floor: `resolve-secret.mjs` ships
110
+ # beside this skill, so a rung pointing at it is reachable from anywhere the
111
+ # plugin itself is installed.
112
+ local candidates=(
113
+ .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs
114
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs
115
+ .opencode/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
116
+ .codex/skills/lisa/lisa-secrets-access/scripts/resolve-secret.mjs
117
+ )
118
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
119
+ candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
120
+ fi
121
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
122
+ candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
123
+ fi
124
+ # Last rung deliberately needs no environment variable: an agent that was
125
+ # never handed a plugin root still has the installed package to fall back on.
126
+ candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
127
+
96
128
  local resolver
97
- for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
98
- .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
129
+ local tried=()
130
+ for resolver in "${candidates[@]}"; do
131
+ tried+=("$resolver")
99
132
  if [ -f "$resolver" ]; then
100
133
  local via_lisa
101
134
  via_lisa=$(node "$resolver" get LINEAR_API_KEY 2>/dev/null) \
@@ -106,15 +139,16 @@ read_linear_key() { # $1=workspace slug
106
139
  # Legacy fallback: the OS keychain written by the guided flow below, for
107
140
  # projects that have not adopted a credentials provider. Reached only when the
108
141
  # chokepoint is absent or has no entry.
142
+ local from_keychain=""
109
143
  case "$(uname -s)" in
110
- Darwin) security find-generic-password -s lisa-linear -a "$ws" -w 2>/dev/null ;;
111
- Linux) command -v secret-tool >/dev/null && secret-tool lookup service lisa-linear account "$ws" 2>/dev/null ;;
144
+ Darwin) from_keychain=$(security find-generic-password -s lisa-linear -a "$ws" -w 2>/dev/null) ;;
145
+ Linux) command -v secret-tool >/dev/null && from_keychain=$(secret-tool lookup service lisa-linear account "$ws" 2>/dev/null) ;;
112
146
  MINGW*|MSYS*|CYGWIN*)
113
147
  # `cmdkey /generic ... /pass:` stores the secret in Windows Credential Manager, but
114
148
  # `cmdkey /list` never prints stored passwords (by design). Read the CredentialBlob
115
149
  # back via the Win32 CredRead API through PowerShell; pass the target name via an env
116
150
  # var to dodge nested quoting, and strip the CRLF powershell.exe appends.
117
- LISA_CRED_TARGET="lisa-linear-${ws}" powershell.exe -NoProfile -NonInteractive -Command '
151
+ from_keychain=$(LISA_CRED_TARGET="lisa-linear-${ws}" powershell.exe -NoProfile -NonInteractive -Command '
118
152
  Add-Type -TypeDefinition @"
119
153
  using System;
120
154
  using System.Runtime.InteropServices;
@@ -140,8 +174,20 @@ public static class LisaCred {
140
174
  }
141
175
  }
142
176
  "@
143
- [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r' ;;
177
+ [LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | tr -d '\r') ;;
144
178
  esac
179
+ [ -n "$from_keychain" ] && { echo "$from_keychain"; return; }
180
+
181
+ # Name every path, the same way `linear-access` does — the diagnostics are
182
+ # part of the parity, not decoration. A silent empty return sends the next
183
+ # reader hunting for a resolver they cannot see the absence of; the
184
+ # enumeration turns that into a seconds-long diagnosis. Paths and store
185
+ # coordinates only — never any resolved value, on any path.
186
+ echo "Error: could not resolve LINEAR_API_KEY through lisa-secrets-access or the legacy keychain." >&2
187
+ echo "Tried, in order (relative paths are from $PWD):" >&2
188
+ printf ' %s\n' "${tried[@]}" >&2
189
+ echo " <OS keychain> service=lisa-linear account=$ws" >&2
190
+ return 1
145
191
  }
146
192
 
147
193
  KEY=$(read_linear_key "$WORKSPACE")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.54.2",
3
+ "version": "3.54.4",
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": "3.54.2",
3
+ "version": "3.54.4",
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": "3.54.2",
3
+ "version": "3.54.4",
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": "3.54.2",
3
+ "version": "3.54.4",
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": "3.54.2",
3
+ "version": "3.54.4",
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": "3.54.2",
3
+ "version": "3.54.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"