@codyswann/lisa 4.86.0 → 4.86.1

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 (103) hide show
  1. package/all/copy-overwrite/scripts/lib/completion-proof-content.mjs +63 -22
  2. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +6 -1
  3. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +19 -3
  4. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +2 -2
  5. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  6. package/dist/core/lisa-owned-hash-ledger.js +16 -0
  7. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  8. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  9. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.js +11 -7
  11. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  12. package/dist/opencode/plugin-templates/parity-safety-net.sh +19 -3
  13. package/package.json +4 -4
  14. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa/.codex-plugin/skills/lisa-parity-sentry-sdk-setup/SKILL.md +24 -17
  17. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +39 -5
  18. package/plugins/lisa/hooks/parity-safety-net.sh +19 -3
  19. package/plugins/lisa/skills/lisa-parity-sentry-sdk-setup/SKILL.md +24 -17
  20. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +39 -5
  21. package/plugins/lisa-agy/hooks/parity-safety-net.sh +19 -3
  22. package/plugins/lisa-agy/plugin.json +1 -1
  23. package/plugins/lisa-agy/skills/lisa-parity-sentry-sdk-setup/SKILL.md +24 -17
  24. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +39 -5
  25. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  28. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  32. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +19 -3
  34. package/plugins/lisa-copilot/skills/lisa-parity-sentry-sdk-setup/SKILL.md +24 -17
  35. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +39 -5
  36. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +19 -3
  38. package/plugins/lisa-cursor/skills/lisa-parity-sentry-sdk-setup/SKILL.md +24 -17
  39. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +39 -5
  40. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  42. package/plugins/lisa-devin/hooks/parity-safety-net.sh +19 -3
  43. package/plugins/lisa-devin/skills/lisa-parity-sentry-sdk-setup/SKILL.md +24 -17
  44. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +39 -5
  45. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo-agy/plugin.json +1 -1
  48. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  59. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  62. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  76. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-rails-agy/plugin.json +1 -1
  83. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  87. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  89. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  90. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  94. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  96. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  97. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  101. package/plugins/src/base/hooks/parity-safety-net.sh +19 -3
  102. package/plugins/src/base/skills/lisa-parity-sentry-sdk-setup/SKILL.md +24 -17
  103. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +39 -5
@@ -3475,12 +3475,28 @@ if [ -f "$rules_file" ]; then
3475
3475
  # here — before the remaining rules ran, and with an exit status that is
3476
3476
  # not a refusal. The `||` puts the pipeline in a condition context, where
3477
3477
  # a non-zero status is expected rather than fatal.
3478
- # Consume the whole input: grep -q can close an early match while printf
3479
- # is still writing, making pipefail report SIGPIPE instead of the match.
3478
+ # A here-string, never a pipe. With `printf | grep` under pipefail, a grep
3479
+ # that stops at its first match (`-q`, and GNU grep whose stdout is
3480
+ # /dev/null, which it treats as `-q`) closes the pipe while printf is still
3481
+ # writing; printf takes SIGPIPE and the pipeline reports 141 instead of
3482
+ # grep's 0. 141 lands in the `*)` arm below and a rule that MATCHED was
3483
+ # reported as an invalid regex and not enforced (CodySwannGT/lisa#4564).
3484
+ # A here-string has no writer process, so the status is grep's alone.
3485
+ #
3486
+ # The here-string is staged by the shell (a pipe, or a temp file for a
3487
+ # large body or on Bash 3.2), and that staging can fail: no temp space,
3488
+ # a file-size limit. A failed redirection on a bare `grep … <<<` returns
3489
+ # 1, the same status as "no match", so the rule would silently pass. The
3490
+ # brace group separates the two: grep's own status is captured INSIDE it,
3491
+ # so the group itself only fails when the redirection could not be set up.
3480
3492
  rule_status=0
3481
- printf '%s' "$custom_command_str" | grep -Ei -- "$rule" >/dev/null || rule_status=$?
3493
+ { grep -qEi -- "$rule" || rule_status=$?; } <<<"$custom_command_str" \
3494
+ || rule_status=unstaged
3482
3495
  case "$rule_status" in
3483
3496
  0) block "matched a project custom safety rule (${rules_file##*/}): $rule" ;;
3497
+ # Fail closed: the rule could not be evaluated at all, which is not the
3498
+ # same thing as the rule not matching.
3499
+ unstaged) block "could not stage the command to check project custom safety rule (${rules_file##*/}): $rule" ;;
3484
3500
  1) ;; # no match
3485
3501
  # grep exits 2 on a malformed ERE. Treated as no-match before, so a typo
3486
3502
  # in a project's rules file silently disabled that rule — a guard the
package/package.json CHANGED
@@ -189,7 +189,7 @@
189
189
  "qs": "^6.16.0"
190
190
  },
191
191
  "name": "@codyswann/lisa",
192
- "version": "4.86.0",
192
+ "version": "4.86.1",
193
193
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
194
194
  "main": "dist/index.js",
195
195
  "exports": {
@@ -347,7 +347,7 @@
347
347
  "test": "tests"
348
348
  },
349
349
  "types": "./dist/index.d.ts",
350
- "lisaReleaseCommit": "e3e3be6b38115b41fb7f83de27d26f8438b2794e",
351
- "gitHead": "e3e3be6b38115b41fb7f83de27d26f8438b2794e",
352
- "lisaReleaseTag": "v4.86.0"
350
+ "lisaReleaseCommit": "9a3b3a837256383ba6101f74e1fcc457bd86f34d",
351
+ "gitHead": "9a3b3a837256383ba6101f74e1fcc457bd86f34d",
352
+ "lisaReleaseTag": "v4.86.1"
353
353
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.86.0",
3
+ "version": "4.86.1",
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": "4.86.0",
3
+ "version": "4.86.1",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -179,8 +179,11 @@ once at module startup with the DSN from the environment. Await a finite positiv
179
179
  flush budget on both success and original-error paths. A false flush result or
180
180
  rejection must not turn successful queue work into a retry or replace its real
181
181
  error. Keep diagnostics local and non-throwing, and preserve the client for warm
182
- invocations: `close()` disables it. The following helper wraps the host's existing
183
- business operation; it neither mutates context nor adds a Lambda callback.
182
+ invocations: `close()` disables it. A warm container reuses the same process, so
183
+ run each invocation inside `Sentry.withIsolationScope()`: tags, user and
184
+ breadcrumbs that one invocation sets then stay with that invocation instead of
185
+ leaking into the next one's events. The following helper wraps the host's existing
186
+ business operation; it neither mutates the Lambda context nor adds a Lambda callback.
184
187
 
185
188
  <!-- lisa-node24-unwrapped-example:start -->
186
189
  ```ts
@@ -202,24 +205,28 @@ export async function withSentryFlush<T>(
202
205
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
203
206
  throw new RangeError("Choose a finite positive Sentry flush budget");
204
207
  }
205
- try {
206
- return await work();
207
- } catch (originalError) {
208
- try {
209
- Sentry.captureException(originalError);
210
- } catch {
211
- // Preserve the original handler error even if capture fails.
212
- }
213
- throw originalError;
214
- } finally {
208
+ // A fresh isolation scope per invocation: scope data set by one warm
209
+ // invocation must not be attached to the next invocation's events.
210
+ return Sentry.withIsolationScope(async () => {
215
211
  try {
216
- if (!(await Sentry.flush(timeoutMs))) {
217
- reportFlushProblem("Sentry flush did not finish in time");
212
+ return await work();
213
+ } catch (originalError) {
214
+ try {
215
+ Sentry.captureException(originalError);
216
+ } catch {
217
+ // Preserve the original handler error even if capture fails.
218
+ }
219
+ throw originalError;
220
+ } finally {
221
+ try {
222
+ if (!(await Sentry.flush(timeoutMs))) {
223
+ reportFlushProblem("Sentry flush did not finish in time");
224
+ }
225
+ } catch {
226
+ reportFlushProblem("Sentry flush rejected");
218
227
  }
219
- } catch {
220
- reportFlushProblem("Sentry flush rejected");
221
228
  }
222
- }
229
+ });
223
230
  }
224
231
  ```
225
232
  <!-- lisa-node24-unwrapped-example:end -->
@@ -712,6 +712,31 @@ function validateNotify(notify) {
712
712
  ];
713
713
  }
714
714
 
715
+ /**
716
+ * Maps under `sessionManager` whose KEYS are names chosen by the project, not
717
+ * config field names. `repos.entries` is keyed by child repository directory,
718
+ * so a repo called `token-service` or `secrets-manager` is a name, not a
719
+ * credential field; testing it against CREDENTIAL_KEY made `init-config
720
+ * --write` emit a block that `validate` then refused (CodySwannGT/lisa#4564).
721
+ * Their VALUES are still walked, so a token placed inside an entry is caught.
722
+ */
723
+ const NAME_KEYED_MAPS = [["repos", "entries"]];
724
+
725
+ /**
726
+ * Whether a key trail below `sessionManager` is exactly a name-keyed map.
727
+ * Compared segment by segment, never as a dotted string: a literal key
728
+ * `"repos.entries"` would otherwise borrow the exemption.
729
+ * @param {readonly string[]} segments Object keys from the block root.
730
+ * @returns {boolean} True for a map whose keys are names.
731
+ */
732
+ function isNameKeyedMap(segments) {
733
+ return NAME_KEYED_MAPS.some(
734
+ map =>
735
+ map.length === segments.length &&
736
+ map.every((segment, i) => segment === segments[i])
737
+ );
738
+ }
739
+
715
740
  /**
716
741
  * Walk the block for anything that looks like a credential.
717
742
  *
@@ -721,10 +746,12 @@ function validateNotify(notify) {
721
746
  * configured), so a token here is both leaked and unused. The value is never
722
747
  * echoed.
723
748
  * @param {unknown} value Subtree.
724
- * @param {string} path Dotted path so far.
749
+ * @param {string} path Dotted path so far, for messages.
750
+ * @param {readonly string[] | null} [segments] Object keys from the block root;
751
+ * `null` once an array is crossed, since no name-keyed map lives there.
725
752
  * @returns {string[]} Problems found.
726
753
  */
727
- function findCredentials(value, path) {
754
+ function findCredentials(value, path, segments = []) {
728
755
  if (typeof value === "string") {
729
756
  return TOKEN_VALUE.test(value.trim())
730
757
  ? [
@@ -736,18 +763,25 @@ function findCredentials(value, path) {
736
763
  : [];
737
764
  }
738
765
  if (Array.isArray(value)) {
739
- return value.flatMap((item, i) => findCredentials(item, `${path}[${i}]`));
766
+ return value.flatMap((item, i) =>
767
+ findCredentials(item, `${path}[${i}]`, null)
768
+ );
740
769
  }
741
770
  if (!isPlainObject(value)) return [];
771
+ const keysAreNames = segments !== null && isNameKeyedMap(segments);
742
772
  return Object.entries(value).flatMap(([key, child]) =>
743
- CREDENTIAL_KEY.test(key)
773
+ !keysAreNames && CREDENTIAL_KEY.test(key)
744
774
  ? [
745
775
  `${path}.${key} names a credential. Tokens and secrets never go in ` +
746
776
  `config: remove it, and keep the bot token in the project's ` +
747
777
  `secrets provider (session-manager.mjs store-token, run by ` +
748
778
  `/lisa:setup:session-manager).`,
749
779
  ]
750
- : findCredentials(child, `${path}.${key}`)
780
+ : findCredentials(
781
+ child,
782
+ `${path}.${key}`,
783
+ segments === null ? null : [...segments, key]
784
+ )
751
785
  );
752
786
  }
753
787
 
@@ -3475,12 +3475,28 @@ if [ -f "$rules_file" ]; then
3475
3475
  # here — before the remaining rules ran, and with an exit status that is
3476
3476
  # not a refusal. The `||` puts the pipeline in a condition context, where
3477
3477
  # a non-zero status is expected rather than fatal.
3478
- # Consume the whole input: grep -q can close an early match while printf
3479
- # is still writing, making pipefail report SIGPIPE instead of the match.
3478
+ # A here-string, never a pipe. With `printf | grep` under pipefail, a grep
3479
+ # that stops at its first match (`-q`, and GNU grep whose stdout is
3480
+ # /dev/null, which it treats as `-q`) closes the pipe while printf is still
3481
+ # writing; printf takes SIGPIPE and the pipeline reports 141 instead of
3482
+ # grep's 0. 141 lands in the `*)` arm below and a rule that MATCHED was
3483
+ # reported as an invalid regex and not enforced (CodySwannGT/lisa#4564).
3484
+ # A here-string has no writer process, so the status is grep's alone.
3485
+ #
3486
+ # The here-string is staged by the shell (a pipe, or a temp file for a
3487
+ # large body or on Bash 3.2), and that staging can fail: no temp space,
3488
+ # a file-size limit. A failed redirection on a bare `grep … <<<` returns
3489
+ # 1, the same status as "no match", so the rule would silently pass. The
3490
+ # brace group separates the two: grep's own status is captured INSIDE it,
3491
+ # so the group itself only fails when the redirection could not be set up.
3480
3492
  rule_status=0
3481
- printf '%s' "$custom_command_str" | grep -Ei -- "$rule" >/dev/null || rule_status=$?
3493
+ { grep -qEi -- "$rule" || rule_status=$?; } <<<"$custom_command_str" \
3494
+ || rule_status=unstaged
3482
3495
  case "$rule_status" in
3483
3496
  0) block "matched a project custom safety rule (${rules_file##*/}): $rule" ;;
3497
+ # Fail closed: the rule could not be evaluated at all, which is not the
3498
+ # same thing as the rule not matching.
3499
+ unstaged) block "could not stage the command to check project custom safety rule (${rules_file##*/}): $rule" ;;
3484
3500
  1) ;; # no match
3485
3501
  # grep exits 2 on a malformed ERE. Treated as no-match before, so a typo
3486
3502
  # in a project's rules file silently disabled that rule — a guard the
@@ -177,8 +177,11 @@ once at module startup with the DSN from the environment. Await a finite positiv
177
177
  flush budget on both success and original-error paths. A false flush result or
178
178
  rejection must not turn successful queue work into a retry or replace its real
179
179
  error. Keep diagnostics local and non-throwing, and preserve the client for warm
180
- invocations: `close()` disables it. The following helper wraps the host's existing
181
- business operation; it neither mutates context nor adds a Lambda callback.
180
+ invocations: `close()` disables it. A warm container reuses the same process, so
181
+ run each invocation inside `Sentry.withIsolationScope()`: tags, user and
182
+ breadcrumbs that one invocation sets then stay with that invocation instead of
183
+ leaking into the next one's events. The following helper wraps the host's existing
184
+ business operation; it neither mutates the Lambda context nor adds a Lambda callback.
182
185
 
183
186
  <!-- lisa-node24-unwrapped-example:start -->
184
187
  ```ts
@@ -200,24 +203,28 @@ export async function withSentryFlush<T>(
200
203
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
201
204
  throw new RangeError("Choose a finite positive Sentry flush budget");
202
205
  }
203
- try {
204
- return await work();
205
- } catch (originalError) {
206
- try {
207
- Sentry.captureException(originalError);
208
- } catch {
209
- // Preserve the original handler error even if capture fails.
210
- }
211
- throw originalError;
212
- } finally {
206
+ // A fresh isolation scope per invocation: scope data set by one warm
207
+ // invocation must not be attached to the next invocation's events.
208
+ return Sentry.withIsolationScope(async () => {
213
209
  try {
214
- if (!(await Sentry.flush(timeoutMs))) {
215
- reportFlushProblem("Sentry flush did not finish in time");
210
+ return await work();
211
+ } catch (originalError) {
212
+ try {
213
+ Sentry.captureException(originalError);
214
+ } catch {
215
+ // Preserve the original handler error even if capture fails.
216
+ }
217
+ throw originalError;
218
+ } finally {
219
+ try {
220
+ if (!(await Sentry.flush(timeoutMs))) {
221
+ reportFlushProblem("Sentry flush did not finish in time");
222
+ }
223
+ } catch {
224
+ reportFlushProblem("Sentry flush rejected");
216
225
  }
217
- } catch {
218
- reportFlushProblem("Sentry flush rejected");
219
226
  }
220
- }
227
+ });
221
228
  }
222
229
  ```
223
230
  <!-- lisa-node24-unwrapped-example:end -->
@@ -712,6 +712,31 @@ function validateNotify(notify) {
712
712
  ];
713
713
  }
714
714
 
715
+ /**
716
+ * Maps under `sessionManager` whose KEYS are names chosen by the project, not
717
+ * config field names. `repos.entries` is keyed by child repository directory,
718
+ * so a repo called `token-service` or `secrets-manager` is a name, not a
719
+ * credential field; testing it against CREDENTIAL_KEY made `init-config
720
+ * --write` emit a block that `validate` then refused (CodySwannGT/lisa#4564).
721
+ * Their VALUES are still walked, so a token placed inside an entry is caught.
722
+ */
723
+ const NAME_KEYED_MAPS = [["repos", "entries"]];
724
+
725
+ /**
726
+ * Whether a key trail below `sessionManager` is exactly a name-keyed map.
727
+ * Compared segment by segment, never as a dotted string: a literal key
728
+ * `"repos.entries"` would otherwise borrow the exemption.
729
+ * @param {readonly string[]} segments Object keys from the block root.
730
+ * @returns {boolean} True for a map whose keys are names.
731
+ */
732
+ function isNameKeyedMap(segments) {
733
+ return NAME_KEYED_MAPS.some(
734
+ map =>
735
+ map.length === segments.length &&
736
+ map.every((segment, i) => segment === segments[i])
737
+ );
738
+ }
739
+
715
740
  /**
716
741
  * Walk the block for anything that looks like a credential.
717
742
  *
@@ -721,10 +746,12 @@ function validateNotify(notify) {
721
746
  * configured), so a token here is both leaked and unused. The value is never
722
747
  * echoed.
723
748
  * @param {unknown} value Subtree.
724
- * @param {string} path Dotted path so far.
749
+ * @param {string} path Dotted path so far, for messages.
750
+ * @param {readonly string[] | null} [segments] Object keys from the block root;
751
+ * `null` once an array is crossed, since no name-keyed map lives there.
725
752
  * @returns {string[]} Problems found.
726
753
  */
727
- function findCredentials(value, path) {
754
+ function findCredentials(value, path, segments = []) {
728
755
  if (typeof value === "string") {
729
756
  return TOKEN_VALUE.test(value.trim())
730
757
  ? [
@@ -736,18 +763,25 @@ function findCredentials(value, path) {
736
763
  : [];
737
764
  }
738
765
  if (Array.isArray(value)) {
739
- return value.flatMap((item, i) => findCredentials(item, `${path}[${i}]`));
766
+ return value.flatMap((item, i) =>
767
+ findCredentials(item, `${path}[${i}]`, null)
768
+ );
740
769
  }
741
770
  if (!isPlainObject(value)) return [];
771
+ const keysAreNames = segments !== null && isNameKeyedMap(segments);
742
772
  return Object.entries(value).flatMap(([key, child]) =>
743
- CREDENTIAL_KEY.test(key)
773
+ !keysAreNames && CREDENTIAL_KEY.test(key)
744
774
  ? [
745
775
  `${path}.${key} names a credential. Tokens and secrets never go in ` +
746
776
  `config: remove it, and keep the bot token in the project's ` +
747
777
  `secrets provider (session-manager.mjs store-token, run by ` +
748
778
  `/lisa:setup:session-manager).`,
749
779
  ]
750
- : findCredentials(child, `${path}.${key}`)
780
+ : findCredentials(
781
+ child,
782
+ `${path}.${key}`,
783
+ segments === null ? null : [...segments, key]
784
+ )
751
785
  );
752
786
  }
753
787
 
@@ -3475,12 +3475,28 @@ if [ -f "$rules_file" ]; then
3475
3475
  # here — before the remaining rules ran, and with an exit status that is
3476
3476
  # not a refusal. The `||` puts the pipeline in a condition context, where
3477
3477
  # a non-zero status is expected rather than fatal.
3478
- # Consume the whole input: grep -q can close an early match while printf
3479
- # is still writing, making pipefail report SIGPIPE instead of the match.
3478
+ # A here-string, never a pipe. With `printf | grep` under pipefail, a grep
3479
+ # that stops at its first match (`-q`, and GNU grep whose stdout is
3480
+ # /dev/null, which it treats as `-q`) closes the pipe while printf is still
3481
+ # writing; printf takes SIGPIPE and the pipeline reports 141 instead of
3482
+ # grep's 0. 141 lands in the `*)` arm below and a rule that MATCHED was
3483
+ # reported as an invalid regex and not enforced (CodySwannGT/lisa#4564).
3484
+ # A here-string has no writer process, so the status is grep's alone.
3485
+ #
3486
+ # The here-string is staged by the shell (a pipe, or a temp file for a
3487
+ # large body or on Bash 3.2), and that staging can fail: no temp space,
3488
+ # a file-size limit. A failed redirection on a bare `grep … <<<` returns
3489
+ # 1, the same status as "no match", so the rule would silently pass. The
3490
+ # brace group separates the two: grep's own status is captured INSIDE it,
3491
+ # so the group itself only fails when the redirection could not be set up.
3480
3492
  rule_status=0
3481
- printf '%s' "$custom_command_str" | grep -Ei -- "$rule" >/dev/null || rule_status=$?
3493
+ { grep -qEi -- "$rule" || rule_status=$?; } <<<"$custom_command_str" \
3494
+ || rule_status=unstaged
3482
3495
  case "$rule_status" in
3483
3496
  0) block "matched a project custom safety rule (${rules_file##*/}): $rule" ;;
3497
+ # Fail closed: the rule could not be evaluated at all, which is not the
3498
+ # same thing as the rule not matching.
3499
+ unstaged) block "could not stage the command to check project custom safety rule (${rules_file##*/}): $rule" ;;
3484
3500
  1) ;; # no match
3485
3501
  # grep exits 2 on a malformed ERE. Treated as no-match before, so a typo
3486
3502
  # in a project's rules file silently disabled that rule — a guard the
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.86.0",
3
+ "version": "4.86.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -177,8 +177,11 @@ once at module startup with the DSN from the environment. Await a finite positiv
177
177
  flush budget on both success and original-error paths. A false flush result or
178
178
  rejection must not turn successful queue work into a retry or replace its real
179
179
  error. Keep diagnostics local and non-throwing, and preserve the client for warm
180
- invocations: `close()` disables it. The following helper wraps the host's existing
181
- business operation; it neither mutates context nor adds a Lambda callback.
180
+ invocations: `close()` disables it. A warm container reuses the same process, so
181
+ run each invocation inside `Sentry.withIsolationScope()`: tags, user and
182
+ breadcrumbs that one invocation sets then stay with that invocation instead of
183
+ leaking into the next one's events. The following helper wraps the host's existing
184
+ business operation; it neither mutates the Lambda context nor adds a Lambda callback.
182
185
 
183
186
  <!-- lisa-node24-unwrapped-example:start -->
184
187
  ```ts
@@ -200,24 +203,28 @@ export async function withSentryFlush<T>(
200
203
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
201
204
  throw new RangeError("Choose a finite positive Sentry flush budget");
202
205
  }
203
- try {
204
- return await work();
205
- } catch (originalError) {
206
- try {
207
- Sentry.captureException(originalError);
208
- } catch {
209
- // Preserve the original handler error even if capture fails.
210
- }
211
- throw originalError;
212
- } finally {
206
+ // A fresh isolation scope per invocation: scope data set by one warm
207
+ // invocation must not be attached to the next invocation's events.
208
+ return Sentry.withIsolationScope(async () => {
213
209
  try {
214
- if (!(await Sentry.flush(timeoutMs))) {
215
- reportFlushProblem("Sentry flush did not finish in time");
210
+ return await work();
211
+ } catch (originalError) {
212
+ try {
213
+ Sentry.captureException(originalError);
214
+ } catch {
215
+ // Preserve the original handler error even if capture fails.
216
+ }
217
+ throw originalError;
218
+ } finally {
219
+ try {
220
+ if (!(await Sentry.flush(timeoutMs))) {
221
+ reportFlushProblem("Sentry flush did not finish in time");
222
+ }
223
+ } catch {
224
+ reportFlushProblem("Sentry flush rejected");
216
225
  }
217
- } catch {
218
- reportFlushProblem("Sentry flush rejected");
219
226
  }
220
- }
227
+ });
221
228
  }
222
229
  ```
223
230
  <!-- lisa-node24-unwrapped-example:end -->
@@ -712,6 +712,31 @@ function validateNotify(notify) {
712
712
  ];
713
713
  }
714
714
 
715
+ /**
716
+ * Maps under `sessionManager` whose KEYS are names chosen by the project, not
717
+ * config field names. `repos.entries` is keyed by child repository directory,
718
+ * so a repo called `token-service` or `secrets-manager` is a name, not a
719
+ * credential field; testing it against CREDENTIAL_KEY made `init-config
720
+ * --write` emit a block that `validate` then refused (CodySwannGT/lisa#4564).
721
+ * Their VALUES are still walked, so a token placed inside an entry is caught.
722
+ */
723
+ const NAME_KEYED_MAPS = [["repos", "entries"]];
724
+
725
+ /**
726
+ * Whether a key trail below `sessionManager` is exactly a name-keyed map.
727
+ * Compared segment by segment, never as a dotted string: a literal key
728
+ * `"repos.entries"` would otherwise borrow the exemption.
729
+ * @param {readonly string[]} segments Object keys from the block root.
730
+ * @returns {boolean} True for a map whose keys are names.
731
+ */
732
+ function isNameKeyedMap(segments) {
733
+ return NAME_KEYED_MAPS.some(
734
+ map =>
735
+ map.length === segments.length &&
736
+ map.every((segment, i) => segment === segments[i])
737
+ );
738
+ }
739
+
715
740
  /**
716
741
  * Walk the block for anything that looks like a credential.
717
742
  *
@@ -721,10 +746,12 @@ function validateNotify(notify) {
721
746
  * configured), so a token here is both leaked and unused. The value is never
722
747
  * echoed.
723
748
  * @param {unknown} value Subtree.
724
- * @param {string} path Dotted path so far.
749
+ * @param {string} path Dotted path so far, for messages.
750
+ * @param {readonly string[] | null} [segments] Object keys from the block root;
751
+ * `null` once an array is crossed, since no name-keyed map lives there.
725
752
  * @returns {string[]} Problems found.
726
753
  */
727
- function findCredentials(value, path) {
754
+ function findCredentials(value, path, segments = []) {
728
755
  if (typeof value === "string") {
729
756
  return TOKEN_VALUE.test(value.trim())
730
757
  ? [
@@ -736,18 +763,25 @@ function findCredentials(value, path) {
736
763
  : [];
737
764
  }
738
765
  if (Array.isArray(value)) {
739
- return value.flatMap((item, i) => findCredentials(item, `${path}[${i}]`));
766
+ return value.flatMap((item, i) =>
767
+ findCredentials(item, `${path}[${i}]`, null)
768
+ );
740
769
  }
741
770
  if (!isPlainObject(value)) return [];
771
+ const keysAreNames = segments !== null && isNameKeyedMap(segments);
742
772
  return Object.entries(value).flatMap(([key, child]) =>
743
- CREDENTIAL_KEY.test(key)
773
+ !keysAreNames && CREDENTIAL_KEY.test(key)
744
774
  ? [
745
775
  `${path}.${key} names a credential. Tokens and secrets never go in ` +
746
776
  `config: remove it, and keep the bot token in the project's ` +
747
777
  `secrets provider (session-manager.mjs store-token, run by ` +
748
778
  `/lisa:setup:session-manager).`,
749
779
  ]
750
- : findCredentials(child, `${path}.${key}`)
780
+ : findCredentials(
781
+ child,
782
+ `${path}.${key}`,
783
+ segments === null ? null : [...segments, key]
784
+ )
751
785
  );
752
786
  }
753
787
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.86.0",
3
+ "version": "4.86.1",
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.86.0",
3
+ "version": "4.86.1",
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.86.0",
3
+ "version": "4.86.1",
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.86.0",
3
+ "version": "4.86.1",
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.86.0",
3
+ "version": "4.86.1",
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.86.0",
3
+ "version": "4.86.1",
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.86.0",
3
+ "version": "4.86.1",
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.86.0",
3
+ "version": "4.86.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"