@codyswann/lisa 4.4.0 → 4.4.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 (75) hide show
  1. package/dist/codex/mcp-installer.d.ts.map +1 -1
  2. package/dist/codex/mcp-installer.js +55 -10
  3. package/dist/codex/mcp-installer.js.map +1 -1
  4. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.js +15 -13
  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-secrets-access/scripts/materialize-secrets.mjs +130 -26
  11. package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
  12. package/plugins/lisa-agy/plugin.json +1 -1
  13. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
  14. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  17. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
  21. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
  23. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo-agy/plugin.json +1 -1
  26. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  46. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails-agy/plugin.json +1 -1
  51. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  56. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  61. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
  64. package/scripts/check-delivery-deletion-conflicts.mjs +2 -2
  65. package/scripts/check-derived-artifacts.mjs +24 -5
  66. package/scripts/check-orphan-test-processes.mjs +2 -2
  67. package/scripts/check-security-floors.mjs +20 -9
  68. package/scripts/check-template-workflow-refs.mjs +23 -8
  69. package/scripts/check-workflow-package-paths.mjs +12 -8
  70. package/scripts/generate-lisa-owned-hash-ledger.mjs +12 -4
  71. package/scripts/generate-upstream-evidence-manifest.mjs +4 -4
  72. package/scripts/verify-health-agentic-built.mjs +4 -3
  73. package/scripts/verify-health-consumer-built.mjs +3 -2
  74. package/scripts/verify-health-contract-built.mjs +3 -2
  75. package/scripts/verify-health-deterministic-built.mjs +5 -4
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.4.0",
3
+ "version": "4.4.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.4.0",
3
+ "version": "4.4.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.4.0",
3
+ "version": "4.4.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.4.0",
3
+ "version": "4.4.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.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -68,11 +68,114 @@ function writeAtomic(destination, contents) {
68
68
  * @param {object} [cfg] Resolved configuration.
69
69
  * @returns {{count: number, dir: string}} What was written, and where.
70
70
  */
71
+ /**
72
+ * Marker version this build writes. Bumping it is SAFE, which is the point.
73
+ *
74
+ * The recognisers below match the FAMILY — the frozen identifier plus any
75
+ * version — rather than one literal string, so a block written by an older
76
+ * build is still found and replaced in place. Before that, the reader looked
77
+ * for the exact text it was about to write, so the first rename made it
78
+ * conclude there was no block here and append a second one.
79
+ *
80
+ * That failure is additive and silent, and this module is the worst place in
81
+ * Lisa for it: it writes into `~/.aws` files and a shell profile, outside any
82
+ * repository, where no apply, diff, or review ever revisits the result. An
83
+ * orphaned block in a shell profile is STILL SOURCED, and profiles apply
84
+ * assignments in order — so whichever block comes last wins, and an operator
85
+ * can end up running under credentials from a block Lisa believes it no longer
86
+ * manages. That is credential selection going wrong with no review step.
87
+ *
88
+ * Everywhere else in the codebase the mitigation is "widen the recogniser and
89
+ * accept the old shape until an apply normalises the file". There is no such
90
+ * operation here: the old population never drains and cannot be counted, so
91
+ * the recogniser must accept every past version permanently.
92
+ *
93
+ * Following `core/apply-receipt`, which treats a `schema_version` it does not
94
+ * recognise as NO RECEIPT rather than one it half-understands: on an
95
+ * unrecognised marker, fail toward redoing the work, not toward assuming it is
96
+ * done. Here that means replacing every family member found, so a file already
97
+ * carrying orphans from a past rename is repaired on the next run rather than
98
+ * accumulating one more.
99
+ */
100
+ const MARKER_VERSION = "v2";
101
+
102
+ /**
103
+ * The frozen half of each marker. Changing one of these DOES orphan blocks —
104
+ * that is the whole contract, and it is why the version lives beside it.
105
+ *
106
+ * This does not make renaming impossible, it makes it deliberate: an innocuous
107
+ * text edit no longer orphans a block, and the one edit that still would is the
108
+ * one these comments forbid.
109
+ */
110
+ const PROFILE_FAMILY = "lisa secrets (managed";
111
+
112
+ /** The frozen family identifier for an `~/.aws` managed region. */
113
+ const MANAGED_FAMILY = "managed by lisa-secrets-access";
114
+
115
+ /**
116
+ * Build the start/end recognisers for one marker family.
117
+ *
118
+ * Matches the family followed by anything up to the closing delimiter, so
119
+ * `(managed)` — every block in the field today — and `(managed v2)` are both
120
+ * found by the same reader.
121
+ * @param {string} family Frozen family identifier.
122
+ * @returns {{start: RegExp, end: RegExp}} Global recognisers for the family.
123
+ */
124
+ function familyRecognisers(family) {
125
+ const quoted = family.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
126
+ return {
127
+ start: new RegExp(`# >>> ${quoted}[^\\n]*>>>`, "g"),
128
+ end: new RegExp(`# <<< ${quoted}[^\\n]*<<<`, "g"),
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Index of the first family match at or after `from`, and its length.
134
+ * @param {string} text Haystack.
135
+ * @param {RegExp} recogniser Global family recogniser.
136
+ * @param {number} from Index to search from.
137
+ * @returns {{index: number, length: number}} `index` is -1 when absent.
138
+ */
139
+ function findFamily(text, recogniser, from = 0) {
140
+ recogniser.lastIndex = from;
141
+ const match = recogniser.exec(text);
142
+ return match === null
143
+ ? { index: -1, length: 0 }
144
+ : { index: match.index, length: match[0].length };
145
+ }
146
+
147
+ /**
148
+ * Remove every managed block of one family, leaving the rest of the file.
149
+ *
150
+ * Every block, not just the first: a file that already carries orphans from a
151
+ * rename that happened before this fix must come out with exactly one block,
152
+ * or the fix would leave the damage it exists to prevent.
153
+ * @param {string} text File contents.
154
+ * @param {string} family Frozen family identifier.
155
+ * @returns {string} The file with every family block removed.
156
+ */
157
+ function stripFamilyBlocks(text, family) {
158
+ const { start, end } = familyRecognisers(family);
159
+ let out = text;
160
+ for (;;) {
161
+ const opened = findFamily(out, start);
162
+ if (opened.index === -1) return out;
163
+ const closed = findFamily(out, end, opened.index + opened.length);
164
+ // A truncated block (opened, never closed) would otherwise swallow the rest
165
+ // of the file on every subsequent write.
166
+ const after =
167
+ closed.index === -1
168
+ ? ""
169
+ : out.slice(closed.index + closed.length).replace(/^\n/, "");
170
+ out = `${out.slice(0, opened.index)}${after}`;
171
+ }
172
+ }
173
+
71
174
  /** Marks the block this owns, so it is replaced rather than appended twice. */
72
- const PROFILE_MARKER = "# >>> lisa secrets (managed) >>>";
175
+ const PROFILE_MARKER = `# >>> ${PROFILE_FAMILY} ${MARKER_VERSION}) >>>`;
73
176
 
74
177
  /** Closes the managed block. */
75
- const PROFILE_END = "# <<< lisa secrets (managed) <<<";
178
+ const PROFILE_END = `# <<< ${PROFILE_FAMILY} ${MARKER_VERSION}) <<<`;
76
179
 
77
180
  /**
78
181
  * Identifies an `~/.aws` file as one this wrote, and may therefore replace.
@@ -81,10 +184,10 @@ const PROFILE_END = "# <<< lisa secrets (managed) <<<";
81
184
  * consumer while still being the thing that distinguishes "our file, refresh
82
185
  * it" from "someone else's file, leave it alone".
83
186
  */
84
- const MANAGED_MARKER = "# >>> managed by lisa-secrets-access >>>";
187
+ const MANAGED_MARKER = `# >>> ${MANAGED_FAMILY} ${MARKER_VERSION} >>>`;
85
188
 
86
189
  /** Closes the managed region of an `~/.aws` file. */
87
- const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
190
+ const MANAGED_END = `# <<< ${MANAGED_FAMILY} ${MARKER_VERSION} <<<`;
88
191
 
89
192
  /**
90
193
  * Replace this module's delimited region in a file, preserving everything else.
@@ -99,22 +202,21 @@ const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
99
202
  */
100
203
  export function upsertManagedBlock(current, body) {
101
204
  const block = `${MANAGED_MARKER}\n${body.trimEnd()}\n${MANAGED_END}`;
102
- const start = current.indexOf(MANAGED_MARKER);
205
+ const opened = findFamily(current, familyRecognisers(MANAGED_FAMILY).start);
103
206
 
104
- if (start === -1) {
207
+ if (opened.index === -1) {
105
208
  const prefix =
106
209
  current && !current.endsWith("\n") ? `${current}\n` : current;
107
210
  return `${prefix}${prefix ? "\n" : ""}${block}\n`;
108
211
  }
109
212
 
110
- const endAt = current.indexOf(MANAGED_END, start);
111
- // A truncated block (marker opened, never closed) would otherwise swallow the
112
- // rest of the file on every subsequent write.
113
- const after =
114
- endAt === -1
115
- ? ""
116
- : current.slice(endAt + MANAGED_END.length).replace(/^\n/, "");
117
- return `${current.slice(0, start)}${block}\n${after}`;
213
+ // Everything this module owns is stripped first, so a file already carrying
214
+ // orphans from a rename that predates the family recogniser comes out with
215
+ // exactly one block rather than one more.
216
+ const withoutOurs = stripFamilyBlocks(current, MANAGED_FAMILY);
217
+ const head = withoutOurs.slice(0, opened.index);
218
+ const tail = withoutOurs.slice(opened.index);
219
+ return `${head}${block}\n${tail}`;
118
220
  }
119
221
 
120
222
  /**
@@ -173,13 +275,18 @@ export function installProfileSourcing(valuesFile, options = {}) {
173
275
 
174
276
  // Replace an existing managed block rather than appending another: this
175
277
  // runs on every session, and an appended-forever profile is its own bug.
176
- const start = current.indexOf(PROFILE_MARKER);
278
+ const opened = findFamily(current, familyRecognisers(PROFILE_FAMILY).start);
279
+ // Stripping EVERY family block before writing one is what makes an already
280
+ // orphaned profile self-heal. It matters more here than anywhere else in
281
+ // Lisa: an orphaned block in a shell profile is still sourced, and the last
282
+ // assignment wins, so a stale block silently selects the wrong credentials.
283
+ const withoutOurs = stripFamilyBlocks(current, PROFILE_FAMILY);
177
284
  const next =
178
- start === -1
285
+ opened.index === -1
179
286
  ? `${current}${current.endsWith("\n") || !current ? "" : "\n"}\n${block}\n`
180
- : `${current.slice(0, start)}${block}${current.slice(
181
- current.indexOf(PROFILE_END, start) + PROFILE_END.length
182
- )}`;
287
+ : `${withoutOurs.slice(0, opened.index)}${block}\n${withoutOurs
288
+ .slice(opened.index)
289
+ .replace(/^\n/, "")}`;
183
290
 
184
291
  if (next !== current) {
185
292
  write(file, next, { mode: 0o600 });
@@ -214,13 +321,10 @@ export function collidingProfiles(dir, names, io = {}) {
214
321
  if (!exists(file)) return [];
215
322
 
216
323
  const text = String(read(file, "utf8"));
217
- const start = text.indexOf(MANAGED_MARKER);
218
- const endAt = start === -1 ? -1 : text.indexOf(MANAGED_END, start);
219
- const outside =
220
- start === -1
221
- ? text
222
- : text.slice(0, start) +
223
- (endAt === -1 ? "" : text.slice(endAt + MANAGED_END.length));
324
+ // Every family block, not just the current version's. An orphan left by an
325
+ // older marker is still OUR previous output, so counting its profiles as a
326
+ // host collision would refuse to write the very names we wrote last time.
327
+ const outside = stripFamilyBlocks(text, MANAGED_FAMILY);
224
328
 
225
329
  return names.filter(name =>
226
330
  new RegExp(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -68,11 +68,114 @@ function writeAtomic(destination, contents) {
68
68
  * @param {object} [cfg] Resolved configuration.
69
69
  * @returns {{count: number, dir: string}} What was written, and where.
70
70
  */
71
+ /**
72
+ * Marker version this build writes. Bumping it is SAFE, which is the point.
73
+ *
74
+ * The recognisers below match the FAMILY — the frozen identifier plus any
75
+ * version — rather than one literal string, so a block written by an older
76
+ * build is still found and replaced in place. Before that, the reader looked
77
+ * for the exact text it was about to write, so the first rename made it
78
+ * conclude there was no block here and append a second one.
79
+ *
80
+ * That failure is additive and silent, and this module is the worst place in
81
+ * Lisa for it: it writes into `~/.aws` files and a shell profile, outside any
82
+ * repository, where no apply, diff, or review ever revisits the result. An
83
+ * orphaned block in a shell profile is STILL SOURCED, and profiles apply
84
+ * assignments in order — so whichever block comes last wins, and an operator
85
+ * can end up running under credentials from a block Lisa believes it no longer
86
+ * manages. That is credential selection going wrong with no review step.
87
+ *
88
+ * Everywhere else in the codebase the mitigation is "widen the recogniser and
89
+ * accept the old shape until an apply normalises the file". There is no such
90
+ * operation here: the old population never drains and cannot be counted, so
91
+ * the recogniser must accept every past version permanently.
92
+ *
93
+ * Following `core/apply-receipt`, which treats a `schema_version` it does not
94
+ * recognise as NO RECEIPT rather than one it half-understands: on an
95
+ * unrecognised marker, fail toward redoing the work, not toward assuming it is
96
+ * done. Here that means replacing every family member found, so a file already
97
+ * carrying orphans from a past rename is repaired on the next run rather than
98
+ * accumulating one more.
99
+ */
100
+ const MARKER_VERSION = "v2";
101
+
102
+ /**
103
+ * The frozen half of each marker. Changing one of these DOES orphan blocks —
104
+ * that is the whole contract, and it is why the version lives beside it.
105
+ *
106
+ * This does not make renaming impossible, it makes it deliberate: an innocuous
107
+ * text edit no longer orphans a block, and the one edit that still would is the
108
+ * one these comments forbid.
109
+ */
110
+ const PROFILE_FAMILY = "lisa secrets (managed";
111
+
112
+ /** The frozen family identifier for an `~/.aws` managed region. */
113
+ const MANAGED_FAMILY = "managed by lisa-secrets-access";
114
+
115
+ /**
116
+ * Build the start/end recognisers for one marker family.
117
+ *
118
+ * Matches the family followed by anything up to the closing delimiter, so
119
+ * `(managed)` — every block in the field today — and `(managed v2)` are both
120
+ * found by the same reader.
121
+ * @param {string} family Frozen family identifier.
122
+ * @returns {{start: RegExp, end: RegExp}} Global recognisers for the family.
123
+ */
124
+ function familyRecognisers(family) {
125
+ const quoted = family.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
126
+ return {
127
+ start: new RegExp(`# >>> ${quoted}[^\\n]*>>>`, "g"),
128
+ end: new RegExp(`# <<< ${quoted}[^\\n]*<<<`, "g"),
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Index of the first family match at or after `from`, and its length.
134
+ * @param {string} text Haystack.
135
+ * @param {RegExp} recogniser Global family recogniser.
136
+ * @param {number} from Index to search from.
137
+ * @returns {{index: number, length: number}} `index` is -1 when absent.
138
+ */
139
+ function findFamily(text, recogniser, from = 0) {
140
+ recogniser.lastIndex = from;
141
+ const match = recogniser.exec(text);
142
+ return match === null
143
+ ? { index: -1, length: 0 }
144
+ : { index: match.index, length: match[0].length };
145
+ }
146
+
147
+ /**
148
+ * Remove every managed block of one family, leaving the rest of the file.
149
+ *
150
+ * Every block, not just the first: a file that already carries orphans from a
151
+ * rename that happened before this fix must come out with exactly one block,
152
+ * or the fix would leave the damage it exists to prevent.
153
+ * @param {string} text File contents.
154
+ * @param {string} family Frozen family identifier.
155
+ * @returns {string} The file with every family block removed.
156
+ */
157
+ function stripFamilyBlocks(text, family) {
158
+ const { start, end } = familyRecognisers(family);
159
+ let out = text;
160
+ for (;;) {
161
+ const opened = findFamily(out, start);
162
+ if (opened.index === -1) return out;
163
+ const closed = findFamily(out, end, opened.index + opened.length);
164
+ // A truncated block (opened, never closed) would otherwise swallow the rest
165
+ // of the file on every subsequent write.
166
+ const after =
167
+ closed.index === -1
168
+ ? ""
169
+ : out.slice(closed.index + closed.length).replace(/^\n/, "");
170
+ out = `${out.slice(0, opened.index)}${after}`;
171
+ }
172
+ }
173
+
71
174
  /** Marks the block this owns, so it is replaced rather than appended twice. */
72
- const PROFILE_MARKER = "# >>> lisa secrets (managed) >>>";
175
+ const PROFILE_MARKER = `# >>> ${PROFILE_FAMILY} ${MARKER_VERSION}) >>>`;
73
176
 
74
177
  /** Closes the managed block. */
75
- const PROFILE_END = "# <<< lisa secrets (managed) <<<";
178
+ const PROFILE_END = `# <<< ${PROFILE_FAMILY} ${MARKER_VERSION}) <<<`;
76
179
 
77
180
  /**
78
181
  * Identifies an `~/.aws` file as one this wrote, and may therefore replace.
@@ -81,10 +184,10 @@ const PROFILE_END = "# <<< lisa secrets (managed) <<<";
81
184
  * consumer while still being the thing that distinguishes "our file, refresh
82
185
  * it" from "someone else's file, leave it alone".
83
186
  */
84
- const MANAGED_MARKER = "# >>> managed by lisa-secrets-access >>>";
187
+ const MANAGED_MARKER = `# >>> ${MANAGED_FAMILY} ${MARKER_VERSION} >>>`;
85
188
 
86
189
  /** Closes the managed region of an `~/.aws` file. */
87
- const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
190
+ const MANAGED_END = `# <<< ${MANAGED_FAMILY} ${MARKER_VERSION} <<<`;
88
191
 
89
192
  /**
90
193
  * Replace this module's delimited region in a file, preserving everything else.
@@ -99,22 +202,21 @@ const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
99
202
  */
100
203
  export function upsertManagedBlock(current, body) {
101
204
  const block = `${MANAGED_MARKER}\n${body.trimEnd()}\n${MANAGED_END}`;
102
- const start = current.indexOf(MANAGED_MARKER);
205
+ const opened = findFamily(current, familyRecognisers(MANAGED_FAMILY).start);
103
206
 
104
- if (start === -1) {
207
+ if (opened.index === -1) {
105
208
  const prefix =
106
209
  current && !current.endsWith("\n") ? `${current}\n` : current;
107
210
  return `${prefix}${prefix ? "\n" : ""}${block}\n`;
108
211
  }
109
212
 
110
- const endAt = current.indexOf(MANAGED_END, start);
111
- // A truncated block (marker opened, never closed) would otherwise swallow the
112
- // rest of the file on every subsequent write.
113
- const after =
114
- endAt === -1
115
- ? ""
116
- : current.slice(endAt + MANAGED_END.length).replace(/^\n/, "");
117
- return `${current.slice(0, start)}${block}\n${after}`;
213
+ // Everything this module owns is stripped first, so a file already carrying
214
+ // orphans from a rename that predates the family recogniser comes out with
215
+ // exactly one block rather than one more.
216
+ const withoutOurs = stripFamilyBlocks(current, MANAGED_FAMILY);
217
+ const head = withoutOurs.slice(0, opened.index);
218
+ const tail = withoutOurs.slice(opened.index);
219
+ return `${head}${block}\n${tail}`;
118
220
  }
119
221
 
120
222
  /**
@@ -173,13 +275,18 @@ export function installProfileSourcing(valuesFile, options = {}) {
173
275
 
174
276
  // Replace an existing managed block rather than appending another: this
175
277
  // runs on every session, and an appended-forever profile is its own bug.
176
- const start = current.indexOf(PROFILE_MARKER);
278
+ const opened = findFamily(current, familyRecognisers(PROFILE_FAMILY).start);
279
+ // Stripping EVERY family block before writing one is what makes an already
280
+ // orphaned profile self-heal. It matters more here than anywhere else in
281
+ // Lisa: an orphaned block in a shell profile is still sourced, and the last
282
+ // assignment wins, so a stale block silently selects the wrong credentials.
283
+ const withoutOurs = stripFamilyBlocks(current, PROFILE_FAMILY);
177
284
  const next =
178
- start === -1
285
+ opened.index === -1
179
286
  ? `${current}${current.endsWith("\n") || !current ? "" : "\n"}\n${block}\n`
180
- : `${current.slice(0, start)}${block}${current.slice(
181
- current.indexOf(PROFILE_END, start) + PROFILE_END.length
182
- )}`;
287
+ : `${withoutOurs.slice(0, opened.index)}${block}\n${withoutOurs
288
+ .slice(opened.index)
289
+ .replace(/^\n/, "")}`;
183
290
 
184
291
  if (next !== current) {
185
292
  write(file, next, { mode: 0o600 });
@@ -214,13 +321,10 @@ export function collidingProfiles(dir, names, io = {}) {
214
321
  if (!exists(file)) return [];
215
322
 
216
323
  const text = String(read(file, "utf8"));
217
- const start = text.indexOf(MANAGED_MARKER);
218
- const endAt = start === -1 ? -1 : text.indexOf(MANAGED_END, start);
219
- const outside =
220
- start === -1
221
- ? text
222
- : text.slice(0, start) +
223
- (endAt === -1 ? "" : text.slice(endAt + MANAGED_END.length));
324
+ // Every family block, not just the current version's. An orphan left by an
325
+ // older marker is still OUR previous output, so counting its profiles as a
326
+ // host collision would refuse to write the very names we wrote last time.
327
+ const outside = stripFamilyBlocks(text, MANAGED_FAMILY);
224
328
 
225
329
  return names.filter(name =>
226
330
  new RegExp(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"