@codyswann/lisa 4.26.5 → 4.27.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 (89) hide show
  1. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  2. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  3. package/dist/core/upstream-evidence-manifest.js +10 -6
  4. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  5. package/package.json +3 -3
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  9. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +10 -0
  10. package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +39 -4
  11. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  12. package/plugins/lisa/scripts/install-remote-agent-aws.mjs +52 -6
  13. package/plugins/lisa/scripts/remote-agent-aws-setup.sh +319 -15
  14. package/plugins/lisa/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  15. package/plugins/lisa/skills/lisa-implement/SKILL.md +10 -0
  16. package/plugins/lisa/skills/lisa-linear-access/SKILL.md +39 -4
  17. package/plugins/lisa/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  18. package/plugins/lisa-agy/plugin.json +1 -1
  19. package/plugins/lisa-agy/scripts/install-remote-agent-aws.mjs +52 -6
  20. package/plugins/lisa-agy/scripts/remote-agent-aws-setup.sh +319 -15
  21. package/plugins/lisa-agy/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  22. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +10 -0
  23. package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +39 -4
  24. package/plugins/lisa-agy/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  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-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/scripts/install-remote-agent-aws.mjs +52 -6
  32. package/plugins/lisa-copilot/scripts/remote-agent-aws-setup.sh +319 -15
  33. package/plugins/lisa-copilot/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  34. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +10 -0
  35. package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +39 -4
  36. package/plugins/lisa-copilot/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  37. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-cursor/scripts/install-remote-agent-aws.mjs +52 -6
  39. package/plugins/lisa-cursor/scripts/remote-agent-aws-setup.sh +319 -15
  40. package/plugins/lisa-cursor/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  41. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +10 -0
  42. package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +39 -4
  43. package/plugins/lisa-cursor/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  44. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-expo-agy/plugin.json +1 -1
  47. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  57. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  62. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  67. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails-agy/plugin.json +1 -1
  72. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  77. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  81. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  82. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  84. package/plugins/src/base/scripts/install-remote-agent-aws.mjs +52 -6
  85. package/plugins/src/base/scripts/remote-agent-aws-setup.sh +319 -15
  86. package/plugins/src/base/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  87. package/plugins/src/base/skills/lisa-implement/SKILL.md +10 -0
  88. package/plugins/src/base/skills/lisa-linear-access/SKILL.md +39 -4
  89. package/plugins/src/base/skills/lisa-setup-remote-aws/SKILL.md +33 -2
@@ -13,13 +13,40 @@
13
13
  # contains.
14
14
  # Optional plain variables:
15
15
  # LISA_REMOTE_AGENT claude | codex | cursor | copilot | agy | opencode
16
- # LISA_AWS_DEFAULT_PROFILE Defaults to dev, then the first available profile.
16
+ # LISA_AWS_PROFILE_NAMESPACE
17
+ # The project component every written profile is
18
+ # scoped by. Falls back to the bundle's `namespace`
19
+ # (or `project`), then to `<owner>-<repo>` from the
20
+ # git origin remote. Required when none resolves.
21
+ # LISA_AWS_DEFAULT_PROFILE Stage selected as the default. Defaults to dev,
22
+ # then the first stage in the bundle.
23
+ # LISA_AWS_EXPECTED_ACCOUNT_ID
24
+ # Operator-declared account for the default stage.
25
+ # Checked against the bundle before anything is
26
+ # written, and against the live identity after.
27
+ # LISA_AWS_VERIFY_ALL_PROFILES
28
+ # 1 to prove every stage's account, not just the
29
+ # default's.
30
+ # LISA_AWS_CLAIM_DEFAULT_PROFILE
31
+ # 1 to take over a `[default]` this project does not
32
+ # already own. Off by default: silently repointing
33
+ # `default` is how one tenant's commands end up in
34
+ # another tenant's account.
35
+ # LISA_AWS_PRUNE_LEGACY_PROFILES
36
+ # 1 to delete the unnamespaced profiles an earlier
37
+ # Lisa bootstrap wrote. Off by default; they are
38
+ # reported either way, never silently orphaned.
17
39
  # LISA_AWS_SKIP_VERIFY Set to 1 only for an offline image build.
18
40
 
19
41
  set -euo pipefail
20
42
  umask 077
21
43
 
22
- BOOTSTRAP_PROFILE="lisa-remote-agent-bootstrap"
44
+ # Profiles are scoped `<namespace>-agent-<stage>`, matching the convention every
45
+ # other profile on a multi-organisation workstation already follows. The old
46
+ # name is retained only so an earlier bootstrap's output can be recognised and
47
+ # migrated rather than left behind unexplained.
48
+ BOOTSTRAP_PROFILE_SUFFIX="-agent-bootstrap"
49
+ LEGACY_BOOTSTRAP_PROFILE="lisa-remote-agent-bootstrap"
23
50
  AWS_CLI_VERSION="2.36.2"
24
51
 
25
52
  fail() {
@@ -141,6 +168,84 @@ remove_profile_setting() {
141
168
  mv "$temporary_file" "$credentials_file"
142
169
  }
143
170
 
171
+ # Delete one whole ini section, header and body, from an AWS shared-config file.
172
+ #
173
+ # Used only for the migration path: profiles a PREVIOUS Lisa bootstrap wrote
174
+ # under the old unnamespaced convention. Sections this script never wrote are
175
+ # never candidates — see legacy_profile_sections, which identifies ours by the
176
+ # retired source_profile name rather than by guessing from the profile name.
177
+ remove_profile_section() {
178
+ local file section temporary_file
179
+ file="$1"
180
+ section="$2"
181
+ [ -f "$file" ] || return 0
182
+
183
+ temporary_file="$(mktemp "${file}.XXXXXX")"
184
+ awk -v section="$section" '
185
+ /^\[[^]]*\][[:space:]]*$/ {
186
+ current = $0
187
+ sub(/^\[[[:space:]]*/, "", current)
188
+ sub(/[[:space:]]*\][[:space:]]*$/, "", current)
189
+ in_section = current == section
190
+ if (in_section) next
191
+ }
192
+ !in_section { print }
193
+ ' "$file" >"$temporary_file"
194
+ chmod 600 "$temporary_file"
195
+ mv "$temporary_file" "$file"
196
+ }
197
+
198
+ # Read one setting out of one ini section, or print nothing.
199
+ profile_setting_value() {
200
+ local file section setting
201
+ file="$1"
202
+ section="$2"
203
+ setting="$3"
204
+ [ -f "$file" ] || return 0
205
+
206
+ awk -v section="$section" -v setting="$setting" '
207
+ /^\[[^]]*\][[:space:]]*$/ {
208
+ current = $0
209
+ sub(/^\[[[:space:]]*/, "", current)
210
+ sub(/[[:space:]]*\][[:space:]]*$/, "", current)
211
+ in_section = current == section
212
+ next
213
+ }
214
+ in_section && $0 ~ "^[[:space:]]*" setting "[[:space:]]*=" {
215
+ sub(/^[^=]*=[[:space:]]*/, "")
216
+ sub(/[[:space:]]*$/, "")
217
+ print
218
+ exit
219
+ }
220
+ ' "$file"
221
+ }
222
+
223
+ # Every section written by the OLD, unnamespaced convention.
224
+ #
225
+ # Identified by the retired bootstrap profile name, which only Lisa ever wrote,
226
+ # so an operator's own `[profile dev]` can never be mistaken for ours.
227
+ legacy_profile_sections() {
228
+ local file
229
+ file="$1"
230
+ [ -f "$file" ] || return 0
231
+
232
+ awk -v bootstrap="$LEGACY_BOOTSTRAP_PROFILE" '
233
+ /^\[[^]]*\][[:space:]]*$/ {
234
+ current = $0
235
+ sub(/^\[[[:space:]]*/, "", current)
236
+ sub(/[[:space:]]*\][[:space:]]*$/, "", current)
237
+ section = current
238
+ next
239
+ }
240
+ $0 ~ "^[[:space:]]*source_profile[[:space:]]*=" {
241
+ value = $0
242
+ sub(/^[^=]*=[[:space:]]*/, "", value)
243
+ sub(/[[:space:]]*$/, "", value)
244
+ if (value == bootstrap && section != "") print section
245
+ }
246
+ ' "$file"
247
+ }
248
+
144
249
  sanitize_session_name() {
145
250
  local candidate
146
251
  candidate="$(printf '%s' "${LISA_REMOTE_AGENT:-remote-agent}" \
@@ -150,6 +255,125 @@ sanitize_session_name() {
150
255
  printf '%s\n' "$candidate"
151
256
  }
152
257
 
258
+ # Reduce any project identifier to the safe, stable component of a profile name.
259
+ sanitize_namespace() {
260
+ printf '%s' "$1" \
261
+ | tr '[:upper:]' '[:lower:]' \
262
+ | sed -e 's/[^a-z0-9]\{1,\}/-/g' -e 's/^-*//' -e 's/-*$//' \
263
+ | cut -c1-48
264
+ }
265
+
266
+ # `<owner>-<repository>` for the checkout this script was run from.
267
+ #
268
+ # The forge owner is what makes the result distinguishing: two organisations
269
+ # routinely have a repository of the same NAME, which is the collision this
270
+ # whole change exists to remove, and they cannot share an owner.
271
+ namespace_from_git() {
272
+ need git || return 0
273
+
274
+ local url slug
275
+ url="$(git config --get remote.origin.url 2>/dev/null || true)"
276
+ [ -n "$url" ] || return 0
277
+ url="${url%.git}"
278
+ url="${url%/}"
279
+ slug="$(printf '%s' "$url" \
280
+ | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#^[^@/]*@##' -e 's#^[^/:]*[:/]##')"
281
+ printf '%s' "$slug" | tr '/' '-'
282
+ }
283
+
284
+ # The project component every profile this script writes is scoped by.
285
+ #
286
+ # Bare stage names (`dev`, `production`) name a stage but not an OWNER. On a
287
+ # workstation carrying more than one organisation two bundles then write the
288
+ # same names into the same shared `~/.aws/config`, the last bootstrap wins, and
289
+ # every later command resolves another tenant's account while reporting success.
290
+ # Nothing about the resulting profile is malformed, so no check on the profile
291
+ # can catch it; only a name that carries the owner can prevent it.
292
+ # Prints `<namespace>\t<where it came from>`. Both halves are returned together
293
+ # because this runs in a command substitution: a variable assigned inside a
294
+ # subshell never reaches the caller, and reporting the wrong provenance is how
295
+ # an operator ends up unable to explain why their profiles are named what they
296
+ # are named.
297
+ resolve_namespace() {
298
+ local candidate sanitized namespace_source
299
+ namespace_source="LISA_AWS_PROFILE_NAMESPACE"
300
+ candidate="${LISA_AWS_PROFILE_NAMESPACE:-}"
301
+ if [ -z "$candidate" ]; then
302
+ namespace_source="the bootstrap bundle"
303
+ candidate="$(printf '%s' "$bootstrap_json" | jq -r '.namespace // .project // empty')"
304
+ fi
305
+ if [ -z "$candidate" ]; then
306
+ namespace_source="git remote origin"
307
+ candidate="$(namespace_from_git)"
308
+ fi
309
+ [ -n "$candidate" ] || fail \
310
+ "cannot determine which project these AWS profiles belong to. Set LISA_AWS_PROFILE_NAMESPACE (for example the repository slug), add a \"namespace\" to the bootstrap bundle, or run this from a checkout with an origin remote. Unnamespaced profiles collide across organisations on a shared workstation."
311
+
312
+ sanitized="$(sanitize_namespace "$candidate")"
313
+ printf '%s' "$sanitized" | grep -qE '^[a-z0-9][a-z0-9-]{1,47}$' || fail \
314
+ "profile namespace from ${namespace_source} (\"${candidate}\") does not reduce to a usable name; set LISA_AWS_PROFILE_NAMESPACE to two or more alphanumeric characters"
315
+ printf '%s\t%s\n' "$sanitized" "$namespace_source"
316
+ }
317
+
318
+ # Prove which ACCOUNT a profile reaches, not merely that it authenticates.
319
+ #
320
+ # `sts:GetCallerIdentity` succeeding proves the credentials work. It says
321
+ # nothing about whose account they work in, and the old code never looked at
322
+ # what it returned — a lookup whose success was read as the answer. Comparing
323
+ # the returned account against the one named in the role ARN just configured is
324
+ # what turns a liveness probe into an identity check.
325
+ assert_profile_account() {
326
+ local profile expected stage identity actual
327
+ profile="$1"
328
+ expected="$2"
329
+ stage="$3"
330
+
331
+ identity="$(AWS_PAGER="" aws sts get-caller-identity --profile "$profile" --output json)" || fail \
332
+ "sts:GetCallerIdentity failed for profile ${profile} (stage ${stage})"
333
+ actual="$(printf '%s' "$identity" | jq -r '.Account // empty')"
334
+ [ -n "$actual" ] || fail \
335
+ "sts:GetCallerIdentity returned no account id for profile ${profile} (stage ${stage}); cannot prove which account it reached"
336
+ [ "$actual" = "$expected" ] || fail \
337
+ "profile ${profile} (stage ${stage}) resolved to AWS account ${actual}, but this project expects ${expected}. The credentials authenticate; they are not this project's account. Refusing to report ready."
338
+ }
339
+
340
+ # Who currently owns `[default]`.
341
+ #
342
+ # `default` is the one name that cannot be namespaced, so it is the one place
343
+ # two projects still contend. Taking it over silently is the whole defect in
344
+ # miniature: every unqualified `aws` command would change accounts with nothing
345
+ # said. An inert `[default]` holding only region/output is not a claim on it,
346
+ # and a container that ships one must still be able to bootstrap.
347
+ default_profile_owner() {
348
+ local source_value setting
349
+ source_value="$(profile_setting_value "$config_file" "default" "source_profile")"
350
+ if [ "$source_value" = "$BOOTSTRAP_PROFILE" ]; then
351
+ printf 'ours\n'
352
+ return 0
353
+ fi
354
+ if [ "$source_value" = "$LEGACY_BOOTSTRAP_PROFILE" ]; then
355
+ printf 'legacy\n'
356
+ return 0
357
+ fi
358
+ case "$source_value" in
359
+ *"$BOOTSTRAP_PROFILE_SUFFIX")
360
+ printf 'another Lisa project\n'
361
+ return 0
362
+ ;;
363
+ esac
364
+ for setting in role_arn credential_process sso_start_url sso_session aws_access_key_id; do
365
+ if [ -n "$(profile_setting_value "$config_file" "default" "$setting")" ]; then
366
+ printf 'this workstation operator\n'
367
+ return 0
368
+ fi
369
+ done
370
+ if [ -n "$(profile_setting_value "$credentials_file" "default" "aws_access_key_id")" ]; then
371
+ printf 'this workstation operator\n'
372
+ return 0
373
+ fi
374
+ printf 'unclaimed\n'
375
+ }
376
+
153
377
  install_base_tools
154
378
  install_aws_cli
155
379
 
@@ -181,21 +405,89 @@ profiles_json="$(printf '%s' "$bootstrap_json" | jq -c '
181
405
  end
182
406
  ')"
183
407
 
408
+ namespace_resolution="$(resolve_namespace)"
409
+ PROFILE_NAMESPACE="${namespace_resolution%%$'\t'*}"
410
+ namespace_source="${namespace_resolution#*$'\t'}"
411
+ BOOTSTRAP_PROFILE="${PROFILE_NAMESPACE}${BOOTSTRAP_PROFILE_SUFFIX}"
412
+
413
+ # The role ARN carries the account the role lives in, so every bundle already
414
+ # declares an expected account per stage whether or not it says so explicitly.
415
+ # Requiring the full ARN shape is what makes that field readable at all.
184
416
  printf '%s' "$profiles_json" | jq -e --arg bootstrap_profile "$BOOTSTRAP_PROFILE" '
185
417
  type == "object" and length > 0 and
186
418
  all(to_entries[];
187
419
  (.key != $bootstrap_profile) and
420
+ (.key != "bootstrap") and
188
421
  (.key | test("^[A-Za-z0-9_-]+$")) and
189
- (.value.roleArn | type == "string" and startswith("arn:aws:iam::")) and
422
+ (.value.roleArn | type == "string" and test("^arn:aws:iam::[0-9]{12}:role/.+")) and
190
423
  (.value.region | type == "string" and length > 0)
191
424
  )
192
- ' >/dev/null || fail "bootstrap profiles must map safe names to roleArn and region"
425
+ ' >/dev/null || fail "bootstrap profiles must map safe names (not \"bootstrap\") to a full arn:aws:iam::<account>:role/<name> roleArn and a region"
426
+
427
+ # An explicitly declared account that disagrees with the role ARN is a bundle
428
+ # that cannot be believed. Fail here, before anything is written, naming both.
429
+ while IFS="$(printf '\t')" read -r stage declared_account arn_account; do
430
+ [ "$declared_account" = "$arn_account" ] || fail \
431
+ "bundle stage ${stage} declares expectedAccountId ${declared_account} but its roleArn names account ${arn_account}"
432
+ done < <(printf '%s' "$profiles_json" | jq -r '
433
+ to_entries[]
434
+ | select(.value.expectedAccountId != null)
435
+ | [.key, (.value.expectedAccountId | tostring), (.value.roleArn | split(":")[4])]
436
+ | @tsv
437
+ ')
438
+
439
+ default_stage="${LISA_AWS_DEFAULT_PROFILE:-dev}"
440
+ if ! printf '%s' "$profiles_json" | jq -e --arg stage "$default_stage" 'has($stage)' >/dev/null; then
441
+ default_stage="$(printf '%s' "$profiles_json" | jq -r 'keys[0]')"
442
+ fi
443
+
444
+ default_role_arn="$(printf '%s' "$profiles_json" | jq -er --arg stage "$default_stage" '.[$stage].roleArn')"
445
+ default_region="$(printf '%s' "$profiles_json" | jq -er --arg stage "$default_stage" '.[$stage].region')"
446
+ default_expected_account="$(printf '%s' "$profiles_json" | jq -er --arg stage "$default_stage" '
447
+ (.[$stage].expectedAccountId // (.[$stage].roleArn | split(":")[4])) | tostring
448
+ ')"
449
+
450
+ if [ -n "${LISA_AWS_EXPECTED_ACCOUNT_ID:-}" ] \
451
+ && [ "$LISA_AWS_EXPECTED_ACCOUNT_ID" != "$default_expected_account" ]; then
452
+ fail "LISA_AWS_EXPECTED_ACCOUNT_ID is ${LISA_AWS_EXPECTED_ACCOUNT_ID} but the bundle's ${default_stage} role is in account ${default_expected_account}"
453
+ fi
193
454
 
194
455
  credentials_file="${AWS_SHARED_CREDENTIALS_FILE:-$HOME/.aws/credentials}"
195
456
  config_file="${AWS_CONFIG_FILE:-$HOME/.aws/config}"
196
457
  mkdir -p "$HOME/.aws" "$(dirname "$credentials_file")" "$(dirname "$config_file")"
197
458
  chmod 700 "$HOME/.aws"
198
459
 
460
+ profile_names="$(printf '%s' "$profiles_json" \
461
+ | jq -r --arg namespace "$PROFILE_NAMESPACE" 'keys | map($namespace + "-agent-" + .) | join(", ")')"
462
+
463
+ # Migration: the previous convention's profiles are still on this workstation.
464
+ #
465
+ # They are reported rather than removed, because deleting a profile an operator
466
+ # may still be using is its own silent surprise. Pruning is opt-in and says
467
+ # exactly what it removed.
468
+ legacy_sections="$(legacy_profile_sections "$config_file")"
469
+ legacy_names=""
470
+ if [ -n "$legacy_sections" ]; then
471
+ legacy_names="$(printf '%s\n' "$legacy_sections" | sed 's/^profile //' | paste -sd ',' - | sed 's/,/, /g')"
472
+ if [ "${LISA_AWS_PRUNE_LEGACY_PROFILES:-0}" = "1" ]; then
473
+ printf '%s\n' "$legacy_sections" | while IFS= read -r section; do
474
+ [ -n "$section" ] || continue
475
+ remove_profile_section "$config_file" "$section"
476
+ done
477
+ remove_profile_section "$config_file" "profile ${LEGACY_BOOTSTRAP_PROFILE}"
478
+ remove_profile_section "$credentials_file" "$LEGACY_BOOTSTRAP_PROFILE"
479
+ fi
480
+ fi
481
+
482
+ default_owner="$(default_profile_owner)"
483
+ case "$default_owner" in
484
+ ours | legacy | unclaimed) ;;
485
+ *)
486
+ [ "${LISA_AWS_CLAIM_DEFAULT_PROFILE:-0}" = "1" ] || fail \
487
+ "refusing to overwrite the [default] AWS profile in ${config_file}: it belongs to ${default_owner}, not to ${PROFILE_NAMESPACE}. Overwriting it would silently repoint every unqualified aws command at this project's accounts. Use the named profiles instead (${profile_names}), or set LISA_AWS_CLAIM_DEFAULT_PROFILE=1 to take it over deliberately."
488
+ ;;
489
+ esac
490
+
199
491
  aws configure set aws_access_key_id "$access_key_id" --profile "$BOOTSTRAP_PROFILE"
200
492
  aws configure set aws_secret_access_key "$secret_access_key" --profile "$BOOTSTRAP_PROFILE"
201
493
  if [ -n "$session_token" ]; then
@@ -208,7 +500,8 @@ else
208
500
  fi
209
501
 
210
502
  session_name="$(sanitize_session_name)"
211
- while IFS=$'\t' read -r profile_name role_arn region; do
503
+ while IFS="$(printf '\t')" read -r stage role_arn region; do
504
+ profile_name="${PROFILE_NAMESPACE}-agent-${stage}"
212
505
  aws configure set role_arn "$role_arn" --profile "$profile_name"
213
506
  aws configure set source_profile "$BOOTSTRAP_PROFILE" --profile "$profile_name"
214
507
  aws configure set external_id "$external_id" --profile "$profile_name"
@@ -216,13 +509,7 @@ while IFS=$'\t' read -r profile_name role_arn region; do
216
509
  aws configure set region "$region" --profile "$profile_name"
217
510
  done < <(printf '%s' "$profiles_json" | jq -r 'to_entries[] | [.key, .value.roleArn, .value.region] | @tsv')
218
511
 
219
- default_profile="${LISA_AWS_DEFAULT_PROFILE:-dev}"
220
- if ! printf '%s' "$profiles_json" | jq -e --arg profile "$default_profile" 'has($profile)' >/dev/null; then
221
- default_profile="$(printf '%s' "$profiles_json" | jq -r 'keys[0]')"
222
- fi
223
-
224
- default_role_arn="$(printf '%s' "$profiles_json" | jq -er --arg profile "$default_profile" '.[$profile].roleArn')"
225
- default_region="$(printf '%s' "$profiles_json" | jq -er --arg profile "$default_profile" '.[$profile].region')"
512
+ default_profile="${PROFILE_NAMESPACE}-agent-${default_stage}"
226
513
  aws configure set role_arn "$default_role_arn" --profile default
227
514
  aws configure set source_profile "$BOOTSTRAP_PROFILE" --profile default
228
515
  aws configure set external_id "$external_id" --profile default
@@ -232,8 +519,25 @@ aws configure set region "$default_region" --profile default
232
519
  chmod 600 "$credentials_file" "$config_file"
233
520
 
234
521
  if [ "${LISA_AWS_SKIP_VERIFY:-0}" != "1" ]; then
235
- AWS_PAGER="" aws sts get-caller-identity --profile "$default_profile" >/dev/null
522
+ if [ "${LISA_AWS_VERIFY_ALL_PROFILES:-0}" = "1" ]; then
523
+ while IFS="$(printf '\t')" read -r stage expected_account; do
524
+ assert_profile_account "${PROFILE_NAMESPACE}-agent-${stage}" "$expected_account" "$stage"
525
+ done < <(printf '%s' "$profiles_json" | jq -r '
526
+ to_entries[]
527
+ | [.key, ((.value.expectedAccountId // (.value.roleArn | split(":")[4])) | tostring)]
528
+ | @tsv
529
+ ')
530
+ else
531
+ assert_profile_account "$default_profile" "$default_expected_account" "$default_stage"
532
+ fi
533
+ fi
534
+
535
+ if [ -n "$legacy_names" ]; then
536
+ if [ "${LISA_AWS_PRUNE_LEGACY_PROFILES:-0}" = "1" ]; then
537
+ echo "remote-agent-aws-setup: removed unnamespaced profiles left by an earlier bootstrap: ${legacy_names}" >&2
538
+ else
539
+ echo "remote-agent-aws-setup: ${config_file} still contains unnamespaced profiles written by an earlier Lisa bootstrap: ${legacy_names}. They name a stage but no owner, so on a workstation carrying more than one organisation they can resolve to another tenant's account. Review them, or re-run with LISA_AWS_PRUNE_LEGACY_PROFILES=1 to delete the ones Lisa wrote." >&2
540
+ fi
236
541
  fi
237
542
 
238
- profile_names="$(printf '%s' "$profiles_json" | jq -r 'keys | join(", ")')"
239
- echo "remote-agent-aws-setup: ready (${session_name}; default=${default_profile}; profiles=${profile_names})"
543
+ echo "remote-agent-aws-setup: ready (${session_name}; namespace=${PROFILE_NAMESPACE} via ${namespace_source}; default=${default_profile} in account ${default_expected_account}; profiles=${profile_names})"
@@ -369,9 +369,9 @@ so the generator can render acquisition comments into its template:
369
369
  ],
370
370
  "awsProfiles": [
371
371
  {
372
- "name": "dev",
372
+ "name": "<project>-agent-dev",
373
373
  "roleArn": "arn:aws:iam::<account-id>:role/<role-name>",
374
- "credentialSource": "lisa-remote-agent-bootstrap",
374
+ "credentialSource": "<project>-agent-bootstrap",
375
375
  "externalId": "<project-external-id>",
376
376
  "region": "us-east-1"
377
377
  }
@@ -346,6 +346,16 @@ Before shutting down the team, execute the Verify flow:
346
346
  4. Record Implement usage on the originating work artifact via `lisa-usage-accounting` so the work item (or other implementation-owned artifact) gains a direct `lisa-implement` usage entry in the canonical `## Lisa Usage` section. If the parent / child graph is already known, prefer `record_and_rollup` so ancestor totals refresh in the same write; otherwise still write the direct entry, and if runtime usage is unavailable, use `source: unavailable` with nullable token/cost fields instead of skipping the row.
347
347
  5. Commit ALL outstanding changes in logical batches on the branch (minus sensitive data/information) — not just changes made by the agent team. This includes pre-existing uncommitted changes that were on the branch before the plan started. Do NOT filter commits to only "task-related" files. If it shows up in git status, it gets committed (unless it contains secrets).
348
348
  6. Push the changes - if any pre-push hook blocks you, create a task for the agent team to fix the error/problem whether it was pre-existing or not
349
+ 6a. **Review the branch locally BEFORE opening the pull request, every time.** Invoke `lisa-review-local` over the branch diff, triage what it finds under the `convergent-review` rule (same contract as any other reviewer — fix what is valid, refute what is not, on the record), and commit the fixes. Only then proceed to step 7.
350
+
351
+ **Unconditional, not a fallback.** Do not gate this on whether a third-party reviewer is available, rate limited, or skipped. Measured on this repository, 47 of the last 60 merged pull requests carried a review context reporting `SUCCESS` while having read nothing — 29 rate limited, 16 skipped because the vendor does not auto-review public repositories, one skipped on size. A rule that fires only when the vendor is absent has to detect four distinct vacuity strings correctly, forever, and still leaves the vendor being *present but wrong* unguarded. Reviewing always needs no detection at all and makes the third-party review **additive**: its findings still arrive at step 8 and are handled there exactly as today.
352
+
353
+ **Before the pull request, not after it.** Auto-merge is armed at submit time, so the pull request can merge the moment CI goes green — a review that starts after step 7 is racing a latch that is already set. Running here has nothing to race.
354
+
355
+ **A local review is a weaker claim than an independent one, and the report must say so.** The agents reviewing the branch are the same team that wrote it. Record the outcome as *self-reviewed* — never as *reviewed* — and never describe it as satisfying the third-party review check, which is ruleset-required and can only be posted by that vendor's app. Collapsing the two would be the same vacuous-success failure this step exists to remove, one level up.
356
+
357
+ **If no subagent delegation is available in this runtime**, say so explicitly in the completion summary and proceed — but record that no local review was possible. "Could not review" must never be reported as "reviewed and found nothing"; an unavailable check is not a passing one.
358
+
349
359
  7. Open a pull request with auto-merge on via `lisa-git-submit-pr`, targeting the **base branch resolved from the ticket's environment** (`target_branch=<base>`, per the branch step above), and including the mandatory work-item ref so the PR can be linked natively to the source issue.
350
360
  7a. Confirm two-way linkage before treating PR submission as complete: the PR body/title/branch must reference the work item, and the work item must have either a verified native PR link or the single managed `[lisa-pr-link]` comment. Establish that comment with `node scripts/lisa-work-item.mjs backlink --ref <work_item_ref> --pr-url <url>` rather than posting it by hand — it is idempotent, and it is the same file that enforces the check, so producer and consumer cannot drift.
351
361
  8. PR Watch Loop: Drive the PR to merge via the `drive-pr-to-merge` skill — the single source of truth for clearing every blocker (auto-merge with direct-merge fallback, `BEHIND` re-sync, conflict resolution, failing-check fixes, human + bot review-comment handling with thread resolution, stale `CHANGES_REQUESTED` dismissal, and post-merge ancestry verification). `git-submit-pr` already invokes it; if you reach this step with a PR already open, invoke `drive-pr-to-merge` directly with the PR number. For a large review backlog you may fan the code-fix work out to the agent team, but `drive-pr-to-merge` owns the loop and the terminal conditions — do not re-implement them.
@@ -82,9 +82,43 @@ All GraphQL calls use:
82
82
  # or AWS has no path to the key at all — the ladder stopped at rung one, which
83
83
  # is also what left `/lisa:setup:linear` reading an OS keychain directly with
84
84
  # nowhere to migrate to. Mirrors `atlassian-access` and `notion-access`.
85
- read_linear_key() {
85
+ read_linear_key() { # $1=workspace slug; defaults to $WORKSPACE read from config
86
86
  [ -n "${LINEAR_API_KEY:-}" ] && { echo "$LINEAR_API_KEY"; return; }
87
87
 
88
+ # Per-workspace rung, matching `atlassian-access` and `notion-access` — and
89
+ # matching what `setup-linear` has always told the reader to export. This
90
+ # copy lacked it, so a key set the documented way validated during setup (the
91
+ # setup skill reads it) and was then ignored by every real Linear call, which
92
+ # silently fell through to whatever other credential it found. Nothing
93
+ # errored; the operation just ran as a different identity. The rung is also
94
+ # load-bearing headlessly: `analyze-claude-remote` reports Linear's env as
95
+ # `LINEAR_API_KEY` "or per-account `LINEAR_API_KEY_<ws-slug>`", and
96
+ # `generate-claude-remote-build-script` emits that suffixed name into cloud
97
+ # routine env templates — so a routine provisioned from those skills had no
98
+ # reachable key at all.
99
+ #
100
+ # An unset variable changes nothing: an empty workspace skips the rung
101
+ # entirely and the ladder below runs exactly as it did before.
102
+ local ws="${1:-${WORKSPACE:-}}"
103
+ if [ -n "$ws" ]; then
104
+ local slug; slug=$(echo "$ws" | tr '[:upper:]-' '[:lower:]_')
105
+ # Constrain the slug before it reaches `${!varname}`. Indirect expansion
106
+ # evaluates an array subscript, so a workspace carrying `[$(...)]` would
107
+ # RUN that command here — and `tr` above only folds case and dashes, it
108
+ # strips nothing. The slug's source is `.lisa.config.json`, which is
109
+ # repository-controlled input: exactly what this ladder already refuses to
110
+ # trust when it picks resolvers. A real workspace slug is lowercase
111
+ # alphanumerics and underscores, so anything else is not a slug we should
112
+ # be expanding — skip the rung and fall through rather than guessing.
113
+ case "$slug" in
114
+ ""|*[!a-z0-9_]*) ;;
115
+ *)
116
+ local varname="LINEAR_API_KEY_${slug}"
117
+ [ -n "${!varname:-}" ] && { echo "${!varname}"; return; }
118
+ ;;
119
+ esac
120
+ fi
121
+
88
122
  # The ladder is ordered across trusted machine-managed substrates and ends at
89
123
  # the installed package. Checkout-local paths are deliberately absent: a
90
124
  # familiar generated destination is still repository-controlled executable
@@ -132,9 +166,10 @@ linear_graphql() {
132
166
  local query="$1"
133
167
  local variables="${2:-{}}"
134
168
  local key
135
- key=$(read_linear_key) || {
136
- echo "Error: no Linear API key. Set LINEAR_API_KEY, or store it as" >&2
137
- echo "LINEAR_API_KEY in this project's secrets provider." >&2
169
+ key=$(read_linear_key "${WORKSPACE:-}") || {
170
+ echo "Error: no Linear API key. Set LINEAR_API_KEY (or the per-workspace" >&2
171
+ echo "LINEAR_API_KEY_<slug>), or store it as LINEAR_API_KEY in this" >&2
172
+ echo "project's secrets provider." >&2
138
173
  return 1
139
174
  }
140
175
  jq -n --arg query "$query" --argjson variables "$variables" \
@@ -30,14 +30,45 @@ Install the vendor-neutral AWS bootstrap into the current repository.
30
30
  `.github/workflows/copilot-setup-steps.yml`; and it always writes
31
31
  `docs/remote-agent-aws.md`.
32
32
  4. Run `bash -n scripts/remote-agent-aws-setup.sh`.
33
- 5. Never write or request the actual bootstrap bundle in repository files. Tell
33
+ 5. Confirm the operator knows which project component the profiles will carry.
34
+ The script resolves it from `LISA_AWS_PROFILE_NAMESPACE`, then the bundle's
35
+ `namespace`, then `<owner>-<repository>` from the git origin remote, and
36
+ refuses to write anything when none resolves. Set the variable explicitly
37
+ whenever the setup hook runs outside a checkout.
38
+ 6. Never write or request the actual bootstrap bundle in repository files. Tell
34
39
  the operator where to obtain it and where to put it — see **Where the bundle
35
40
  lives** below. Pass `--secret-name` when the source names it something other
36
41
  than `remote-agent-credentials`, so the generated runbook contains the exact
37
42
  retrieval command.
38
- 6. Report the external step that remains: configure that one secret in the
43
+ 7. Report the external step that remains: configure that one secret in the
39
44
  remote environment and launch a smoke session.
40
45
 
46
+ ## Profile naming and readiness
47
+
48
+ Every profile the script writes is scoped `<project>-agent-<stage>`, and the
49
+ private source profile is `<project>-agent-bootstrap`. Bare stage names state a
50
+ stage but not an owner, so on a workstation carrying more than one organisation
51
+ two bundles declaring the same stages overwrite each other in the shared
52
+ `~/.aws/config` — silently, because nothing about the surviving profile is
53
+ malformed. The name is the only place the owner can be recorded.
54
+
55
+ `default` cannot be namespaced. The script claims it only when nothing else
56
+ owns it, and otherwise stops and names the current owner;
57
+ `LISA_AWS_CLAIM_DEFAULT_PROFILE=1` takes it over deliberately.
58
+
59
+ Readiness is an account check, not a liveness check. `sts:GetCallerIdentity`
60
+ succeeding proves the credentials work, not that they reached the intended
61
+ account — a check that ignores the returned value passes on any credential that
62
+ authenticates anywhere. The script compares the returned account id against the
63
+ account named in the role ARN it configured and refuses to report ready on a
64
+ mismatch, quoting both. Do not substitute a hand-run `get-caller-identity` for
65
+ the script's own exit status.
66
+
67
+ Workstations bootstrapped under the previous unnamespaced convention keep their
68
+ old profiles. They are reported by name on every run and removed only when
69
+ `LISA_AWS_PRUNE_LEGACY_PROFILES=1` is passed, so the migration is explicit in
70
+ both directions.
71
+
41
72
  ## Where the bundle lives
42
73
 
43
74
  `LISA_AWS_BOOTSTRAP_JSON` is **just another secret**, and it belongs wherever
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
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.26.5",
3
+ "version": "4.27.1",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"