@codyswann/lisa 4.87.0 → 4.87.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 (121) hide show
  1. package/all/copy-overwrite/scripts/lib/completion-proof-content.mjs +20 -6
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +10 -8
  3. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  5. package/dist/core/lisa-owned-hash-ledger.js +8 -0
  6. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  7. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  8. package/dist/core/upstream-evidence-manifest.js +10 -10
  9. package/package.json +4 -4
  10. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +93 -29
  13. package/plugins/lisa/.codex-plugin/skills/lisa-pull-request-review/SKILL.md +8 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +2 -2
  15. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  16. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  17. package/plugins/lisa/hooks/block-blind-automerge.sh +10 -8
  18. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +93 -29
  19. package/plugins/lisa/skills/lisa-pull-request-review/SKILL.md +8 -0
  20. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +2 -2
  21. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  22. package/plugins/lisa/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  23. package/plugins/lisa-agy/hooks/block-blind-automerge.sh +10 -8
  24. package/plugins/lisa-agy/plugin.json +1 -1
  25. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +93 -29
  26. package/plugins/lisa-agy/skills/lisa-pull-request-review/SKILL.md +8 -0
  27. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +2 -2
  28. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  29. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  30. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  33. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  37. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +10 -8
  39. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +93 -29
  40. package/plugins/lisa-copilot/skills/lisa-pull-request-review/SKILL.md +8 -0
  41. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +2 -2
  42. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  43. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  44. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +10 -8
  46. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +93 -29
  47. package/plugins/lisa-cursor/skills/lisa-pull-request-review/SKILL.md +8 -0
  48. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +2 -2
  49. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  50. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  51. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  53. package/plugins/lisa-devin/hooks/block-blind-automerge.sh +10 -8
  54. package/plugins/lisa-devin/skills/lisa-drive-pr-to-merge/SKILL.md +93 -29
  55. package/plugins/lisa-devin/skills/lisa-pull-request-review/SKILL.md +8 -0
  56. package/plugins/lisa-devin/skills/lisa-secrets-access/SKILL.md +2 -2
  57. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  58. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  59. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-expo-agy/plugin.json +1 -1
  62. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  66. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  69. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  73. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  76. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  80. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  83. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  87. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  89. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  90. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  94. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  96. package/plugins/lisa-rails-agy/plugin.json +1 -1
  97. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  101. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  103. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  104. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  108. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  111. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  115. package/plugins/src/base/hooks/block-blind-automerge.sh +10 -8
  116. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +93 -29
  117. package/plugins/src/base/skills/lisa-pull-request-review/SKILL.md +8 -0
  118. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +2 -2
  119. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  120. package/plugins/src/base/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  121. package/scripts/lisa-ruleset-payload.mjs +17 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.87.0",
3
+ "version": "4.87.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.87.0",
3
+ "version": "4.87.2",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -393,15 +393,16 @@ declared, arm normally (the scope rule above). Where one IS declared, absent a
393
393
  description that positively states a review ran, treat the context as vacuous.
394
394
  Fail closed: absence of evidence is not evidence of a review.
395
395
 
396
- #### Vacuous and transient are different properties
396
+ #### Vacuous, transient or standing: the response is the same
397
397
 
398
- A vacuous context is not automatically something to wait out, and this is the
399
- distinction a naive wait-and-arm gets wrong — it waits forever on a repository
400
- where the state never clears. **Only the rate limit has both properties.**
398
+ A vacuous context is never something to wait out. Waiting looks like progress
399
+ but it is a stall, and on a repository where the state never clears it waits
400
+ forever. Some vacuities clear on their own and some do not. **That changes what
401
+ the report says, not what you do:**
401
402
 
402
403
  | description | vacuous? | clears on its own? |
403
404
  | --- | --- | --- |
404
- | `Review rate limited` | yes | **yes** — a throughput window |
405
+ | `Review rate limited` (and decorated forms such as `Review rate limited (retry in 12m)`) | yes | eventually — a throughput window of unknown length |
405
406
  | `Review skipped: manual review required for this OSS repository` | yes | **no** — a standing vendor policy for public repositories |
406
407
  | `Review skipped: N files exceed the limit of M` | yes | **no** — not without splitting the pull request |
407
408
  | `Review skipped` (bare, no stated reason) | yes | **unknown — treat as standing** |
@@ -410,17 +411,33 @@ A bare skip carries no machine-readable reason, so nothing distinguishes it from
410
411
  the standing forms. Requiring a stated reason before treating a skip as vacuous
411
412
  is how the commonest form gets missed.
412
413
 
413
- - **Transient (the rate limit, and only the rate limit)** — do not arm yet. Wait
414
- the window out and re-request the review **at most once**, then re-classify.
415
- If it now proves work, arm. If it does not, stop and treat it as standing.
416
- **Never loop**: a retry loop re-triggers the very limit it is escaping, and
417
- measured on #3220 a single explicit re-request in this repository was
418
- acknowledged, went `Review in progress`, and settled back at
419
- `Review rate limited` two seconds later having reviewed nothing — minting a
420
- second hollow green. One attempt, then report.
421
- - **Standing** — do not wait at all, and do not re-request. There is no window
422
- to wait out, so waiting produces a stall that reads as progress. Go straight
423
- to the watch loop and let step (d) adjudicate the merge in the open.
414
+ **Every row, the rate limit included: do not wait, and do not re-request.** Do
415
+ not arm. Go straight to the watch loop, run the local adversarial review over the
416
+ PR diff at the current head **now**, post it with the substitute marker (step
417
+ (d), "In case 3, substitute"), and keep driving. Fix what it finds, substitute
418
+ again on each new head, and let step (d) merge when the vacuous context is the
419
+ sole remaining gate.
420
+
421
+ The rate limit used to get a wait-the-window-out-and-re-request-once exception.
422
+ That exception is removed, for three reasons:
423
+
424
+ - **The window is not short.** Agents waiting it out stalled for an hour or more
425
+ per pull request, across a whole portfolio of repositories, and nothing about
426
+ the wait made the merge safer than a substitute review would.
427
+ - **Re-requesting does not work under a throttle.** Measured on #3220, a single
428
+ explicit re-request was acknowledged, went `Review in progress`, and settled
429
+ back at `Review rate limited` two seconds later having reviewed nothing. That
430
+ minted a second hollow green.
431
+ - **Re-requesting destroys evidence.** It overwrites the existing status (see
432
+ "Never re-request a review to 'refresh' it" in step (d)), so the exception
433
+ contradicted a rule this file states with no exceptions.
434
+
435
+ If the reviewer later reviews on its own, nothing special happens. Its threads
436
+ are review threads like any other, and step (d) handles them. A `CHANGES_REQUESTED`
437
+ it leaves is a real blocker (step (e)), and the substitute never overrides it.
438
+ If it now proves work, arm, per the table above. That is a re-classification
439
+ the watch loop makes on what it reads anyway. It is never a reason to wait or
440
+ re-request.
424
441
 
425
442
  #### A latch somebody else armed
426
443
 
@@ -644,17 +661,24 @@ step (e) first — which discriminates a live objection from a stranded one by
644
661
  unresolved thread count, not by `reviewDecision` — and arm afterwards.
645
662
 
646
663
  **Two ruleset settings decide whether a standing verdict ever clears itself, and
647
- in this repository both say no.** Verify them for the repo you are in rather
648
- than assuming, with `gh api repos/<owner>/<repo>/rulesets`:
649
-
650
- - `dismiss_stale_reviews_on_push: false` — the review stays attached to the
651
- commit it was made on, so **pushing the fix never clears it**. The intuition
652
- that a new head resets the verdict is wrong here.
664
+ neither one clears a change request.** Verify them for the repo you are in
665
+ rather than assuming, with `gh api repos/<owner>/<repo>/rules/branches/<base>`
666
+ (the rules in force on the base branch, with their parameters; the
667
+ `gh api repos/<owner>/<repo>/rulesets` list carries no rule parameters):
668
+
669
+ - `dismiss_stale_reviews_on_push` dismisses stale **approvals** only. The `base`
670
+ ruleset Lisa generates defaults it to `true` when
671
+ `policy.protect.dismiss_stale_reviews` is undeclared, and keeps a declared
672
+ value (CodySwannGT/lisa#4574), but a
673
+ `CHANGES_REQUESTED` review made on an older head stays in force after a push
674
+ under either setting (measured on a probe repository on 2026-11-01: the push
675
+ changed the diff and `reviewDecision` stayed `CHANGES_REQUESTED`), so
676
+ **pushing the fix never clears it**.
653
677
  - `required_approving_review_count: 0` — no approval is required to merge, so a
654
678
  standing `CHANGES_REQUESTED` is not holding a slot for a review that would
655
679
  otherwise arrive. Nothing is scheduled to clear it.
656
680
 
657
- Together those mean the block is **indefinite, not slow**. Waiting is not a
681
+ Where both hold, the block is **indefinite, not slow**. Waiting is not a
658
682
  strategy; something has to act.
659
683
 
660
684
  **Unresolved threads block independently of the verdict.** This repository sets
@@ -1048,10 +1072,11 @@ configured reviewed-when phrase says what "did work" looks like for this
1048
1072
  project.
1049
1073
 
1050
1074
  Prove the signal from the commit status API as above; a merely pending or queued
1051
- check is not proof of anything and must keep polling. **A transient vacuity is not adjudicated here until the arm gate's
1052
- single wait-and-re-request has been spent** (section 1): a rate limit has a
1053
- window, and merging past one you never waited out is a choice you did not have
1054
- to make. A standing vacuity has no window, so it arrives here immediately.
1075
+ check is not proof of anything and must keep polling. **A settled vacuity, the
1076
+ rate limit included, arrives here immediately.** There is no wait to spend first
1077
+ and no re-request to try: a rate limit's window is of unknown length, and
1078
+ waiting it out stalls the pull request without reading a line of the diff. The
1079
+ substitute is what makes the merge safe, not the passage of time.
1055
1080
 
1056
1081
  Once the signal is explicit, merge directly with `gh pr merge <pr>
1057
1082
  --<merge_method>` (pass `--admin` only if branch protection lists the vacuous
@@ -1172,6 +1197,34 @@ Deleting the fallback outright would over-correct: it is the only remedy a lane
1172
1197
  without dismissal permission has, and that permission is not universal. This
1173
1198
  narrows it to the case that costs nothing.
1174
1199
 
1200
+ **A stale bot verdict under a rate limit: substitute, then stop and report.**
1201
+ Under a rate limit or an exhausted review allowance, a bot's old
1202
+ `CHANGES_REQUESTED` on an outdated head can end up as the only thing blocking
1203
+ the merge. The bot cannot post a fresh review to replace it, so the verdict
1204
+ will not clear by itself while the throttle lasts. This was measured on a
1205
+ caller repo in the portfolio: every check green, zero unresolved threads, and
1206
+ the bot's verdict from an earlier head as the only blocker. In that case:
1207
+
1208
+ 1. **Do not wait for the window to reset, and do not re-request.** Waiting
1209
+ leaves the pull request wedged for an hour or more. A re-request under the
1210
+ throttle settles hollow and can take away the bot's standing review (see
1211
+ above).
1212
+ 2. Post the local adversarial review substitute for the **current** head, as
1213
+ step (d) case 3 describes, if one is not already there.
1214
+ 3. **Then stop** with `blocked:stale-bot-verdict` (section 4). Report exactly
1215
+ that blocker: the bot reviewer, the review id, the outdated head it reviewed
1216
+ and the current head, the unresolved thread count, and that the substitute
1217
+ is posted for the current head. Name the decision that is needed: a human
1218
+ with dismissal rights dismisses that review, or decides it must stand.
1219
+
1220
+ Do not dismiss it on the strength of your own substitute. Here the bot's review
1221
+ is the only third-party review on the pull request. Dismissing it because of a
1222
+ review this run wrote would make the agent both author and judge of the change.
1223
+ That is the escalation the bullet above requires, reached right away instead of
1224
+ after a wait. A stranded verdict that is **not** the only substantive review, or
1225
+ one left by a reviewer that is not throttled, is still handled by the
1226
+ dismissal rule above.
1227
+
1175
1228
  **Unverified, and stated as such rather than asserted:** that a bot's re-review
1176
1229
  REPLACES its predecessor in the effective set is inferred from the observed
1177
1230
  consequence, not measured against the vendor. It has not been tested here, and
@@ -1455,7 +1508,7 @@ Loop until one of:
1455
1508
  the mode forbids acting on them: `on_blocker=report`). The latch is
1456
1509
  deliberately not armed, so the PR sits OPEN rather than merging on a review
1457
1510
  nothing performed. Report the PR URL, the review context's settled
1458
- description, whether it is transient or standing, and which sole-gate
1511
+ description, whether it clears on its own (a rate limit) or is standing, and which sole-gate
1459
1512
  condition failed. This is an outcome that needs an operator, stated in one
1460
1513
  line a non-technical reader can act on — for example: *"PR #123 is ready to
1461
1514
  merge but nobody reviewed it: the review tool says it skips public
@@ -1485,6 +1538,16 @@ Loop until one of:
1485
1538
  — but a review asked for changes and that is still on record, so it will sit
1486
1539
  there forever. Someone needs to clear that review or resolve the open
1487
1540
  comments."* Never report this PR as merging.
1541
+ - **`blocked:stale-bot-verdict`** — the only thing blocking the merge is a
1542
+ bot's `CHANGES_REQUESTED` on an outdated head, the bot is rate limited or out
1543
+ of review allowance so it cannot replace that verdict, and the local
1544
+ adversarial review substitute is posted for the current head (step (e)).
1545
+ Report the reviewer, the review id, both heads, the unresolved thread count,
1546
+ and the dismissal decision a human needs to make. For example: *"PR #123 is
1547
+ ready except for an old review from the review bot on an earlier version. The
1548
+ bot is rate limited and cannot re-review, and I posted my own review of the
1549
+ current version. Someone with dismissal rights needs to dismiss review 456,
1550
+ or decide it should stand."* Do not wait for the bot's window to reset.
1488
1551
  - **`CLOSED`** → report (PR was closed without merge).
1489
1552
  - **Hard block needing a human**: an unresolvable conflict, a failing check that
1490
1553
  needs design input, or genuine unresolved human objection (not a bot gate). Stop
@@ -1593,7 +1656,8 @@ declare none, one, or several, and each shows its own evidence.
1593
1656
 
1594
1657
  Quote the description that was actually read. A report that says "rate limited"
1595
1658
  on a policy skip is a wrong record, and the two call for different operator
1596
- actions: one clears itself, the other never will.
1659
+ actions: one clears itself, the other never will. Neither changes what this
1660
+ skill does in the meantime: it substitutes and keeps driving in both cases.
1597
1661
 
1598
1662
  `NOT REVIEWED` is reporting, and on a merged PR it is never a terminal state of
1599
1663
  its own: it does not turn a merged PR into a blocked one. What it does gate is
@@ -75,6 +75,14 @@ node scripts/check-skipped-required-checks.mjs --pr=<pr> --json
75
75
  It reports and never fails — a hollow check is often a vendor spending cap, not
76
76
  a repository defect, and it is not this skill's call to block on one.
77
77
 
78
+ **Do not wait for a rate limit to reset, and do not re-request the review.** A
79
+ `Review rate limited` status, including decorated forms such as
80
+ `Review rate limited (retry in 12m)`, is handled like every other vacuous
81
+ review. Report it here. `lisa-drive-pr-to-merge` then posts a local adversarial
82
+ review substitute for the current head right away and keeps driving. Waiting
83
+ out the window stalls the pull request for an hour or more and reads no code.
84
+ Re-requesting overwrites the status and can destroy a real review.
85
+
78
86
  Carry the finding into your Step 4 report. **Do not write "no unresolved review
79
87
  threads" on its own** — it is true of an unreviewed PR too. Say which you
80
88
  observed:
@@ -275,7 +275,7 @@ No create, no update, no rotate. Writing secrets or their notes requires an auth
275
275
 
276
276
  The two writers are siblings, not modes: `rotate-secret.mjs` replaces a value **at its source**, and `sync-secret-to-ci.mjs` copies one **into a second store** without touching the source. Each needs an authority the resolver must not hold, so each is its own program with its own declaration list.
277
277
 
278
- A third, narrow writer lives outside this skill: the session manager's `store-token` (`lisa-session-manager/scripts/token-store.mjs`) stores the Telegram bot token as `TELEGRAM_BOT_TOKEN_<NAME>` through `providers.mjs` `createSecret` (a new record) or `writeSecret` (`--replace`), then reads it back through the ordinary provider view and compares hashes before deleting the plaintext file it replaces. It writes that one name only, and only when an operator runs it. `createSecret` is Bitwarden-only; on any other provider the operator creates the secret and `store-token` verifies it. Like `bws secret edit`, `bws secret create` (2.1.0) takes the value as a positional argument and its help offers no stdin or file option, so the value is in argv for the life of that one call: the same documented exception, confined the same way (no shell, `--output none`, and a failure message that never repeats the command line).
278
+ A third, narrow writer lives outside this skill: the session manager's `store-token` (`lisa-session-manager/scripts/token-store.mjs`) stores the Telegram bot token as `TELEGRAM_BOT_TOKEN_<NAME>` through `providers.mjs` `createSecret` (a new record) or `writeSecret` (`--replace`), then reads it back through the ordinary provider view and compares hashes before deleting the plaintext file it replaces. It writes that one name only, and only when an operator runs it. `createSecret` is Bitwarden-only; on any other provider the operator creates the secret and `store-token` verifies it. Like `bws secret edit`, `bws secret create` (2.1.0) takes the value as a positional argument and its help offers no stdin or file option, so the value is in argv for the life of that one call: the second documented exception in Rule 4, held to the same limits (no shell, the CLI's output never shown, here through `--output none`, and a failure message that never repeats the command line).
279
279
 
280
280
  ## Rotating credentials
281
281
 
@@ -429,6 +429,6 @@ Cache **in-process only**. Never write a resolved value to disk except through t
429
429
  1. **Never read a keychain, `.env`, or provider CLI outside this skill.** One chokepoint is what makes the single-store rule enforceable.
430
430
  2. **Never log a secret value.** Print a length or a hash prefix when proving identity.
431
431
  3. **Never write a resolved value to disk** except through the materialization contract, on a surface whose capabilities permit it.
432
- 4. **Never pass a secret as a command-line argument.** The one documented exception is the Bitwarden rotation write, whose CLI exposes no stdin path; it is confined to that single operation and noted in the code.
432
+ 4. **Never pass a secret as a command-line argument.** There are two documented exceptions, both Bitwarden writes whose CLI (2.1.0) takes the value only on the command line: `bws secret edit --value` (the rotation write, `writeSecret`) and `bws secret create` (`store-token`'s `createSecret`). Each is confined to that single operation and noted in the code, and both are held to the same limits: no shell, the CLI's output (which echoes the record) is never shown (`create` passes `--output none`; `edit`'s output is captured and discarded), and a failure message never repeats the command line.
433
433
  5. **Verify a credential when it is stored, not when it is first used.** An unverified credential is indistinguishable from a broken one, and the gap between the two is measured in weeks.
434
434
  6. **Treat a mismatch as stop-and-ask.** If a value differs between two places, they are two live credentials — not a stale copy to be discarded. Deleting the one you cannot verify leaves a working credential that no record accounts for.
@@ -170,8 +170,8 @@ Per cycle
170
170
  [--sender <name>] STDIN: question
171
171
  --kind: worker-question (default) | repo-route | exhausted | decision.
172
172
  No message id with no channel. A re-ask keeps the item's marker
173
- fields (markerType, marker, markedAt); a queued answer survives only a
174
- re-ping of the same text.
173
+ fields (markerType, marker); markedAt and a queued answer survive only
174
+ a re-ping of the same text, so a changed question must be marked again.
175
175
  ledger marked <ref> --marker-type label|role --marker <value>
176
176
  --comment-id <id>
177
177
  Records that the human-needed marker was applied on the tracker and
@@ -177,9 +177,14 @@ export function listPendingQuestions(ledger, answerCommand) {
177
177
  * gets a placeholder item with zero attempts.
178
178
  *
179
179
  * A re-ask keeps how the item is marked on the tracker (`markerType`,
180
- * `marker`, `markedAt`), so re-sending a question never makes the manager
181
- * mark the item a second time or forget the marker it must remove. A queued
182
- * answer survives only a re-ping of the same text: it answered that text.
180
+ * `marker`), so re-sending a question never makes the manager forget the
181
+ * marker it must remove. `markedAt` survives only a re-ping of the same text:
182
+ * a changed question has not been posted or read back yet, so it reads as
183
+ * unmarked until `ledger marked` records the new question comment. Keeping it
184
+ * would let the next cycle run `item-answer` against the old question comment
185
+ * and queue a reply to the old question as the answer to the new one
186
+ * (CodySwannGT/lisa#4570). A queued answer survives only a re-ping of the same
187
+ * text for the same reason: it answered that text.
183
188
  * `kind` defaults to the previous kind, then `worker-question`.
184
189
  * @param {string} projectRoot - Absolute project root
185
190
  * @param {{ref: string, text: string, telegramMessageId: number|null,
@@ -213,7 +218,7 @@ export function setPendingQuestion(
213
218
  telegramMessageId: telegramMessageId ?? null,
214
219
  markerType: existing?.markerType ?? null,
215
220
  marker: existing?.marker ?? null,
216
- markedAt: existing?.markedAt ?? null,
221
+ markedAt: sameText ? (existing?.markedAt ?? null) : null,
217
222
  managerCommentIds: existing?.managerCommentIds ?? [],
218
223
  answer: sameText ? (existing.answer ?? null) : null,
219
224
  },
@@ -16,11 +16,13 @@
16
16
  # for hours before someone queried the field by hand.
17
17
  #
18
18
  # The stale case is the one actually observed, and it is the one prose cannot
19
- # help with: the `base` ruleset sets `dismiss_stale_reviews_on_push: false`, so
20
- # a CHANGES_REQUESTED review stays attached to the commit it was made on and
21
- # pushing the fix never clears it, while `required_approving_review_count: 0`
22
- # means no scheduled re-review will ever clear it either. The PR is blocked
23
- # indefinitely and every check-based signal reads ready.
19
+ # help with: a CHANGES_REQUESTED review stays attached to the commit it was made
20
+ # on and pushing the fix never clears it, while
21
+ # `required_approving_review_count: 0` means no scheduled re-review will ever
22
+ # clear it either. The PR is blocked indefinitely and every check-based signal
23
+ # reads ready. `dismiss_stale_reviews_on_push` does not help: GitHub dismisses
24
+ # stale APPROVALS only, so a change request survives a push under either
25
+ # setting (measured 2026-11-01, CodySwannGT/lisa#4574).
24
26
  #
25
27
  # WHAT IT CHECKS, AND WHY THAT AND NOT THE CHECK ROLLUP
26
28
  #
@@ -1048,9 +1050,9 @@ REFUSAL = """Blocked: arming auto-merge on %s would report a merge that can neve
1048
1050
 
1049
1051
  GitHub reports reviewDecision = CHANGES_REQUESTED. That is a merge blocker with
1050
1052
  NO check-run representation: the failing-check count reads 0 and the checks tab
1051
- is entirely green while the PR is permanently unmergeable. Where the ruleset
1052
- sets dismiss_stale_reviews_on_push: false, pushing the fix does not clear it and
1053
- no re-review is scheduled to.
1053
+ is entirely green while the PR is permanently unmergeable. Pushing the fix does
1054
+ not clear it (dismiss_stale_reviews_on_push dismisses stale approvals only, not
1055
+ change requests), and no re-review is scheduled to.
1054
1056
 
1055
1057
  Clear the blocker, then arm:
1056
1058
  - resolve every review thread and address or reply to the feedback
@@ -391,15 +391,16 @@ declared, arm normally (the scope rule above). Where one IS declared, absent a
391
391
  description that positively states a review ran, treat the context as vacuous.
392
392
  Fail closed: absence of evidence is not evidence of a review.
393
393
 
394
- #### Vacuous and transient are different properties
394
+ #### Vacuous, transient or standing: the response is the same
395
395
 
396
- A vacuous context is not automatically something to wait out, and this is the
397
- distinction a naive wait-and-arm gets wrong — it waits forever on a repository
398
- where the state never clears. **Only the rate limit has both properties.**
396
+ A vacuous context is never something to wait out. Waiting looks like progress
397
+ but it is a stall, and on a repository where the state never clears it waits
398
+ forever. Some vacuities clear on their own and some do not. **That changes what
399
+ the report says, not what you do:**
399
400
 
400
401
  | description | vacuous? | clears on its own? |
401
402
  | --- | --- | --- |
402
- | `Review rate limited` | yes | **yes** — a throughput window |
403
+ | `Review rate limited` (and decorated forms such as `Review rate limited (retry in 12m)`) | yes | eventually — a throughput window of unknown length |
403
404
  | `Review skipped: manual review required for this OSS repository` | yes | **no** — a standing vendor policy for public repositories |
404
405
  | `Review skipped: N files exceed the limit of M` | yes | **no** — not without splitting the pull request |
405
406
  | `Review skipped` (bare, no stated reason) | yes | **unknown — treat as standing** |
@@ -408,17 +409,33 @@ A bare skip carries no machine-readable reason, so nothing distinguishes it from
408
409
  the standing forms. Requiring a stated reason before treating a skip as vacuous
409
410
  is how the commonest form gets missed.
410
411
 
411
- - **Transient (the rate limit, and only the rate limit)** — do not arm yet. Wait
412
- the window out and re-request the review **at most once**, then re-classify.
413
- If it now proves work, arm. If it does not, stop and treat it as standing.
414
- **Never loop**: a retry loop re-triggers the very limit it is escaping, and
415
- measured on #3220 a single explicit re-request in this repository was
416
- acknowledged, went `Review in progress`, and settled back at
417
- `Review rate limited` two seconds later having reviewed nothing — minting a
418
- second hollow green. One attempt, then report.
419
- - **Standing** — do not wait at all, and do not re-request. There is no window
420
- to wait out, so waiting produces a stall that reads as progress. Go straight
421
- to the watch loop and let step (d) adjudicate the merge in the open.
412
+ **Every row, the rate limit included: do not wait, and do not re-request.** Do
413
+ not arm. Go straight to the watch loop, run the local adversarial review over the
414
+ PR diff at the current head **now**, post it with the substitute marker (step
415
+ (d), "In case 3, substitute"), and keep driving. Fix what it finds, substitute
416
+ again on each new head, and let step (d) merge when the vacuous context is the
417
+ sole remaining gate.
418
+
419
+ The rate limit used to get a wait-the-window-out-and-re-request-once exception.
420
+ That exception is removed, for three reasons:
421
+
422
+ - **The window is not short.** Agents waiting it out stalled for an hour or more
423
+ per pull request, across a whole portfolio of repositories, and nothing about
424
+ the wait made the merge safer than a substitute review would.
425
+ - **Re-requesting does not work under a throttle.** Measured on #3220, a single
426
+ explicit re-request was acknowledged, went `Review in progress`, and settled
427
+ back at `Review rate limited` two seconds later having reviewed nothing. That
428
+ minted a second hollow green.
429
+ - **Re-requesting destroys evidence.** It overwrites the existing status (see
430
+ "Never re-request a review to 'refresh' it" in step (d)), so the exception
431
+ contradicted a rule this file states with no exceptions.
432
+
433
+ If the reviewer later reviews on its own, nothing special happens. Its threads
434
+ are review threads like any other, and step (d) handles them. A `CHANGES_REQUESTED`
435
+ it leaves is a real blocker (step (e)), and the substitute never overrides it.
436
+ If it now proves work, arm, per the table above. That is a re-classification
437
+ the watch loop makes on what it reads anyway. It is never a reason to wait or
438
+ re-request.
422
439
 
423
440
  #### A latch somebody else armed
424
441
 
@@ -642,17 +659,24 @@ step (e) first — which discriminates a live objection from a stranded one by
642
659
  unresolved thread count, not by `reviewDecision` — and arm afterwards.
643
660
 
644
661
  **Two ruleset settings decide whether a standing verdict ever clears itself, and
645
- in this repository both say no.** Verify them for the repo you are in rather
646
- than assuming, with `gh api repos/<owner>/<repo>/rulesets`:
647
-
648
- - `dismiss_stale_reviews_on_push: false` — the review stays attached to the
649
- commit it was made on, so **pushing the fix never clears it**. The intuition
650
- that a new head resets the verdict is wrong here.
662
+ neither one clears a change request.** Verify them for the repo you are in
663
+ rather than assuming, with `gh api repos/<owner>/<repo>/rules/branches/<base>`
664
+ (the rules in force on the base branch, with their parameters; the
665
+ `gh api repos/<owner>/<repo>/rulesets` list carries no rule parameters):
666
+
667
+ - `dismiss_stale_reviews_on_push` dismisses stale **approvals** only. The `base`
668
+ ruleset Lisa generates defaults it to `true` when
669
+ `policy.protect.dismiss_stale_reviews` is undeclared, and keeps a declared
670
+ value (CodySwannGT/lisa#4574), but a
671
+ `CHANGES_REQUESTED` review made on an older head stays in force after a push
672
+ under either setting (measured on a probe repository on 2026-11-01: the push
673
+ changed the diff and `reviewDecision` stayed `CHANGES_REQUESTED`), so
674
+ **pushing the fix never clears it**.
651
675
  - `required_approving_review_count: 0` — no approval is required to merge, so a
652
676
  standing `CHANGES_REQUESTED` is not holding a slot for a review that would
653
677
  otherwise arrive. Nothing is scheduled to clear it.
654
678
 
655
- Together those mean the block is **indefinite, not slow**. Waiting is not a
679
+ Where both hold, the block is **indefinite, not slow**. Waiting is not a
656
680
  strategy; something has to act.
657
681
 
658
682
  **Unresolved threads block independently of the verdict.** This repository sets
@@ -1046,10 +1070,11 @@ configured reviewed-when phrase says what "did work" looks like for this
1046
1070
  project.
1047
1071
 
1048
1072
  Prove the signal from the commit status API as above; a merely pending or queued
1049
- check is not proof of anything and must keep polling. **A transient vacuity is not adjudicated here until the arm gate's
1050
- single wait-and-re-request has been spent** (section 1): a rate limit has a
1051
- window, and merging past one you never waited out is a choice you did not have
1052
- to make. A standing vacuity has no window, so it arrives here immediately.
1073
+ check is not proof of anything and must keep polling. **A settled vacuity, the
1074
+ rate limit included, arrives here immediately.** There is no wait to spend first
1075
+ and no re-request to try: a rate limit's window is of unknown length, and
1076
+ waiting it out stalls the pull request without reading a line of the diff. The
1077
+ substitute is what makes the merge safe, not the passage of time.
1053
1078
 
1054
1079
  Once the signal is explicit, merge directly with `gh pr merge <pr>
1055
1080
  --<merge_method>` (pass `--admin` only if branch protection lists the vacuous
@@ -1170,6 +1195,34 @@ Deleting the fallback outright would over-correct: it is the only remedy a lane
1170
1195
  without dismissal permission has, and that permission is not universal. This
1171
1196
  narrows it to the case that costs nothing.
1172
1197
 
1198
+ **A stale bot verdict under a rate limit: substitute, then stop and report.**
1199
+ Under a rate limit or an exhausted review allowance, a bot's old
1200
+ `CHANGES_REQUESTED` on an outdated head can end up as the only thing blocking
1201
+ the merge. The bot cannot post a fresh review to replace it, so the verdict
1202
+ will not clear by itself while the throttle lasts. This was measured on a
1203
+ caller repo in the portfolio: every check green, zero unresolved threads, and
1204
+ the bot's verdict from an earlier head as the only blocker. In that case:
1205
+
1206
+ 1. **Do not wait for the window to reset, and do not re-request.** Waiting
1207
+ leaves the pull request wedged for an hour or more. A re-request under the
1208
+ throttle settles hollow and can take away the bot's standing review (see
1209
+ above).
1210
+ 2. Post the local adversarial review substitute for the **current** head, as
1211
+ step (d) case 3 describes, if one is not already there.
1212
+ 3. **Then stop** with `blocked:stale-bot-verdict` (section 4). Report exactly
1213
+ that blocker: the bot reviewer, the review id, the outdated head it reviewed
1214
+ and the current head, the unresolved thread count, and that the substitute
1215
+ is posted for the current head. Name the decision that is needed: a human
1216
+ with dismissal rights dismisses that review, or decides it must stand.
1217
+
1218
+ Do not dismiss it on the strength of your own substitute. Here the bot's review
1219
+ is the only third-party review on the pull request. Dismissing it because of a
1220
+ review this run wrote would make the agent both author and judge of the change.
1221
+ That is the escalation the bullet above requires, reached right away instead of
1222
+ after a wait. A stranded verdict that is **not** the only substantive review, or
1223
+ one left by a reviewer that is not throttled, is still handled by the
1224
+ dismissal rule above.
1225
+
1173
1226
  **Unverified, and stated as such rather than asserted:** that a bot's re-review
1174
1227
  REPLACES its predecessor in the effective set is inferred from the observed
1175
1228
  consequence, not measured against the vendor. It has not been tested here, and
@@ -1453,7 +1506,7 @@ Loop until one of:
1453
1506
  the mode forbids acting on them: `on_blocker=report`). The latch is
1454
1507
  deliberately not armed, so the PR sits OPEN rather than merging on a review
1455
1508
  nothing performed. Report the PR URL, the review context's settled
1456
- description, whether it is transient or standing, and which sole-gate
1509
+ description, whether it clears on its own (a rate limit) or is standing, and which sole-gate
1457
1510
  condition failed. This is an outcome that needs an operator, stated in one
1458
1511
  line a non-technical reader can act on — for example: *"PR #123 is ready to
1459
1512
  merge but nobody reviewed it: the review tool says it skips public
@@ -1483,6 +1536,16 @@ Loop until one of:
1483
1536
  — but a review asked for changes and that is still on record, so it will sit
1484
1537
  there forever. Someone needs to clear that review or resolve the open
1485
1538
  comments."* Never report this PR as merging.
1539
+ - **`blocked:stale-bot-verdict`** — the only thing blocking the merge is a
1540
+ bot's `CHANGES_REQUESTED` on an outdated head, the bot is rate limited or out
1541
+ of review allowance so it cannot replace that verdict, and the local
1542
+ adversarial review substitute is posted for the current head (step (e)).
1543
+ Report the reviewer, the review id, both heads, the unresolved thread count,
1544
+ and the dismissal decision a human needs to make. For example: *"PR #123 is
1545
+ ready except for an old review from the review bot on an earlier version. The
1546
+ bot is rate limited and cannot re-review, and I posted my own review of the
1547
+ current version. Someone with dismissal rights needs to dismiss review 456,
1548
+ or decide it should stand."* Do not wait for the bot's window to reset.
1486
1549
  - **`CLOSED`** → report (PR was closed without merge).
1487
1550
  - **Hard block needing a human**: an unresolvable conflict, a failing check that
1488
1551
  needs design input, or genuine unresolved human objection (not a bot gate). Stop
@@ -1591,7 +1654,8 @@ declare none, one, or several, and each shows its own evidence.
1591
1654
 
1592
1655
  Quote the description that was actually read. A report that says "rate limited"
1593
1656
  on a policy skip is a wrong record, and the two call for different operator
1594
- actions: one clears itself, the other never will.
1657
+ actions: one clears itself, the other never will. Neither changes what this
1658
+ skill does in the meantime: it substitutes and keeps driving in both cases.
1595
1659
 
1596
1660
  `NOT REVIEWED` is reporting, and on a merged PR it is never a terminal state of
1597
1661
  its own: it does not turn a merged PR into a blocked one. What it does gate is
@@ -73,6 +73,14 @@ node scripts/check-skipped-required-checks.mjs --pr=<pr> --json
73
73
  It reports and never fails — a hollow check is often a vendor spending cap, not
74
74
  a repository defect, and it is not this skill's call to block on one.
75
75
 
76
+ **Do not wait for a rate limit to reset, and do not re-request the review.** A
77
+ `Review rate limited` status, including decorated forms such as
78
+ `Review rate limited (retry in 12m)`, is handled like every other vacuous
79
+ review. Report it here. `lisa-drive-pr-to-merge` then posts a local adversarial
80
+ review substitute for the current head right away and keeps driving. Waiting
81
+ out the window stalls the pull request for an hour or more and reads no code.
82
+ Re-requesting overwrites the status and can destroy a real review.
83
+
76
84
  Carry the finding into your Step 4 report. **Do not write "no unresolved review
77
85
  threads" on its own** — it is true of an unreviewed PR too. Say which you
78
86
  observed:
@@ -273,7 +273,7 @@ No create, no update, no rotate. Writing secrets or their notes requires an auth
273
273
 
274
274
  The two writers are siblings, not modes: `rotate-secret.mjs` replaces a value **at its source**, and `sync-secret-to-ci.mjs` copies one **into a second store** without touching the source. Each needs an authority the resolver must not hold, so each is its own program with its own declaration list.
275
275
 
276
- A third, narrow writer lives outside this skill: the session manager's `store-token` (`lisa-session-manager/scripts/token-store.mjs`) stores the Telegram bot token as `TELEGRAM_BOT_TOKEN_<NAME>` through `providers.mjs` `createSecret` (a new record) or `writeSecret` (`--replace`), then reads it back through the ordinary provider view and compares hashes before deleting the plaintext file it replaces. It writes that one name only, and only when an operator runs it. `createSecret` is Bitwarden-only; on any other provider the operator creates the secret and `store-token` verifies it. Like `bws secret edit`, `bws secret create` (2.1.0) takes the value as a positional argument and its help offers no stdin or file option, so the value is in argv for the life of that one call: the same documented exception, confined the same way (no shell, `--output none`, and a failure message that never repeats the command line).
276
+ A third, narrow writer lives outside this skill: the session manager's `store-token` (`lisa-session-manager/scripts/token-store.mjs`) stores the Telegram bot token as `TELEGRAM_BOT_TOKEN_<NAME>` through `providers.mjs` `createSecret` (a new record) or `writeSecret` (`--replace`), then reads it back through the ordinary provider view and compares hashes before deleting the plaintext file it replaces. It writes that one name only, and only when an operator runs it. `createSecret` is Bitwarden-only; on any other provider the operator creates the secret and `store-token` verifies it. Like `bws secret edit`, `bws secret create` (2.1.0) takes the value as a positional argument and its help offers no stdin or file option, so the value is in argv for the life of that one call: the second documented exception in Rule 4, held to the same limits (no shell, the CLI's output never shown, here through `--output none`, and a failure message that never repeats the command line).
277
277
 
278
278
  ## Rotating credentials
279
279
 
@@ -427,6 +427,6 @@ Cache **in-process only**. Never write a resolved value to disk except through t
427
427
  1. **Never read a keychain, `.env`, or provider CLI outside this skill.** One chokepoint is what makes the single-store rule enforceable.
428
428
  2. **Never log a secret value.** Print a length or a hash prefix when proving identity.
429
429
  3. **Never write a resolved value to disk** except through the materialization contract, on a surface whose capabilities permit it.
430
- 4. **Never pass a secret as a command-line argument.** The one documented exception is the Bitwarden rotation write, whose CLI exposes no stdin path; it is confined to that single operation and noted in the code.
430
+ 4. **Never pass a secret as a command-line argument.** There are two documented exceptions, both Bitwarden writes whose CLI (2.1.0) takes the value only on the command line: `bws secret edit --value` (the rotation write, `writeSecret`) and `bws secret create` (`store-token`'s `createSecret`). Each is confined to that single operation and noted in the code, and both are held to the same limits: no shell, the CLI's output (which echoes the record) is never shown (`create` passes `--output none`; `edit`'s output is captured and discarded), and a failure message never repeats the command line.
431
431
  5. **Verify a credential when it is stored, not when it is first used.** An unverified credential is indistinguishable from a broken one, and the gap between the two is measured in weeks.
432
432
  6. **Treat a mismatch as stop-and-ask.** If a value differs between two places, they are two live credentials — not a stale copy to be discarded. Deleting the one you cannot verify leaves a working credential that no record accounts for.
@@ -170,8 +170,8 @@ Per cycle
170
170
  [--sender <name>] STDIN: question
171
171
  --kind: worker-question (default) | repo-route | exhausted | decision.
172
172
  No message id with no channel. A re-ask keeps the item's marker
173
- fields (markerType, marker, markedAt); a queued answer survives only a
174
- re-ping of the same text.
173
+ fields (markerType, marker); markedAt and a queued answer survive only
174
+ a re-ping of the same text, so a changed question must be marked again.
175
175
  ledger marked <ref> --marker-type label|role --marker <value>
176
176
  --comment-id <id>
177
177
  Records that the human-needed marker was applied on the tracker and
@@ -177,9 +177,14 @@ export function listPendingQuestions(ledger, answerCommand) {
177
177
  * gets a placeholder item with zero attempts.
178
178
  *
179
179
  * A re-ask keeps how the item is marked on the tracker (`markerType`,
180
- * `marker`, `markedAt`), so re-sending a question never makes the manager
181
- * mark the item a second time or forget the marker it must remove. A queued
182
- * answer survives only a re-ping of the same text: it answered that text.
180
+ * `marker`), so re-sending a question never makes the manager forget the
181
+ * marker it must remove. `markedAt` survives only a re-ping of the same text:
182
+ * a changed question has not been posted or read back yet, so it reads as
183
+ * unmarked until `ledger marked` records the new question comment. Keeping it
184
+ * would let the next cycle run `item-answer` against the old question comment
185
+ * and queue a reply to the old question as the answer to the new one
186
+ * (CodySwannGT/lisa#4570). A queued answer survives only a re-ping of the same
187
+ * text for the same reason: it answered that text.
183
188
  * `kind` defaults to the previous kind, then `worker-question`.
184
189
  * @param {string} projectRoot - Absolute project root
185
190
  * @param {{ref: string, text: string, telegramMessageId: number|null,
@@ -213,7 +218,7 @@ export function setPendingQuestion(
213
218
  telegramMessageId: telegramMessageId ?? null,
214
219
  markerType: existing?.markerType ?? null,
215
220
  marker: existing?.marker ?? null,
216
- markedAt: existing?.markedAt ?? null,
221
+ markedAt: sameText ? (existing?.markedAt ?? null) : null,
217
222
  managerCommentIds: existing?.managerCommentIds ?? [],
218
223
  answer: sameText ? (existing.answer ?? null) : null,
219
224
  },