@codyswann/lisa 4.87.1 → 4.87.3

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 (116) 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.d.ts.map +1 -1
  9. package/dist/core/upstream-evidence-manifest.js +14 -9
  10. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  11. package/package.json +4 -4
  12. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +14 -7
  15. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +2 -2
  16. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  17. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  18. package/plugins/lisa/hooks/block-blind-automerge.sh +10 -8
  19. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +14 -7
  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 +14 -7
  26. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +2 -2
  27. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  28. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  29. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  32. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  36. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +10 -8
  38. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +14 -7
  39. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +2 -2
  40. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  41. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  42. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +10 -8
  44. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +14 -7
  45. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +2 -2
  46. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  47. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  48. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  50. package/plugins/lisa-devin/hooks/block-blind-automerge.sh +10 -8
  51. package/plugins/lisa-devin/skills/lisa-drive-pr-to-merge/SKILL.md +14 -7
  52. package/plugins/lisa-devin/skills/lisa-secrets-access/SKILL.md +2 -2
  53. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  54. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  55. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-expo-agy/plugin.json +1 -1
  58. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  62. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  65. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  69. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  72. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  76. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  79. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  83. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  86. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  90. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-rails-agy/plugin.json +1 -1
  93. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  97. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  99. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  100. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  104. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  106. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  107. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  111. package/plugins/src/base/hooks/block-blind-automerge.sh +10 -8
  112. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +14 -7
  113. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +2 -2
  114. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +2 -2
  115. package/plugins/src/base/skills/lisa-session-manager/scripts/ledger-questions.mjs +9 -4
  116. package/scripts/lisa-ruleset-payload.mjs +17 -2
package/package.json CHANGED
@@ -189,7 +189,7 @@
189
189
  "qs": "^6.16.0"
190
190
  },
191
191
  "name": "@codyswann/lisa",
192
- "version": "4.87.1",
192
+ "version": "4.87.3",
193
193
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
194
194
  "main": "dist/index.js",
195
195
  "exports": {
@@ -347,7 +347,7 @@
347
347
  "test": "tests"
348
348
  },
349
349
  "types": "./dist/index.d.ts",
350
- "lisaReleaseCommit": "685c54f5fdefa2acbc9a412557124db84d898f79",
351
- "gitHead": "685c54f5fdefa2acbc9a412557124db84d898f79",
352
- "lisaReleaseTag": "v4.87.1"
350
+ "lisaReleaseCommit": "5a1944f7ad31a802d3f47945995b2f938f187a8d",
351
+ "gitHead": "5a1944f7ad31a802d3f47945995b2f938f187a8d",
352
+ "lisaReleaseTag": "v4.87.3"
353
353
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
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.1",
3
+ "version": "4.87.3",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -661,17 +661,24 @@ step (e) first — which discriminates a live objection from a stranded one by
661
661
  unresolved thread count, not by `reviewDecision` — and arm afterwards.
662
662
 
663
663
  **Two ruleset settings decide whether a standing verdict ever clears itself, and
664
- in this repository both say no.** Verify them for the repo you are in rather
665
- than assuming, with `gh api repos/<owner>/<repo>/rulesets`:
666
-
667
- - `dismiss_stale_reviews_on_push: false` — the review stays attached to the
668
- commit it was made on, so **pushing the fix never clears it**. The intuition
669
- 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**.
670
677
  - `required_approving_review_count: 0` — no approval is required to merge, so a
671
678
  standing `CHANGES_REQUESTED` is not holding a slot for a review that would
672
679
  otherwise arrive. Nothing is scheduled to clear it.
673
680
 
674
- 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
675
682
  strategy; something has to act.
676
683
 
677
684
  **Unresolved threads block independently of the verdict.** This repository sets
@@ -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
@@ -659,17 +659,24 @@ step (e) first — which discriminates a live objection from a stranded one by
659
659
  unresolved thread count, not by `reviewDecision` — and arm afterwards.
660
660
 
661
661
  **Two ruleset settings decide whether a standing verdict ever clears itself, and
662
- in this repository both say no.** Verify them for the repo you are in rather
663
- than assuming, with `gh api repos/<owner>/<repo>/rulesets`:
664
-
665
- - `dismiss_stale_reviews_on_push: false` — the review stays attached to the
666
- commit it was made on, so **pushing the fix never clears it**. The intuition
667
- 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**.
668
675
  - `required_approving_review_count: 0` — no approval is required to merge, so a
669
676
  standing `CHANGES_REQUESTED` is not holding a slot for a review that would
670
677
  otherwise arrive. Nothing is scheduled to clear it.
671
678
 
672
- 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
673
680
  strategy; something has to act.
674
681
 
675
682
  **Unresolved threads block independently of the verdict.** This repository sets
@@ -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
  },
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -659,17 +659,24 @@ step (e) first — which discriminates a live objection from a stranded one by
659
659
  unresolved thread count, not by `reviewDecision` — and arm afterwards.
660
660
 
661
661
  **Two ruleset settings decide whether a standing verdict ever clears itself, and
662
- in this repository both say no.** Verify them for the repo you are in rather
663
- than assuming, with `gh api repos/<owner>/<repo>/rulesets`:
664
-
665
- - `dismiss_stale_reviews_on_push: false` — the review stays attached to the
666
- commit it was made on, so **pushing the fix never clears it**. The intuition
667
- 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**.
668
675
  - `required_approving_review_count: 0` — no approval is required to merge, so a
669
676
  standing `CHANGES_REQUESTED` is not holding a slot for a review that would
670
677
  otherwise arrive. Nothing is scheduled to clear it.
671
678
 
672
- 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
673
680
  strategy; something has to act.
674
681
 
675
682
  **Unresolved threads block independently of the verdict.** This repository sets
@@ -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
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.87.1",
3
+ "version": "4.87.3",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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
@@ -659,17 +659,24 @@ step (e) first — which discriminates a live objection from a stranded one by
659
659
  unresolved thread count, not by `reviewDecision` — and arm afterwards.
660
660
 
661
661
  **Two ruleset settings decide whether a standing verdict ever clears itself, and
662
- in this repository both say no.** Verify them for the repo you are in rather
663
- than assuming, with `gh api repos/<owner>/<repo>/rulesets`:
664
-
665
- - `dismiss_stale_reviews_on_push: false` — the review stays attached to the
666
- commit it was made on, so **pushing the fix never clears it**. The intuition
667
- 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**.
668
675
  - `required_approving_review_count: 0` — no approval is required to merge, so a
669
676
  standing `CHANGES_REQUESTED` is not holding a slot for a review that would
670
677
  otherwise arrive. Nothing is scheduled to clear it.
671
678
 
672
- 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
673
680
  strategy; something has to act.
674
681
 
675
682
  **Unresolved threads block independently of the verdict.** This repository sets
@@ -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