@codyswann/lisa 4.34.15 → 4.34.17
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.
- package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +3 -5
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +20 -3
- package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +33 -8
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +49 -4
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +1 -1
- package/dist/codex/scripts/block-no-verify.sh +49 -4
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +18 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +17 -14
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +49 -4
- package/expo/copy-overwrite/scripts/check-lighthouse-details.mjs +13 -7
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/plugins/lisa/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/lisa/hooks/block-managed-file-edits.sh +33 -8
- package/plugins/lisa/hooks/block-no-verify.agy.sh +49 -4
- package/plugins/lisa/hooks/block-no-verify.sh +49 -4
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +49 -4
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +33 -8
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +49 -4
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +33 -8
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +49 -4
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/src/base/hooks/block-managed-file-edits.sh +33 -8
- package/plugins/src/base/hooks/block-no-verify.agy.sh +49 -4
- package/plugins/src/base/hooks/block-no-verify.sh +49 -4
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/rails/create-only/.github/workflows/deploy.yml +30 -0
- package/typescript/copy-overwrite/.claude/hooks/worktree-create.sh +3 -1
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +14 -54
|
@@ -460,6 +460,50 @@ passed off as success.
|
|
|
460
460
|
`blocked:unreviewed`. This fallback lives inside the gated section above —
|
|
461
461
|
with `auto_merge=false` it never fires; the PR remains open awaiting a human.
|
|
462
462
|
|
|
463
|
+
### The mergeability gate — never arm a PR that cannot merge
|
|
464
|
+
|
|
465
|
+
The arm gate above asks *did the review context do work?* This one asks a
|
|
466
|
+
different question — *is there a verdict standing in the way?* — and they are
|
|
467
|
+
not the same. A `CHANGES_REQUESTED` review unambiguously **did work**, so it
|
|
468
|
+
sails through the arm gate and the latch goes on over the top of it.
|
|
469
|
+
|
|
470
|
+
**Read `reviewDecision` explicitly. Never infer it from a check count.**
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
gh pr view <pr> --json reviewDecision,reviewThreads \
|
|
474
|
+
--jq '{decision: .reviewDecision, unresolved: [.reviewThreads[]? | select(.isResolved == false)] | length}'
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
`reviewDecision` is **not part of `statusCheckRollup`**. That is the whole reason
|
|
478
|
+
this stayed invisible: a failing-check count reads **zero** on a PR that can
|
|
479
|
+
never merge, and the checks tab is entirely green. Any rule keyed on "no red
|
|
480
|
+
checks" is satisfied by exactly the PR this gate exists to catch (#3720).
|
|
481
|
+
|
|
482
|
+
**Do not arm while `reviewDecision == CHANGES_REQUESTED`.** Arming is a claim
|
|
483
|
+
that this PR will merge, and that claim is false here. Clear the verdict through
|
|
484
|
+
step (e) first — which discriminates a live objection from a stranded one by
|
|
485
|
+
unresolved thread count, not by `reviewDecision` — and arm afterwards.
|
|
486
|
+
|
|
487
|
+
**Two ruleset settings decide whether a standing verdict ever clears itself, and
|
|
488
|
+
in this repository both say no.** Verify them for the repo you are in rather
|
|
489
|
+
than assuming, with `gh api repos/<owner>/<repo>/rulesets`:
|
|
490
|
+
|
|
491
|
+
- `dismiss_stale_reviews_on_push: false` — the review stays attached to the
|
|
492
|
+
commit it was made on, so **pushing the fix never clears it**. The intuition
|
|
493
|
+
that a new head resets the verdict is wrong here.
|
|
494
|
+
- `required_approving_review_count: 0` — no approval is required to merge, so a
|
|
495
|
+
standing `CHANGES_REQUESTED` is not holding a slot for a review that would
|
|
496
|
+
otherwise arrive. Nothing is scheduled to clear it.
|
|
497
|
+
|
|
498
|
+
Together those mean the block is **indefinite, not slow**. Waiting is not a
|
|
499
|
+
strategy; something has to act.
|
|
500
|
+
|
|
501
|
+
**Unresolved threads block independently of the verdict.** This repository sets
|
|
502
|
+
`required_review_thread_resolution: true`, so a PR can have a clear
|
|
503
|
+
`reviewDecision` and still be unmergeable on threads alone — a second blocker
|
|
504
|
+
that is equally absent from the check rollup. Read both, and name whichever one
|
|
505
|
+
is standing; reporting the wrong one sends the operator to the wrong place.
|
|
506
|
+
|
|
463
507
|
## 2. The watch loop
|
|
464
508
|
|
|
465
509
|
Poll the live state each iteration:
|
|
@@ -1078,6 +1122,14 @@ Loop until one of:
|
|
|
1078
1122
|
the one thing nobody would then do is investigate it. Usually a missing
|
|
1079
1123
|
`issues: read` / label-read permission, which is fixable and invisible from
|
|
1080
1124
|
the merge outcome alone.
|
|
1125
|
+
- **`blocked:armed-unmergeable`** — the PR is OPEN, auto-merge is ON, every check
|
|
1126
|
+
is green, and it still cannot merge: `reviewDecision == CHANGES_REQUESTED`, or
|
|
1127
|
+
unresolved review threads under `required_review_thread_resolution`. Report the
|
|
1128
|
+
PR URL, **which** of the two is standing, and that it will not clear itself.
|
|
1129
|
+
For example: *"PR #123 looks ready — every check is green and auto-merge is on
|
|
1130
|
+
— but a review asked for changes and that is still on record, so it will sit
|
|
1131
|
+
there forever. Someone needs to clear that review or resolve the open
|
|
1132
|
+
comments."* Never report this PR as merging.
|
|
1081
1133
|
- **`CLOSED`** → report (PR was closed without merge).
|
|
1082
1134
|
- **Hard block needing a human**: an unresolvable conflict, a failing check that
|
|
1083
1135
|
needs design input, or genuine unresolved human objection (not a bot gate). Stop
|
|
@@ -1087,6 +1139,44 @@ Loop until one of:
|
|
|
1087
1139
|
adversarial-review substitute is posted (step d); it never extends to any
|
|
1088
1140
|
other gate.
|
|
1089
1141
|
|
|
1142
|
+
### Before you stop: never leave a PR armed and unmergeable in silence
|
|
1143
|
+
|
|
1144
|
+
**This applies at every exit that leaves the PR OPEN** — not only the terminal
|
|
1145
|
+
states above, but any point where this run concludes it is finished, hands back,
|
|
1146
|
+
or gives up. Re-read, from the live PR, immediately before reporting:
|
|
1147
|
+
|
|
1148
|
+
```bash
|
|
1149
|
+
gh pr view <pr> --json state,autoMergeRequest,reviewDecision,reviewThreads \
|
|
1150
|
+
--jq '{state, armed: (.autoMergeRequest != null), decision: .reviewDecision,
|
|
1151
|
+
unresolved: [.reviewThreads[]? | select(.isResolved == false)] | length}'
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
If `state == OPEN` and `armed` and either `decision == "CHANGES_REQUESTED"` or
|
|
1155
|
+
`unresolved > 0`, the outcome is **`blocked:armed-unmergeable`** — never a
|
|
1156
|
+
success, and never silence.
|
|
1157
|
+
|
|
1158
|
+
**Why this exists rather than trusting the loop.** While the loop runs this is
|
|
1159
|
+
already covered: the watch poll reads `reviewDecision` and step (e) handles it.
|
|
1160
|
+
The failure is a run that **stops** while the PR is armed and blocked — the
|
|
1161
|
+
session ends, or the agent decides the work is done because arming looked like
|
|
1162
|
+
completion. Measured (#3720): three PRs were left armed, green and permanently
|
|
1163
|
+
unmergeable, and every one was found only because a person queried
|
|
1164
|
+
`reviewDecision` by hand. An agent that arms auto-merge and reports "armed, will
|
|
1165
|
+
merge" is reporting something false and has no reason to know it.
|
|
1166
|
+
|
|
1167
|
+
**Arming is not completion.** It is a claim about the future that this check is
|
|
1168
|
+
what makes honest. The claim costs nothing when true — one read, and the report
|
|
1169
|
+
says merging — and when false it is the only thing standing between a parked PR
|
|
1170
|
+
and nobody noticing for hours.
|
|
1171
|
+
|
|
1172
|
+
**Read the two fields; do not substitute a check count.** Zero failing checks is
|
|
1173
|
+
the symptom, not the signal — that is precisely why this class was invisible.
|
|
1174
|
+
|
|
1175
|
+
In `on_blocker=report` mode the same condition returns the existing
|
|
1176
|
+
`blocked:changes_requested` classification rather than this terminal state: that
|
|
1177
|
+
mode returns a blocker for the caller to act on, and does not drive. The two
|
|
1178
|
+
names describe the same PR seen from the two modes.
|
|
1179
|
+
|
|
1090
1180
|
At every terminal state, release the babysitter lease
|
|
1091
1181
|
(`gh pr edit <pr> --remove-label "lisa:babysitter-on-duty"`) so the CI
|
|
1092
1182
|
auto-fix workflow can take over as fixer of last resort if the branch goes
|
|
@@ -1432,8 +1432,20 @@ EXEC_WRAPPERS = {
|
|
|
1432
1432
|
}
|
|
1433
1433
|
|
|
1434
1434
|
# Interpreter options that mean "no script file follows": a command string or a
|
|
1435
|
-
# module name, both of which the guard reads by other means or not at all.
|
|
1436
|
-
|
|
1435
|
+
# module name, both of which the guard reads by other means or not at all. These
|
|
1436
|
+
# are interpreter-specific on purpose. `bash -e create.sh` and `sh -m create.sh`
|
|
1437
|
+
# still execute the following file; treating Python and Node flags as universal
|
|
1438
|
+
# silently stopped the scan before those shell operands.
|
|
1439
|
+
NO_SCRIPT_OPTIONS = {
|
|
1440
|
+
"bun": {"-e", "--eval"},
|
|
1441
|
+
"node": {"-e", "--eval"},
|
|
1442
|
+
"nodejs": {"-e", "--eval"},
|
|
1443
|
+
"perl": {"-e"},
|
|
1444
|
+
"php": {"-r"},
|
|
1445
|
+
"python": {"-c", "-m", "--command", "--module"},
|
|
1446
|
+
"python3": {"-c", "-m", "--command", "--module"},
|
|
1447
|
+
"ruby": {"-e"},
|
|
1448
|
+
}
|
|
1437
1449
|
|
|
1438
1450
|
# Programs that READ their operands and never execute them as programs.
|
|
1439
1451
|
#
|
|
@@ -1569,7 +1581,12 @@ def executed_operand(argv):
|
|
|
1569
1581
|
if argument.startswith("-"):
|
|
1570
1582
|
# A command string or a module name; no script file follows.
|
|
1571
1583
|
head = argument.split("=", 1)[0]
|
|
1572
|
-
if
|
|
1584
|
+
if program in {"bash", "dash", "ksh", "sh", "zsh"}:
|
|
1585
|
+
# Shell `-c` may be clustered (`-ec`), while `-e` and `-m` are
|
|
1586
|
+
# ordinary shell options and do not consume the script path.
|
|
1587
|
+
if not head.startswith("--") and "c" in head[1:]:
|
|
1588
|
+
return None
|
|
1589
|
+
elif head in NO_SCRIPT_OPTIONS.get(program, set()):
|
|
1573
1590
|
return None
|
|
1574
1591
|
continue
|
|
1575
1592
|
return argument
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
# from mine", and guessing "behind" is how a fork's stronger guard gets silently
|
|
91
91
|
# replaced by a weaker upstream one. Add a name here in the same commit that
|
|
92
92
|
# closes a vector.
|
|
93
|
-
# lisa-guard-capabilities: managed-path-resolution, lisaignore-precedence, generated-path-rebuild, redirect-target, tee-target, sed-in-place-all-spellings, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand
|
|
93
|
+
# lisa-guard-capabilities: managed-path-resolution, lisaignore-precedence, generated-path-rebuild, redirect-target, tee-target, sed-in-place-all-spellings, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, direct-script-command-reach, analyzer-failure-visible
|
|
94
94
|
set -euo pipefail
|
|
95
95
|
|
|
96
96
|
input="$(cat)"
|
|
@@ -607,7 +607,9 @@ def command_word(statement):
|
|
|
607
607
|
statement: One statement's tokens.
|
|
608
608
|
|
|
609
609
|
Returns:
|
|
610
|
-
A
|
|
610
|
+
A triple (token, program, args); (None, None, []) when no program is
|
|
611
|
+
named. The raw token is retained because `./edit.sh` has no interpreter:
|
|
612
|
+
its command word is itself the executed script.
|
|
611
613
|
"""
|
|
612
614
|
index = 0
|
|
613
615
|
while index < len(statement):
|
|
@@ -617,7 +619,7 @@ def command_word(statement):
|
|
|
617
619
|
continue
|
|
618
620
|
program = token.rsplit("/", 1)[-1]
|
|
619
621
|
if program not in FOLLOW_WRAPPERS:
|
|
620
|
-
return (program, statement[index + 1 :])
|
|
622
|
+
return (token, program, statement[index + 1 :])
|
|
621
623
|
separate, positional = FOLLOW_WRAPPERS[program]
|
|
622
624
|
index += 1
|
|
623
625
|
while index < len(statement) and statement[index].startswith("-"):
|
|
@@ -627,7 +629,7 @@ def command_word(statement):
|
|
|
627
629
|
break
|
|
628
630
|
index += 2 if option in separate else 1
|
|
629
631
|
index += positional
|
|
630
|
-
return (None, [])
|
|
632
|
+
return (None, None, [])
|
|
631
633
|
|
|
632
634
|
|
|
633
635
|
def shell_script_operand(args):
|
|
@@ -674,7 +676,7 @@ def write_targets(statement):
|
|
|
674
676
|
Candidate paths this statement writes.
|
|
675
677
|
"""
|
|
676
678
|
found = []
|
|
677
|
-
program, args = command_word(statement)
|
|
679
|
+
_token, program, args = command_word(statement)
|
|
678
680
|
for index, token in enumerate(statement):
|
|
679
681
|
if token in REDIRECT_OUT and index + 1 < len(statement):
|
|
680
682
|
found.append(statement[index + 1])
|
|
@@ -697,7 +699,7 @@ def executed_script(statement):
|
|
|
697
699
|
Returns:
|
|
698
700
|
The operand token naming a script that will run, or None.
|
|
699
701
|
"""
|
|
700
|
-
program, args = command_word(statement)
|
|
702
|
+
token, program, args = command_word(statement)
|
|
701
703
|
if program is None:
|
|
702
704
|
return None
|
|
703
705
|
if program in SHELL_PROGRAMS:
|
|
@@ -708,6 +710,12 @@ def executed_script(statement):
|
|
|
708
710
|
return shell_script_operand(args)
|
|
709
711
|
if program in SOURCE_BUILTINS:
|
|
710
712
|
return args[0] if args else None
|
|
713
|
+
# A script in command position executes through its shebang. This also
|
|
714
|
+
# covers a direct path after wrappers such as `sudo ./edit.sh`; `command_word`
|
|
715
|
+
# deliberately keeps the raw token while using its basename for wrapper and
|
|
716
|
+
# shell classification.
|
|
717
|
+
if resolve(token) is not None:
|
|
718
|
+
return token
|
|
711
719
|
return None
|
|
712
720
|
|
|
713
721
|
|
|
@@ -798,10 +806,27 @@ for target in targets:
|
|
|
798
806
|
if cleaned:
|
|
799
807
|
print(cleaned)
|
|
800
808
|
PY
|
|
801
|
-
|
|
809
|
+
analyzer_stderr="$(mktemp "${TMPDIR:-/tmp}/lisa-managed-edit-analyzer.XXXXXX")" || {
|
|
810
|
+
printf 'block-managed-file-edits: could not allocate analyzer stderr capture; Bash write protection is NOT active\n' >&2
|
|
811
|
+
exit 0
|
|
812
|
+
}
|
|
813
|
+
if write_targets="$(printf '%s' "$analyzer" |
|
|
802
814
|
MANAGED_EDIT_COMMAND="$command_str" \
|
|
803
815
|
MANAGED_EDIT_PROJECT="$project_root" \
|
|
804
|
-
python3 - 2
|
|
816
|
+
python3 - 2>"$analyzer_stderr")"; then
|
|
817
|
+
analyzer_status=0
|
|
818
|
+
else
|
|
819
|
+
analyzer_status=$?
|
|
820
|
+
fi
|
|
821
|
+
if [ "$analyzer_status" -ne 0 ]; then
|
|
822
|
+
printf 'block-managed-file-edits: Bash analyzer failed (exit %s); Bash write protection is NOT active\n' "$analyzer_status" >&2
|
|
823
|
+
while IFS= read -r analyzer_line; do
|
|
824
|
+
[ -n "$analyzer_line" ] && printf ' python3: %s\n' "$analyzer_line" >&2
|
|
825
|
+
done <"$analyzer_stderr"
|
|
826
|
+
rm -f "$analyzer_stderr"
|
|
827
|
+
exit 0
|
|
828
|
+
fi
|
|
829
|
+
rm -f "$analyzer_stderr"
|
|
805
830
|
while IFS= read -r token; do
|
|
806
831
|
[ -n "$token" ] || continue
|
|
807
832
|
if source_path="$(managed_source "$token")"; then
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
# Add a name here in the same commit that closes a vector. A hardening that
|
|
34
34
|
# forgets to is invisible to refresh, and shows up as an unexplained diff at
|
|
35
35
|
# review time instead of a named capability.
|
|
36
|
-
# lisa-guard-capabilities: no-verify-abbrev, husky-env, hookspath-allowlist, config-env, env-split-string, git-config-key, git-config-parameters, git-config-parameters-append, git-config-parameters-expansion, heredoc-shell-word, herestring-aware, no-verify-short, nested-shell-no-verify, nested-shell-long-options, env-split-string-abbrev, command-wrapper-normalization, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, dispatcher-exec-position
|
|
36
|
+
# lisa-guard-capabilities: no-verify-abbrev, husky-env, hookspath-allowlist, config-env, env-split-string, git-config-key, git-config-parameters, git-config-parameters-append, git-config-parameters-expansion, heredoc-shell-word, herestring-aware, no-verify-short, nested-shell-no-verify, nested-shell-long-options, env-split-string-abbrev, command-wrapper-normalization, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, dispatcher-exec-position, tilde-script-reach, shell-option-stdin-reach
|
|
37
37
|
#
|
|
38
38
|
# Shell-token matching avoids false positives from issue bodies, heredocs, and
|
|
39
39
|
# commit-message prose while still catching quoted real argv values such as
|
|
@@ -1007,7 +1007,7 @@ def resolve_script(token):
|
|
|
1007
1007
|
"""
|
|
1008
1008
|
if COMPUTED_VALUE.search(token):
|
|
1009
1009
|
return (None, "a computed path the guard cannot resolve before the shell does")
|
|
1010
|
-
text = token.strip().strip("'\"")
|
|
1010
|
+
text = os.path.expanduser(token.strip().strip("'\""))
|
|
1011
1011
|
# `bash -` and a bare `-` read the script from stdin; there is no file.
|
|
1012
1012
|
if not text or text == "-":
|
|
1013
1013
|
return (None, None)
|
|
@@ -1029,6 +1029,48 @@ def resolve_script(token):
|
|
|
1029
1029
|
return (None, "a script that does not exist or cannot be read")
|
|
1030
1030
|
|
|
1031
1031
|
|
|
1032
|
+
SHELL_COMMAND_STRING = object()
|
|
1033
|
+
SHELL_STDIN = object()
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def shell_execution_operand(args):
|
|
1037
|
+
"""The file a shell runs, or a sentinel for `-c` and stdin modes.
|
|
1038
|
+
|
|
1039
|
+
`shell_script_operand` intentionally collapses both cases to None for the
|
|
1040
|
+
nested-command scanner. Reach cannot: `bash -c '...'` has already exposed
|
|
1041
|
+
its text, while `bash -x`, `bash -e`, and `bash --` still await a script on
|
|
1042
|
+
stdin and must remain linked to a following `< file` separator.
|
|
1043
|
+
"""
|
|
1044
|
+
index = 0
|
|
1045
|
+
opaque_option_seen = False
|
|
1046
|
+
while index < len(args):
|
|
1047
|
+
token = args[index]
|
|
1048
|
+
if token == "--":
|
|
1049
|
+
index += 1
|
|
1050
|
+
break
|
|
1051
|
+
if not token.startswith(("-", "+")):
|
|
1052
|
+
if not opaque_option_seen:
|
|
1053
|
+
break
|
|
1054
|
+
index += 1
|
|
1055
|
+
opaque_option_seen = False
|
|
1056
|
+
continue
|
|
1057
|
+
if token in SHELL_OPTIONS_SEPARATE_VALUE:
|
|
1058
|
+
index += 2
|
|
1059
|
+
continue
|
|
1060
|
+
if token.startswith(SHELL_OPTIONS_INLINE_VALUE):
|
|
1061
|
+
index += 1
|
|
1062
|
+
continue
|
|
1063
|
+
if token.startswith("--"):
|
|
1064
|
+
if token not in SHELL_LONG_OPTIONS_NO_VALUE:
|
|
1065
|
+
opaque_option_seen = True
|
|
1066
|
+
index += 1
|
|
1067
|
+
continue
|
|
1068
|
+
if "c" in token[1:]:
|
|
1069
|
+
return SHELL_COMMAND_STRING
|
|
1070
|
+
index += 1
|
|
1071
|
+
return args[index] if index < len(args) else SHELL_STDIN
|
|
1072
|
+
|
|
1073
|
+
|
|
1032
1074
|
def executed_scripts(tokens):
|
|
1033
1075
|
"""The files a command line will RUN, and the executions it cannot follow.
|
|
1034
1076
|
|
|
@@ -1063,9 +1105,12 @@ def executed_scripts(tokens):
|
|
|
1063
1105
|
if name in SOURCE_BUILTINS:
|
|
1064
1106
|
operand = arguments[0] if arguments else None
|
|
1065
1107
|
elif name in SHELL_PROGRAMS:
|
|
1066
|
-
operand =
|
|
1067
|
-
if operand is
|
|
1108
|
+
operand = shell_execution_operand(arguments)
|
|
1109
|
+
if operand is SHELL_STDIN:
|
|
1068
1110
|
previous_shell_awaiting_stdin = True
|
|
1111
|
+
continue
|
|
1112
|
+
if operand is SHELL_COMMAND_STRING:
|
|
1113
|
+
continue
|
|
1069
1114
|
else:
|
|
1070
1115
|
continue
|
|
1071
1116
|
if operand is None:
|
|
@@ -460,6 +460,50 @@ passed off as success.
|
|
|
460
460
|
`blocked:unreviewed`. This fallback lives inside the gated section above —
|
|
461
461
|
with `auto_merge=false` it never fires; the PR remains open awaiting a human.
|
|
462
462
|
|
|
463
|
+
### The mergeability gate — never arm a PR that cannot merge
|
|
464
|
+
|
|
465
|
+
The arm gate above asks *did the review context do work?* This one asks a
|
|
466
|
+
different question — *is there a verdict standing in the way?* — and they are
|
|
467
|
+
not the same. A `CHANGES_REQUESTED` review unambiguously **did work**, so it
|
|
468
|
+
sails through the arm gate and the latch goes on over the top of it.
|
|
469
|
+
|
|
470
|
+
**Read `reviewDecision` explicitly. Never infer it from a check count.**
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
gh pr view <pr> --json reviewDecision,reviewThreads \
|
|
474
|
+
--jq '{decision: .reviewDecision, unresolved: [.reviewThreads[]? | select(.isResolved == false)] | length}'
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
`reviewDecision` is **not part of `statusCheckRollup`**. That is the whole reason
|
|
478
|
+
this stayed invisible: a failing-check count reads **zero** on a PR that can
|
|
479
|
+
never merge, and the checks tab is entirely green. Any rule keyed on "no red
|
|
480
|
+
checks" is satisfied by exactly the PR this gate exists to catch (#3720).
|
|
481
|
+
|
|
482
|
+
**Do not arm while `reviewDecision == CHANGES_REQUESTED`.** Arming is a claim
|
|
483
|
+
that this PR will merge, and that claim is false here. Clear the verdict through
|
|
484
|
+
step (e) first — which discriminates a live objection from a stranded one by
|
|
485
|
+
unresolved thread count, not by `reviewDecision` — and arm afterwards.
|
|
486
|
+
|
|
487
|
+
**Two ruleset settings decide whether a standing verdict ever clears itself, and
|
|
488
|
+
in this repository both say no.** Verify them for the repo you are in rather
|
|
489
|
+
than assuming, with `gh api repos/<owner>/<repo>/rulesets`:
|
|
490
|
+
|
|
491
|
+
- `dismiss_stale_reviews_on_push: false` — the review stays attached to the
|
|
492
|
+
commit it was made on, so **pushing the fix never clears it**. The intuition
|
|
493
|
+
that a new head resets the verdict is wrong here.
|
|
494
|
+
- `required_approving_review_count: 0` — no approval is required to merge, so a
|
|
495
|
+
standing `CHANGES_REQUESTED` is not holding a slot for a review that would
|
|
496
|
+
otherwise arrive. Nothing is scheduled to clear it.
|
|
497
|
+
|
|
498
|
+
Together those mean the block is **indefinite, not slow**. Waiting is not a
|
|
499
|
+
strategy; something has to act.
|
|
500
|
+
|
|
501
|
+
**Unresolved threads block independently of the verdict.** This repository sets
|
|
502
|
+
`required_review_thread_resolution: true`, so a PR can have a clear
|
|
503
|
+
`reviewDecision` and still be unmergeable on threads alone — a second blocker
|
|
504
|
+
that is equally absent from the check rollup. Read both, and name whichever one
|
|
505
|
+
is standing; reporting the wrong one sends the operator to the wrong place.
|
|
506
|
+
|
|
463
507
|
## 2. The watch loop
|
|
464
508
|
|
|
465
509
|
Poll the live state each iteration:
|
|
@@ -1078,6 +1122,14 @@ Loop until one of:
|
|
|
1078
1122
|
the one thing nobody would then do is investigate it. Usually a missing
|
|
1079
1123
|
`issues: read` / label-read permission, which is fixable and invisible from
|
|
1080
1124
|
the merge outcome alone.
|
|
1125
|
+
- **`blocked:armed-unmergeable`** — the PR is OPEN, auto-merge is ON, every check
|
|
1126
|
+
is green, and it still cannot merge: `reviewDecision == CHANGES_REQUESTED`, or
|
|
1127
|
+
unresolved review threads under `required_review_thread_resolution`. Report the
|
|
1128
|
+
PR URL, **which** of the two is standing, and that it will not clear itself.
|
|
1129
|
+
For example: *"PR #123 looks ready — every check is green and auto-merge is on
|
|
1130
|
+
— but a review asked for changes and that is still on record, so it will sit
|
|
1131
|
+
there forever. Someone needs to clear that review or resolve the open
|
|
1132
|
+
comments."* Never report this PR as merging.
|
|
1081
1133
|
- **`CLOSED`** → report (PR was closed without merge).
|
|
1082
1134
|
- **Hard block needing a human**: an unresolvable conflict, a failing check that
|
|
1083
1135
|
needs design input, or genuine unresolved human objection (not a bot gate). Stop
|
|
@@ -1087,6 +1139,44 @@ Loop until one of:
|
|
|
1087
1139
|
adversarial-review substitute is posted (step d); it never extends to any
|
|
1088
1140
|
other gate.
|
|
1089
1141
|
|
|
1142
|
+
### Before you stop: never leave a PR armed and unmergeable in silence
|
|
1143
|
+
|
|
1144
|
+
**This applies at every exit that leaves the PR OPEN** — not only the terminal
|
|
1145
|
+
states above, but any point where this run concludes it is finished, hands back,
|
|
1146
|
+
or gives up. Re-read, from the live PR, immediately before reporting:
|
|
1147
|
+
|
|
1148
|
+
```bash
|
|
1149
|
+
gh pr view <pr> --json state,autoMergeRequest,reviewDecision,reviewThreads \
|
|
1150
|
+
--jq '{state, armed: (.autoMergeRequest != null), decision: .reviewDecision,
|
|
1151
|
+
unresolved: [.reviewThreads[]? | select(.isResolved == false)] | length}'
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
If `state == OPEN` and `armed` and either `decision == "CHANGES_REQUESTED"` or
|
|
1155
|
+
`unresolved > 0`, the outcome is **`blocked:armed-unmergeable`** — never a
|
|
1156
|
+
success, and never silence.
|
|
1157
|
+
|
|
1158
|
+
**Why this exists rather than trusting the loop.** While the loop runs this is
|
|
1159
|
+
already covered: the watch poll reads `reviewDecision` and step (e) handles it.
|
|
1160
|
+
The failure is a run that **stops** while the PR is armed and blocked — the
|
|
1161
|
+
session ends, or the agent decides the work is done because arming looked like
|
|
1162
|
+
completion. Measured (#3720): three PRs were left armed, green and permanently
|
|
1163
|
+
unmergeable, and every one was found only because a person queried
|
|
1164
|
+
`reviewDecision` by hand. An agent that arms auto-merge and reports "armed, will
|
|
1165
|
+
merge" is reporting something false and has no reason to know it.
|
|
1166
|
+
|
|
1167
|
+
**Arming is not completion.** It is a claim about the future that this check is
|
|
1168
|
+
what makes honest. The claim costs nothing when true — one read, and the report
|
|
1169
|
+
says merging — and when false it is the only thing standing between a parked PR
|
|
1170
|
+
and nobody noticing for hours.
|
|
1171
|
+
|
|
1172
|
+
**Read the two fields; do not substitute a check count.** Zero failing checks is
|
|
1173
|
+
the symptom, not the signal — that is precisely why this class was invisible.
|
|
1174
|
+
|
|
1175
|
+
In `on_blocker=report` mode the same condition returns the existing
|
|
1176
|
+
`blocked:changes_requested` classification rather than this terminal state: that
|
|
1177
|
+
mode returns a blocker for the caller to act on, and does not drive. The two
|
|
1178
|
+
names describe the same PR seen from the two modes.
|
|
1179
|
+
|
|
1090
1180
|
At every terminal state, release the babysitter lease
|
|
1091
1181
|
(`gh pr edit <pr> --remove-label "lisa:babysitter-on-duty"`) so the CI
|
|
1092
1182
|
auto-fix workflow can take over as fixer of last resort if the branch goes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.34.
|
|
3
|
+
"version": "4.34.17",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.34.
|
|
3
|
+
"version": "4.34.17",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.34.
|
|
3
|
+
"version": "4.34.17",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.34.
|
|
3
|
+
"version": "4.34.17",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.34.
|
|
3
|
+
"version": "4.34.17",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|