@codyswann/lisa 4.52.1 → 4.53.0

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 (80) 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 +12 -4
  4. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  5. package/package.json +4 -4
  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-repair-intake/SKILL.md +23 -4
  9. package/plugins/lisa/rules/reference/blocker-containment.md +2 -0
  10. package/plugins/lisa/scripts/blocker-edge-resolution.mjs +150 -0
  11. package/plugins/lisa/scripts/install-remote-agent-aws.mjs +14 -0
  12. package/plugins/lisa/scripts/remote-agent-aws-setup.sh +134 -0
  13. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +23 -4
  14. package/plugins/lisa-agy/plugin.json +1 -1
  15. package/plugins/lisa-agy/scripts/blocker-edge-resolution.mjs +150 -0
  16. package/plugins/lisa-agy/scripts/install-remote-agent-aws.mjs +14 -0
  17. package/plugins/lisa-agy/scripts/remote-agent-aws-setup.sh +134 -0
  18. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +23 -4
  19. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/rules/reference/blocker-containment.md +2 -0
  26. package/plugins/lisa-copilot/scripts/blocker-edge-resolution.mjs +150 -0
  27. package/plugins/lisa-copilot/scripts/install-remote-agent-aws.mjs +14 -0
  28. package/plugins/lisa-copilot/scripts/remote-agent-aws-setup.sh +134 -0
  29. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +23 -4
  30. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +2 -0
  32. package/plugins/lisa-cursor/scripts/blocker-edge-resolution.mjs +150 -0
  33. package/plugins/lisa-cursor/scripts/install-remote-agent-aws.mjs +14 -0
  34. package/plugins/lisa-cursor/scripts/remote-agent-aws-setup.sh +134 -0
  35. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +23 -4
  36. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-expo-agy/plugin.json +1 -1
  39. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  54. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  59. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-agy/plugin.json +1 -1
  64. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  69. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  74. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/src/base/rules/reference/blocker-containment.md +2 -0
  77. package/plugins/src/base/scripts/blocker-edge-resolution.mjs +150 -0
  78. package/plugins/src/base/scripts/install-remote-agent-aws.mjs +14 -0
  79. package/plugins/src/base/scripts/remote-agent-aws-setup.sh +134 -0
  80. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +23 -4
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * What to do with a dependency edge once its blocker has been judged.
4
+ *
5
+ * ## The gap this fills
6
+ *
7
+ * `blocker-containment` decides ONE question — given an edge, is it satisfied —
8
+ * and says so in its own words: it "does not decide when a dependency edge
9
+ * should exist, dissolve an edge once satisfied, or govern what a surface does
10
+ * after clearing". Nothing else claimed that job. So a repair pass that ruled a
11
+ * dependency satisfied moved the STATE and left the EDGE, recording the ruling
12
+ * as a comment; the next reader parses relations, not prose, and blocks again
13
+ * (CodySwannGT/lisa#3472). The durable artifact of "this is satisfied" had no
14
+ * mechanical effect.
15
+ *
16
+ * Measured on this repository while writing this: of 11 open `status:blocked`
17
+ * items carrying a parsable `Blocked by #N`, 5 were held behind blockers that
18
+ * were all closed.
19
+ *
20
+ * ## Why closure is not the test, and neither is "it is finished"
21
+ *
22
+ * Two different mistakes converge on the same wrong answer.
23
+ *
24
+ * The first is testing a state NAME. That half is already fixed: the triage
25
+ * gate now clear-checks by containment, because a promotion workflow types its
26
+ * merged-and-deployed rungs `started`, so `Done` strands every dependent behind
27
+ * work already merged where the dependent needs it.
28
+ *
29
+ * The second is treating CLOSED as satisfied. An `is blocked by` edge is a
30
+ * DEVELOPMENT dependency: it is satisfied when the blocker's code is on the
31
+ * branch this work builds from. A blocker can be closed with no code — and a
32
+ * blocker closed `NOT_PLANNED` is the sharp case, because it means the thing
33
+ * depended on will never be built. Dissolving that edge silently converts "what
34
+ * we needed is cancelled" into "we are unblocked", which is the same wrong-write
35
+ * family as CodySwannGT/lisa#3479, where a reason-blind repair would have
36
+ * stamped a production terminal on abandoned work. So an abandoned blocker
37
+ * ESCALATES to a human rather than dissolving or silently holding forever:
38
+ * somebody has to decide whether the dependent still makes sense.
39
+ *
40
+ * ## What this module does not decide
41
+ *
42
+ * How an edge is dissolved on a given tracker. That is vendor work with its own
43
+ * hazard: on Linear, `issueRelationCreate` CONVERTS an existing relation rather
44
+ * than adding one (CodySwannGT/lisa#3605), so a writer that "updates" an edge
45
+ * can destroy a different one. This module returns a decision; the vendor
46
+ * surface performs it and reads it back.
47
+ * @module blocker-edge-resolution
48
+ */
49
+
50
+ /** Every action a surface may take on a judged edge. */
51
+ export const EDGE_ACTIONS = Object.freeze(["dissolve", "keep", "escalate"]);
52
+
53
+ /** GitHub's closure reason for work that will deliberately not be done. */
54
+ const NOT_PLANNED = "not_planned";
55
+
56
+ /**
57
+ * Decide what happens to one `is blocked by` edge.
58
+ *
59
+ * `contained` is the containment verdict — the blocker's merged code is an
60
+ * ancestor of the branch this work builds from — and it is deliberately the
61
+ * ONLY positive signal. Everything else here exists to keep a non-containment
62
+ * signal from being mistaken for one.
63
+ * @param {{
64
+ * contained?: boolean | null,
65
+ * blockerState?: string,
66
+ * blockerReason?: string,
67
+ * }} input The containment verdict and the blocker's tracker state.
68
+ * @returns {{action: string, reason: string}} What to do, and why.
69
+ */
70
+ export function resolveBlockerEdge(input = {}) {
71
+ const state = String(input.blockerState ?? "")
72
+ .trim()
73
+ .toLowerCase();
74
+ const reason = String(input.blockerReason ?? "")
75
+ .trim()
76
+ .toLowerCase();
77
+
78
+ // Checked BEFORE containment, because an abandoned blocker whose branch
79
+ // happens to contain some earlier commit of its own is still abandoned. The
80
+ // question a human has to answer is whether the dependent still makes sense,
81
+ // and a containment true would silently answer it for them.
82
+ if (state === "closed" && reason === NOT_PLANNED) {
83
+ return {
84
+ action: "escalate",
85
+ reason:
86
+ "the blocker was closed as NOT PLANNED, so the work this item depends on will never be " +
87
+ "delivered. Dissolving the edge would read as 'unblocked' when nothing was built, and " +
88
+ "holding it forever hides a decision somebody has to make: does this item still make sense " +
89
+ "without its blocker? That is a human call, not a sweep's.",
90
+ };
91
+ }
92
+
93
+ if (input.contained === true) {
94
+ return {
95
+ action: "dissolve",
96
+ reason:
97
+ "the blocker's merged code is present on the branch this item builds from, so the " +
98
+ "development dependency is satisfied. Remove the edge in the same write that records the " +
99
+ "ruling, and read it back — a state move plus a comment is a claim about a clearance, not " +
100
+ "a clearance, and the next reader parses relations rather than prose.",
101
+ };
102
+ }
103
+
104
+ if (input.contained === null || input.contained === undefined) {
105
+ return {
106
+ action: "escalate",
107
+ reason:
108
+ "containment could not be computed for this blocker, so whether the dependency is satisfied " +
109
+ "is unknown. An unknown is not a clearance: dissolving here would be a guess in the " +
110
+ "direction that releases work, which is the direction nothing downstream re-checks.",
111
+ };
112
+ }
113
+
114
+ return {
115
+ action: "keep",
116
+ reason:
117
+ state === "closed"
118
+ ? "the blocker is closed but its code is not on the branch this item builds from. Closing an " +
119
+ "item is not the same as shipping it, and an `is blocked by` edge is a development " +
120
+ "dependency, not a bookkeeping one."
121
+ : "the blocker's code is not yet on the branch this item builds from, so the dependency " +
122
+ "still holds.",
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Decide a whole set of edges, and say whether the item can proceed.
128
+ *
129
+ * An item proceeds only when every edge dissolves. One escalation holds the
130
+ * item — reporting it, rather than proceeding on the majority verdict, is what
131
+ * keeps a single unanswerable edge from being rounded away.
132
+ * @param {readonly object[]} edges Per-edge inputs, in the item's own order.
133
+ * @returns {{
134
+ * proceed: boolean,
135
+ * decisions: {action: string, reason: string}[],
136
+ * escalations: number,
137
+ * }} The per-edge decisions and whether the item is free to move.
138
+ */
139
+ export function resolveBlockerEdges(edges = []) {
140
+ const decisions = (edges ?? []).map(edge => ({
141
+ ...edge,
142
+ ...resolveBlockerEdge(edge),
143
+ }));
144
+ return {
145
+ proceed:
146
+ decisions.length > 0 && decisions.every(one => one.action === "dissolve"),
147
+ decisions,
148
+ escalations: decisions.filter(one => one.action === "escalate").length,
149
+ };
150
+ }
@@ -342,6 +342,20 @@ so rather than repointing every unqualified \`aws\` command. Pass
342
342
  Production and shared profiles are observer-only. Production repair remains a
343
343
  human-driven local-workstation operation.
344
344
 
345
+ Observer-only names what those profiles may READ, not only what they may not
346
+ write. Prove that half — a role that can read nothing satisfies every deny-side
347
+ assertion ever made about it:
348
+
349
+ \`\`\`bash
350
+ LISA_AWS_VERIFY_OBSERVER_READS=1 bash scripts/remote-agent-aws-setup.sh
351
+ \`\`\`
352
+
353
+ It runs one representative read per observability surface against every observer
354
+ profile — deployment stacks, compute inventory, alarm state, delivery pipelines,
355
+ builds, functions, logs, http endpoints, queues, workflows — and refuses to
356
+ report ready if any is denied, naming all of them at once. Every action is
357
+ read-only, so widening the role's policy to satisfy it grants no write anywhere.
358
+
345
359
  After the infrastructure pipeline deploys the remote-agent IAM stacks, retrieve
346
360
  the complete bundle from the shared account. Do not extract or distribute its
347
361
  individual access-key fields:
@@ -27,6 +27,11 @@
27
27
  # LISA_AWS_VERIFY_ALL_PROFILES
28
28
  # 1 to prove every stage's account, not just the
29
29
  # default's.
30
+ # LISA_AWS_VERIFY_OBSERVER_READS
31
+ # 1 to prove the ALLOW side of every observer-only
32
+ # profile: one representative read per observability
33
+ # surface must succeed. A role that can read nothing
34
+ # fails here instead of at verification time.
30
35
  # LISA_AWS_CLAIM_DEFAULT_PROFILE
31
36
  # 1 to take over a `[default]` this project does not
32
37
  # already own. Off by default: silently repointing
@@ -337,6 +342,120 @@ assert_profile_account() {
337
342
  "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
343
  }
339
344
 
345
+ # What an observer-only role must be able to READ.
346
+ #
347
+ # "Observer-only" was defined entirely by what it must not do, and the proof
348
+ # prescribed for it tested only the deny side. A role that can read NOTHING
349
+ # passes a deny-side check perfectly, so the read surface of a headless verifier
350
+ # was being discovered one `AccessDenied` at a time, mid-verification, and
351
+ # granted one ticket at a time. Three consecutive incidents in one consumer had
352
+ # that exact shape: instance discovery, alarm reads, and pipeline definition
353
+ # reads — none of those services appeared in the shipped starter policy at all,
354
+ # because that policy enumerated the services the stack happened to deploy
355
+ # rather than answering "what must a verifier be able to read?".
356
+ #
357
+ # So the surfaces below are derived from the ROLE'S PURPOSE, not from any one
358
+ # stack's topology. Each row is a surface, the read that represents it, and the
359
+ # question an observer cannot answer without it. Every action is read-only:
360
+ # widening the allow side generously is far cheaper than rediscovering it by
361
+ # denial, and it grants no write anywhere.
362
+ #
363
+ # Each probe takes no resource name, which is what lets it run against a fresh
364
+ # account with nothing deployed — these ask an IAM question, not an inventory
365
+ # one, and an empty result is a pass. The limitation that buys: a surface whose
366
+ # list action is granted while its detail action is not still passes here. That
367
+ # is exactly incident three, where the role could list pipeline executions but
368
+ # could not read the pipeline definition, so the table names both halves in the
369
+ # question column and the docs state the requirement; the probe proves the
370
+ # surface is reachable, not that every action on it is granted.
371
+ OBSERVER_READ_SURFACES=$(
372
+ cat <<'SURFACES'
373
+ deployment stacks cloudformation describe-stacks what is deployed, and did the last change apply or roll back
374
+ compute inventory ec2 describe-instances what is actually running, and is it healthy
375
+ alarm state cloudwatch describe-alarms is anything alarming right now, and since when
376
+ delivery pipelines codepipeline list-pipelines what the pipeline does and where this change stopped, definition as well as executions
377
+ builds codebuild list-projects did the build run, and what did it say
378
+ functions lambda list-functions is the handler deployed, and at which version
379
+ logs logs describe-log-groups what it printed when it failed
380
+ http endpoints apigateway get-rest-apis is the endpoint published and reachable
381
+ queues sqs list-queues is work backing up or dead-lettering
382
+ workflows stepfunctions list-state-machines did the orchestrated run complete or stall
383
+ SURFACES
384
+ )
385
+
386
+ # Stages whose profile is observer-only, per the contract's own words.
387
+ #
388
+ # The bootstrap bundle carries no observer field today, so this reads one if a
389
+ # bundle supplies it and otherwise falls back to the definition the contract
390
+ # already states in prose: dev and staging may repair, production and shared are
391
+ # observer-only. Deriving it is what makes "observer" exist anywhere other than
392
+ # a sentence in a document — until now nothing in the data or the code could
393
+ # answer which profiles the word applied to.
394
+ is_observer_stage() {
395
+ local stage declared
396
+ stage="$1"
397
+ # `has` rather than `.observer // empty`: jq's alternative operator treats
398
+ # `false` as absent, so a stage that explicitly declared itself NOT an
399
+ # observer fell through to the name-based default and was probed anyway —
400
+ # the declaration was accepted only when it agreed with the guess.
401
+ declared="$(printf '%s' "$profiles_json" | jq -r --arg s "$stage" '
402
+ .[$s] | if type == "object" and has("observer") then (.observer | tostring) else empty end
403
+ ')"
404
+ if [ -n "$declared" ]; then
405
+ [ "$declared" = "true" ]
406
+ return
407
+ fi
408
+ case "$stage" in
409
+ production | shared) return 0 ;;
410
+ *) return 1 ;;
411
+ esac
412
+ }
413
+
414
+ # Prove the ALLOW side of an observer profile.
415
+ #
416
+ # Reports every surface it could not read rather than stopping at the first,
417
+ # because an operator repairing a policy wants the whole list — the same reason
418
+ # the toolchain planner reports every unresolvable tool at once. One ticket, not
419
+ # three in a row.
420
+ #
421
+ # Denied and unreachable are kept apart. A read that fails because the service
422
+ # could not be reached says nothing about the policy, and folding it into
423
+ # "denied" would send someone to widen a policy that was already correct; a pass
424
+ # is never claimed from either.
425
+ assert_observer_reads() {
426
+ local profile stage denied unreachable surface probe question output status
427
+ profile="$1"
428
+ stage="$2"
429
+ denied=""
430
+ unreachable=""
431
+
432
+ while IFS="$(printf '\t')" read -r surface probe question; do
433
+ [ -n "$surface" ] || continue
434
+ set +e
435
+ output="$(AWS_PAGER="" aws --profile "$profile" $probe 2>&1)"
436
+ status=$?
437
+ set -e
438
+ [ "$status" -eq 0 ] && continue
439
+ case "$output" in
440
+ *AccessDenied* | *UnauthorizedOperation* | *not\ authorized*)
441
+ denied="${denied}
442
+ ${surface} — \`aws ${probe}\` was denied, so this role cannot answer: ${question}"
443
+ ;;
444
+ *)
445
+ unreachable="${unreachable}
446
+ ${surface} — \`aws ${probe}\` did not return an authorization answer at all: ${output}"
447
+ ;;
448
+ esac
449
+ done <<<"$OBSERVER_READ_SURFACES"
450
+
451
+ if [ -n "$unreachable" ]; then
452
+ fail "profile ${profile} (stage ${stage}) could not be checked against the observer read surface, so nothing about its permissions was proved:${unreachable}"
453
+ fi
454
+ if [ -n "$denied" ]; then
455
+ fail "profile ${profile} (stage ${stage}) is observer-only but cannot read the surfaces an observer exists to read. Widen the role's read-only policy to cover them; every action below is read-only and none grants a write:${denied}"
456
+ fi
457
+ }
458
+
340
459
  # Who currently owns `[default]`.
341
460
  #
342
461
  # `default` is the one name that cannot be namespaced, so it is the one place
@@ -530,6 +649,21 @@ if [ "${LISA_AWS_SKIP_VERIFY:-0}" != "1" ]; then
530
649
  else
531
650
  assert_profile_account "$default_profile" "$default_expected_account" "$default_stage"
532
651
  fi
652
+ # Ordered after the account assertions on purpose, and it is the ordering that
653
+ # makes either half mean anything. `AccessDenied` is what an unassumed role, a
654
+ # misconfigured profile and a correctly-scoped observer all return, so a
655
+ # boundary check run before identity is bound passes on a credential that
656
+ # reached the wrong account entirely — the same class of vacuous pass this
657
+ # script already refuses for `sts:GetCallerIdentity`. Binding the profile to
658
+ # its expected account first is what turns the reads below into a statement
659
+ # about THIS role.
660
+ if [ "${LISA_AWS_VERIFY_OBSERVER_READS:-0}" = "1" ]; then
661
+ while IFS= read -r stage; do
662
+ [ -n "$stage" ] || continue
663
+ is_observer_stage "$stage" || continue
664
+ assert_observer_reads "${PROFILE_NAMESPACE}-agent-${stage}" "$stage"
665
+ done < <(printf '%s' "$profiles_json" | jq -r 'keys[]')
666
+ fi
533
667
  fi
534
668
 
535
669
  if [ -n "$legacy_names" ]; then
@@ -566,10 +566,29 @@ condition observed, filed under the wrong heading, and overridden.
566
566
  **deployed / runtime verification failure**, by an **ambiguity**, or by more than one at once.
567
567
  Re-check **every** class present — do not stop at "no `is blocked by` links, therefore nothing
568
568
  to do." A self-block has zero dependencies by definition, yet is fully re-checkable.
569
- 2. **Dependency cleared** — if every parsed `is blocked by` dependency is **cleared** → move
570
- `blocked → claimed`, then run the same agent-dispatch + post-agent `claimed → done` sequence as
571
- the stalled-`claimed` path above (one-cycle recovery). If the agent re-blocks, move back to
572
- `blocked` — a valid outcome.
569
+ 2. **Dependency cleared** — decide each parsed `is blocked by` edge with
570
+ `scripts/blocker-edge-resolution.mjs`, feeding it the `blocker-containment` verdict plus the
571
+ blocker's state and closure reason. It returns `dissolve` / `keep` / `escalate` per edge.
572
+ **Dissolve the edge in the SAME write that records the ruling, then read the relation back and
573
+ confirm it is gone.** Only when every edge dissolves → move `blocked → claimed`, then run the
574
+ same agent-dispatch + post-agent `claimed → done` sequence as the stalled-`claimed` path above
575
+ (one-cycle recovery). If the agent re-blocks, move back to `blocked` — a valid outcome.
576
+
577
+ **Moving the state without removing the edge is not a clearance, it is a claim about one.** That
578
+ was the defect (CodySwannGT/lisa#3472): the pass moved the state and left the relation, recording
579
+ the ruling as a comment, and the next reader parses relations rather than prose — so the block
580
+ re-formed every cycle and a human had to break the loop. Reading the relation back is what makes
581
+ "I removed the link" distinguishable from "I meant to".
582
+
583
+ **An `escalate` verdict holds the item and reports it — never dissolve on it.** The sharp case is
584
+ a blocker closed `NOT PLANNED`: the work depended on will never be delivered, so dissolving reads
585
+ as "unblocked" when nothing was built, and holding silently hides a decision only a human can
586
+ make. Treating closure as satisfaction is the same reason-blind wrong write as
587
+ CodySwannGT/lisa#3479. A single escalation among otherwise-dissolving edges holds the item.
588
+
589
+ **On Linear, dissolve with a relation DELETE.** Do not "update" an edge:
590
+ `issueRelationCreate` CONVERTS an existing relation rather than adding one
591
+ (CodySwannGT/lisa#3605), so a writer reaching for it can destroy a different edge.
573
592
  3. **Validation / quality-gate self-block re-check** — if the block reason is a pre-flight
574
593
  `verify`/`validate` gate failure (its `[lisa-*]` block note carries a gate marker + a "Missing
575
594
  requirements" list and there is **no** open `is blocked by` dependency), re-run the **same gate**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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.52.1",
3
+ "version": "4.53.0",
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-phaser",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.52.1",
3
+ "version": "4.53.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"