@codyswann/lisa 4.34.8 → 4.34.9
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/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 +3 -2
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- 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 +141 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-git-submit-pr/SKILL.md +2 -0
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
- package/plugins/lisa/skills/lisa-git-submit-pr/SKILL.md +2 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
- package/plugins/lisa-agy/skills/lisa-git-submit-pr/SKILL.md +2 -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/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
- package/plugins/lisa-copilot/skills/lisa-git-submit-pr/SKILL.md +2 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
- package/plugins/lisa-cursor/skills/lisa-git-submit-pr/SKILL.md +2 -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/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
- package/plugins/src/base/skills/lisa-git-submit-pr/SKILL.md +2 -0
package/package.json
CHANGED
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
"zod-validation-error": "^4.0.0"
|
|
167
167
|
},
|
|
168
168
|
"name": "@codyswann/lisa",
|
|
169
|
-
"version": "4.34.
|
|
169
|
+
"version": "4.34.9",
|
|
170
170
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
171
171
|
"main": "dist/index.js",
|
|
172
172
|
"exports": {
|
|
@@ -314,7 +314,7 @@
|
|
|
314
314
|
"test": "tests"
|
|
315
315
|
},
|
|
316
316
|
"types": "./dist/index.d.ts",
|
|
317
|
-
"lisaReleaseCommit": "
|
|
318
|
-
"gitHead": "
|
|
319
|
-
"lisaReleaseTag": "v4.34.
|
|
317
|
+
"lisaReleaseCommit": "b4e9e9b943eecf9730d9ed65be780ceabe6f08c9",
|
|
318
|
+
"gitHead": "b4e9e9b943eecf9730d9ed65be780ceabe6f08c9",
|
|
319
|
+
"lisaReleaseTag": "v4.34.9"
|
|
320
320
|
}
|
|
@@ -41,6 +41,11 @@ merges" loop. Other skills delegate here instead of re-implementing it. Runs
|
|
|
41
41
|
green, open, un-merged PR is the *success* outcome of this mode, not a hang.
|
|
42
42
|
Used by learning-persistence flows whose low-confidence PRs must wait for a
|
|
43
43
|
human (`lisa-persist-learning`).
|
|
44
|
+
|
|
45
|
+
**`auto_merge` is caller-time and nothing re-reads it.** That is what the hold
|
|
46
|
+
label (section 0) exists for: the same behaviour, raisable by anyone at any
|
|
47
|
+
point while the loop runs. Both routes land in `awaiting-human`; they differ
|
|
48
|
+
only in who can take them and when.
|
|
44
49
|
- `on_blocker=<fix|report>` — what to do when a blocker needs code or review work.
|
|
45
50
|
Default `fix`.
|
|
46
51
|
- **`fix`** (the full loop): resolve conflicts, fix failing checks, address +
|
|
@@ -97,9 +102,115 @@ gh pr edit <pr> --add-label "lisa:babysitter-on-duty"
|
|
|
97
102
|
closed, or a hard block handed to a human. A crashed session that never
|
|
98
103
|
releases is why the TTL exists; do not rely on it as the normal release path.
|
|
99
104
|
|
|
105
|
+
### The hold gate — a stop anyone can raise
|
|
106
|
+
|
|
107
|
+
Section 0 owns this skill's two PR labels: the lease it **writes** to declare
|
|
108
|
+
ownership, and the hold it **reads** to be told to stop. Same mechanism, pointed
|
|
109
|
+
at different things — a lease is a claim, a hold is a decision.
|
|
110
|
+
|
|
111
|
+
**The label** is `lisa:hold` by default, overridable per project at
|
|
112
|
+
`github.labels.merge.hold`, resolved local-over-global like every other
|
|
113
|
+
configured name. Resolve it ONCE at startup and reuse the resolved string; a
|
|
114
|
+
name re-resolved each iteration can change identity mid-loop for no reason a
|
|
115
|
+
reader could reconstruct afterwards.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
HOLD_LABEL=$(
|
|
119
|
+
jq -r 'first(.github.labels.merge.hold | strings)' \
|
|
120
|
+
.lisa.config.local.json .lisa.config.json 2>/dev/null | head -n1
|
|
121
|
+
)
|
|
122
|
+
HOLD_LABEL="${HOLD_LABEL:-lisa:hold}"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**What it means:** exactly `auto_merge=false`, for as long as it is present. Not
|
|
126
|
+
a new concept and not a new terminal state — the same disarm, the same
|
|
127
|
+
fail-closed re-read, the same `awaiting-human` landing. The only new surface is
|
|
128
|
+
one label read.
|
|
129
|
+
|
|
130
|
+
**Why it exists.** `auto_merge` is decided by the caller at invocation, and
|
|
131
|
+
nothing re-reads it, so the only person who can stop the merge is the person who
|
|
132
|
+
started it. Anyone else — a reviewer who opens the PR while the loop is driving
|
|
133
|
+
it, someone holding a PR another session armed — has no lever and loses the
|
|
134
|
+
race. Measured (#3558): four explicit `gh pr merge --disable-auto` calls were
|
|
135
|
+
each followed by a re-arm 7–25 seconds later, and the PR merged two seconds
|
|
136
|
+
after its final check went green. **The hold did not fail; it lost a race.** A
|
|
137
|
+
signal the loop itself reads cannot be raced, because the loop is what reads it.
|
|
138
|
+
|
|
139
|
+
#### Evaluate it in two places, and re-read it every iteration
|
|
140
|
+
|
|
141
|
+
- **Before arming** (section 1) — a PR already carrying the label is never armed.
|
|
142
|
+
- **On every watch-loop iteration** (section 2), before acting on any blocker.
|
|
143
|
+
|
|
144
|
+
Re-reading is the entire point. A check that runs once at startup is a
|
|
145
|
+
caller-time signal wearing a loop's clothing and fixes nothing, because the case
|
|
146
|
+
that matters is a label applied **after** the loop is already running. If you
|
|
147
|
+
are changing this and it is convenient to read the label once, you have
|
|
148
|
+
reintroduced the defect.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
gh pr view <pr> --json labels --jq "[.labels[].name] | index(\"$HOLD_LABEL\") != null"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Match the resolved name exactly — never a prefix.** `lisa:babysitter-on-duty`
|
|
155
|
+
is a label this skill applies to itself and shares the `lisa:` prefix; a
|
|
156
|
+
prefix match would make the loop stop on its own lease, which is a deadlock
|
|
157
|
+
that looks like a human decision.
|
|
158
|
+
|
|
159
|
+
#### When held
|
|
160
|
+
|
|
161
|
+
1. Run the existing `auto_merge=false` disarm (section 1) — disable the latch
|
|
162
|
+
once and prove with the fail-closed re-read that it took.
|
|
163
|
+
2. Return `awaiting-human:held` (section 4).
|
|
164
|
+
3. **Do not remove the label.** Removing a human's signal is unrecoverable from
|
|
165
|
+
inside the loop; leaving a stale one costs a cycle and is visible. Only
|
|
166
|
+
whoever applied it takes it off, and a later invocation then resumes normally.
|
|
167
|
+
|
|
168
|
+
Do not keep polling while held. Held is terminal for this run, not a pause to
|
|
169
|
+
wait out — the loop has been told a human is looking, and continuing to drive is
|
|
170
|
+
the behaviour the label exists to stop.
|
|
171
|
+
|
|
172
|
+
**The disarm is once and terminal: never re-arm the latch afterwards, and never
|
|
173
|
+
resume driving in the same run.** This is what keeps a hold compatible with the
|
|
174
|
+
armed-across-fix-pushes rule in section 1, which otherwise forbids disarming on
|
|
175
|
+
the `auto_merge=true` path. That rule exists to prevent a *race* — disarm, then
|
|
176
|
+
re-arm, repeatedly, while still driving — and a hold does neither half of it: it
|
|
177
|
+
turns the latch off exactly once and then stops. A hold that re-armed, or that
|
|
178
|
+
kept driving afterwards, would be the race that rule names, wearing a label.
|
|
179
|
+
|
|
180
|
+
#### When the read fails
|
|
181
|
+
|
|
182
|
+
**Fail toward hold.** The asymmetry decides it: a false hold costs one cycle and
|
|
183
|
+
is visible in a terminal report someone can act on, while a false no-hold merges
|
|
184
|
+
past a human's objection and cannot be undone. Where the two errors are
|
|
185
|
+
unequal, take the recoverable one.
|
|
186
|
+
|
|
187
|
+
But a read that keeps failing must not become a silent deadlock, so the failure
|
|
188
|
+
is **reported as its own outcome** rather than dressed up as a decision:
|
|
189
|
+
|
|
190
|
+
- retry the read once;
|
|
191
|
+
- if it still fails, disarm as above and return **`awaiting-human:hold-unknown`**,
|
|
192
|
+
naming the error.
|
|
193
|
+
|
|
194
|
+
`awaiting-human:held` and `awaiting-human:hold-unknown` are different facts and
|
|
195
|
+
an operator needs to know which: the first says *a human stopped this*, the
|
|
196
|
+
second says *I could not tell whether a human stopped this.* Collapsing them
|
|
197
|
+
would hide a broken permission behind a human decision, and nobody would ever
|
|
198
|
+
look.
|
|
199
|
+
|
|
200
|
+
**A PR with no hold label is driven exactly as it is today.** This gate adds a
|
|
201
|
+
stop; it does not add a block. If the label is absent and the read succeeded,
|
|
202
|
+
nothing about the run changes.
|
|
203
|
+
|
|
100
204
|
## 1. Enable auto-merge
|
|
101
205
|
|
|
102
|
-
**Gate:
|
|
206
|
+
**Gate: the hold label is absent (section 0's hold gate), and `auto_merge=true`
|
|
207
|
+
(the default).** Evaluate the hold gate BEFORE anything in this section,
|
|
208
|
+
including the arm gate below and the direct-merge capability fallback: a held PR
|
|
209
|
+
is not armed, not merged directly, and not driven — it disarms once and returns
|
|
210
|
+
`awaiting-human:held`. Checking after arming would authorise the merge the label
|
|
211
|
+
exists to prevent, which is the same ordering error the arm gate itself fixes.
|
|
212
|
+
|
|
213
|
+
When `auto_merge=false`,
|
|
103
214
|
skip the enable step and its capability fallback — do not enable auto-merge,
|
|
104
215
|
and do **not** use the capability fallback below: on a repo that disallows
|
|
105
216
|
auto-merge, an `auto_merge=false` PR must stay OPEN for human triage, never be
|
|
@@ -357,6 +468,12 @@ Poll the live state each iteration:
|
|
|
357
468
|
gh pr view <pr> --json state,mergeStateStatus,mergeable,reviewDecision,statusCheckRollup,headRefName,baseRefName
|
|
358
469
|
```
|
|
359
470
|
|
|
471
|
+
**Re-read the hold label on every iteration, before handling any blocker**
|
|
472
|
+
(section 0's hold gate). It is polled separately from the line above rather than
|
|
473
|
+
folded into it, because `labels` is not part of the blocker state: the hold is a
|
|
474
|
+
question about whether to keep going at all, asked before the blocker questions,
|
|
475
|
+
and a run that answers it once at startup has not implemented it.
|
|
476
|
+
|
|
360
477
|
Handle every blocker class; after any fix, re-poll and continue. Do not stop while
|
|
361
478
|
the PR is still open and progress is possible. On each iteration, refresh the
|
|
362
479
|
babysitter lease if its last stamp is older than ~30 minutes (section 0).
|
|
@@ -393,6 +510,13 @@ and return per the input contract above. That includes (f): adjudicating a pendi
|
|
|
393
510
|
auto-fix PR (merging, closing, or deleting its branch) is destructive work, not
|
|
394
511
|
diagnosis — return its classification (`blocked:pending-auto-fix`) instead.
|
|
395
512
|
|
|
513
|
+
**The hold gate binds in report mode too**, and it binds first: a held PR
|
|
514
|
+
returns `awaiting-human:held` without arming, without the mechanical step (a)
|
|
515
|
+
nudge, and without an `update-branch`. Hold is not a blocker classification and
|
|
516
|
+
never returns as one — `blocked:*` says *this PR cannot proceed yet*, while a
|
|
517
|
+
hold says *someone asked me not to proceed*, and reporting the second as the
|
|
518
|
+
first would file a human's decision as a defect for something else to clear.
|
|
519
|
+
|
|
396
520
|
The arm gate binds in report mode too, and this is the mode where forgetting it
|
|
397
521
|
does the most damage: a diagnose-only run that arms the latch has not diagnosed
|
|
398
522
|
anything, it has authorised a merge. Under a vacuous review context, report mode
|
|
@@ -938,6 +1062,22 @@ Loop until one of:
|
|
|
938
1062
|
merge but nobody reviewed it: the review tool says it skips public
|
|
939
1063
|
repositories, and that will not change on its own. Merge it yourself or ask
|
|
940
1064
|
for a human review."*
|
|
1065
|
+
- **`awaiting-human:held`** → success, and the only terminal state a person can
|
|
1066
|
+
cause on purpose from outside the run. The hold label (section 0) was present,
|
|
1067
|
+
the latch was disarmed once and the re-read proved it, and the label was left
|
|
1068
|
+
in place. Report the PR URL, the resolved label name, and that removing the
|
|
1069
|
+
label lets a later invocation resume — for example: *"PR #123 is on hold:
|
|
1070
|
+
someone added the `lisa:hold` label, so I stopped instead of merging it.
|
|
1071
|
+
Remove that label when you want it to continue."* Not a stall and not a
|
|
1072
|
+
failure: a human asked for the PR to stop, and it stopped.
|
|
1073
|
+
- **`awaiting-human:hold-unknown`** → the hold label could not be read (twice),
|
|
1074
|
+
so whether a human asked for a stop is **unknown**. The latch is disarmed and
|
|
1075
|
+
the PR sits OPEN, on the fail-toward-hold rule in section 0. Report the read
|
|
1076
|
+
error verbatim. This is deliberately NOT reported as `awaiting-human:held`:
|
|
1077
|
+
that would file a broken permission or a rate limit as a human decision, and
|
|
1078
|
+
the one thing nobody would then do is investigate it. Usually a missing
|
|
1079
|
+
`issues: read` / label-read permission, which is fixable and invisible from
|
|
1080
|
+
the merge outcome alone.
|
|
941
1081
|
- **`CLOSED`** → report (PR was closed without merge).
|
|
942
1082
|
- **Hard block needing a human**: an unresolvable conflict, a failing check that
|
|
943
1083
|
needs design input, or genuine unresolved human objection (not a bot gate). Stop
|
|
@@ -48,6 +48,8 @@ Recognized optional hints:
|
|
|
48
48
|
|
|
49
49
|
This delegation is now load-bearing rather than a convenience: it is the only path that arms auto-merge, so skipping it leaves the PR open rather than quietly merging it. Reporting submission as complete without invoking it is a failed submit, not a partial one — say so instead of implying the PR is on its way to merged.
|
|
50
50
|
|
|
51
|
+
**The hold label needs no check here, and adding one would be dead code.** `drive-pr-to-merge` evaluates its hold gate before it arms anything, so a PR created with the label already on it is never armed — by the delegation, not by a second check in this skill. That guarantee used to require one: while step 5 armed the latch at creation, a hold that covered only the loop left the creation-time arm wide open, which is why CodySwannGT/lisa#3558 asked for a check in both places. Step 5 no longer arms, so there is nothing here for a label to gate. If you are adding one because the ticket says to, re-read step 5 first.
|
|
52
|
+
|
|
51
53
|
### Native Development Linkage
|
|
52
54
|
|
|
53
55
|
Add provider-appropriate linkage to the PR title and/or body without changing the status lifecycle:
|
|
@@ -41,6 +41,11 @@ merges" loop. Other skills delegate here instead of re-implementing it. Runs
|
|
|
41
41
|
green, open, un-merged PR is the *success* outcome of this mode, not a hang.
|
|
42
42
|
Used by learning-persistence flows whose low-confidence PRs must wait for a
|
|
43
43
|
human (`lisa-persist-learning`).
|
|
44
|
+
|
|
45
|
+
**`auto_merge` is caller-time and nothing re-reads it.** That is what the hold
|
|
46
|
+
label (section 0) exists for: the same behaviour, raisable by anyone at any
|
|
47
|
+
point while the loop runs. Both routes land in `awaiting-human`; they differ
|
|
48
|
+
only in who can take them and when.
|
|
44
49
|
- `on_blocker=<fix|report>` — what to do when a blocker needs code or review work.
|
|
45
50
|
Default `fix`.
|
|
46
51
|
- **`fix`** (the full loop): resolve conflicts, fix failing checks, address +
|
|
@@ -97,9 +102,115 @@ gh pr edit <pr> --add-label "lisa:babysitter-on-duty"
|
|
|
97
102
|
closed, or a hard block handed to a human. A crashed session that never
|
|
98
103
|
releases is why the TTL exists; do not rely on it as the normal release path.
|
|
99
104
|
|
|
105
|
+
### The hold gate — a stop anyone can raise
|
|
106
|
+
|
|
107
|
+
Section 0 owns this skill's two PR labels: the lease it **writes** to declare
|
|
108
|
+
ownership, and the hold it **reads** to be told to stop. Same mechanism, pointed
|
|
109
|
+
at different things — a lease is a claim, a hold is a decision.
|
|
110
|
+
|
|
111
|
+
**The label** is `lisa:hold` by default, overridable per project at
|
|
112
|
+
`github.labels.merge.hold`, resolved local-over-global like every other
|
|
113
|
+
configured name. Resolve it ONCE at startup and reuse the resolved string; a
|
|
114
|
+
name re-resolved each iteration can change identity mid-loop for no reason a
|
|
115
|
+
reader could reconstruct afterwards.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
HOLD_LABEL=$(
|
|
119
|
+
jq -r 'first(.github.labels.merge.hold | strings)' \
|
|
120
|
+
.lisa.config.local.json .lisa.config.json 2>/dev/null | head -n1
|
|
121
|
+
)
|
|
122
|
+
HOLD_LABEL="${HOLD_LABEL:-lisa:hold}"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**What it means:** exactly `auto_merge=false`, for as long as it is present. Not
|
|
126
|
+
a new concept and not a new terminal state — the same disarm, the same
|
|
127
|
+
fail-closed re-read, the same `awaiting-human` landing. The only new surface is
|
|
128
|
+
one label read.
|
|
129
|
+
|
|
130
|
+
**Why it exists.** `auto_merge` is decided by the caller at invocation, and
|
|
131
|
+
nothing re-reads it, so the only person who can stop the merge is the person who
|
|
132
|
+
started it. Anyone else — a reviewer who opens the PR while the loop is driving
|
|
133
|
+
it, someone holding a PR another session armed — has no lever and loses the
|
|
134
|
+
race. Measured (#3558): four explicit `gh pr merge --disable-auto` calls were
|
|
135
|
+
each followed by a re-arm 7–25 seconds later, and the PR merged two seconds
|
|
136
|
+
after its final check went green. **The hold did not fail; it lost a race.** A
|
|
137
|
+
signal the loop itself reads cannot be raced, because the loop is what reads it.
|
|
138
|
+
|
|
139
|
+
#### Evaluate it in two places, and re-read it every iteration
|
|
140
|
+
|
|
141
|
+
- **Before arming** (section 1) — a PR already carrying the label is never armed.
|
|
142
|
+
- **On every watch-loop iteration** (section 2), before acting on any blocker.
|
|
143
|
+
|
|
144
|
+
Re-reading is the entire point. A check that runs once at startup is a
|
|
145
|
+
caller-time signal wearing a loop's clothing and fixes nothing, because the case
|
|
146
|
+
that matters is a label applied **after** the loop is already running. If you
|
|
147
|
+
are changing this and it is convenient to read the label once, you have
|
|
148
|
+
reintroduced the defect.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
gh pr view <pr> --json labels --jq "[.labels[].name] | index(\"$HOLD_LABEL\") != null"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Match the resolved name exactly — never a prefix.** `lisa:babysitter-on-duty`
|
|
155
|
+
is a label this skill applies to itself and shares the `lisa:` prefix; a
|
|
156
|
+
prefix match would make the loop stop on its own lease, which is a deadlock
|
|
157
|
+
that looks like a human decision.
|
|
158
|
+
|
|
159
|
+
#### When held
|
|
160
|
+
|
|
161
|
+
1. Run the existing `auto_merge=false` disarm (section 1) — disable the latch
|
|
162
|
+
once and prove with the fail-closed re-read that it took.
|
|
163
|
+
2. Return `awaiting-human:held` (section 4).
|
|
164
|
+
3. **Do not remove the label.** Removing a human's signal is unrecoverable from
|
|
165
|
+
inside the loop; leaving a stale one costs a cycle and is visible. Only
|
|
166
|
+
whoever applied it takes it off, and a later invocation then resumes normally.
|
|
167
|
+
|
|
168
|
+
Do not keep polling while held. Held is terminal for this run, not a pause to
|
|
169
|
+
wait out — the loop has been told a human is looking, and continuing to drive is
|
|
170
|
+
the behaviour the label exists to stop.
|
|
171
|
+
|
|
172
|
+
**The disarm is once and terminal: never re-arm the latch afterwards, and never
|
|
173
|
+
resume driving in the same run.** This is what keeps a hold compatible with the
|
|
174
|
+
armed-across-fix-pushes rule in section 1, which otherwise forbids disarming on
|
|
175
|
+
the `auto_merge=true` path. That rule exists to prevent a *race* — disarm, then
|
|
176
|
+
re-arm, repeatedly, while still driving — and a hold does neither half of it: it
|
|
177
|
+
turns the latch off exactly once and then stops. A hold that re-armed, or that
|
|
178
|
+
kept driving afterwards, would be the race that rule names, wearing a label.
|
|
179
|
+
|
|
180
|
+
#### When the read fails
|
|
181
|
+
|
|
182
|
+
**Fail toward hold.** The asymmetry decides it: a false hold costs one cycle and
|
|
183
|
+
is visible in a terminal report someone can act on, while a false no-hold merges
|
|
184
|
+
past a human's objection and cannot be undone. Where the two errors are
|
|
185
|
+
unequal, take the recoverable one.
|
|
186
|
+
|
|
187
|
+
But a read that keeps failing must not become a silent deadlock, so the failure
|
|
188
|
+
is **reported as its own outcome** rather than dressed up as a decision:
|
|
189
|
+
|
|
190
|
+
- retry the read once;
|
|
191
|
+
- if it still fails, disarm as above and return **`awaiting-human:hold-unknown`**,
|
|
192
|
+
naming the error.
|
|
193
|
+
|
|
194
|
+
`awaiting-human:held` and `awaiting-human:hold-unknown` are different facts and
|
|
195
|
+
an operator needs to know which: the first says *a human stopped this*, the
|
|
196
|
+
second says *I could not tell whether a human stopped this.* Collapsing them
|
|
197
|
+
would hide a broken permission behind a human decision, and nobody would ever
|
|
198
|
+
look.
|
|
199
|
+
|
|
200
|
+
**A PR with no hold label is driven exactly as it is today.** This gate adds a
|
|
201
|
+
stop; it does not add a block. If the label is absent and the read succeeded,
|
|
202
|
+
nothing about the run changes.
|
|
203
|
+
|
|
100
204
|
## 1. Enable auto-merge
|
|
101
205
|
|
|
102
|
-
**Gate:
|
|
206
|
+
**Gate: the hold label is absent (section 0's hold gate), and `auto_merge=true`
|
|
207
|
+
(the default).** Evaluate the hold gate BEFORE anything in this section,
|
|
208
|
+
including the arm gate below and the direct-merge capability fallback: a held PR
|
|
209
|
+
is not armed, not merged directly, and not driven — it disarms once and returns
|
|
210
|
+
`awaiting-human:held`. Checking after arming would authorise the merge the label
|
|
211
|
+
exists to prevent, which is the same ordering error the arm gate itself fixes.
|
|
212
|
+
|
|
213
|
+
When `auto_merge=false`,
|
|
103
214
|
skip the enable step and its capability fallback — do not enable auto-merge,
|
|
104
215
|
and do **not** use the capability fallback below: on a repo that disallows
|
|
105
216
|
auto-merge, an `auto_merge=false` PR must stay OPEN for human triage, never be
|
|
@@ -357,6 +468,12 @@ Poll the live state each iteration:
|
|
|
357
468
|
gh pr view <pr> --json state,mergeStateStatus,mergeable,reviewDecision,statusCheckRollup,headRefName,baseRefName
|
|
358
469
|
```
|
|
359
470
|
|
|
471
|
+
**Re-read the hold label on every iteration, before handling any blocker**
|
|
472
|
+
(section 0's hold gate). It is polled separately from the line above rather than
|
|
473
|
+
folded into it, because `labels` is not part of the blocker state: the hold is a
|
|
474
|
+
question about whether to keep going at all, asked before the blocker questions,
|
|
475
|
+
and a run that answers it once at startup has not implemented it.
|
|
476
|
+
|
|
360
477
|
Handle every blocker class; after any fix, re-poll and continue. Do not stop while
|
|
361
478
|
the PR is still open and progress is possible. On each iteration, refresh the
|
|
362
479
|
babysitter lease if its last stamp is older than ~30 minutes (section 0).
|
|
@@ -393,6 +510,13 @@ and return per the input contract above. That includes (f): adjudicating a pendi
|
|
|
393
510
|
auto-fix PR (merging, closing, or deleting its branch) is destructive work, not
|
|
394
511
|
diagnosis — return its classification (`blocked:pending-auto-fix`) instead.
|
|
395
512
|
|
|
513
|
+
**The hold gate binds in report mode too**, and it binds first: a held PR
|
|
514
|
+
returns `awaiting-human:held` without arming, without the mechanical step (a)
|
|
515
|
+
nudge, and without an `update-branch`. Hold is not a blocker classification and
|
|
516
|
+
never returns as one — `blocked:*` says *this PR cannot proceed yet*, while a
|
|
517
|
+
hold says *someone asked me not to proceed*, and reporting the second as the
|
|
518
|
+
first would file a human's decision as a defect for something else to clear.
|
|
519
|
+
|
|
396
520
|
The arm gate binds in report mode too, and this is the mode where forgetting it
|
|
397
521
|
does the most damage: a diagnose-only run that arms the latch has not diagnosed
|
|
398
522
|
anything, it has authorised a merge. Under a vacuous review context, report mode
|
|
@@ -938,6 +1062,22 @@ Loop until one of:
|
|
|
938
1062
|
merge but nobody reviewed it: the review tool says it skips public
|
|
939
1063
|
repositories, and that will not change on its own. Merge it yourself or ask
|
|
940
1064
|
for a human review."*
|
|
1065
|
+
- **`awaiting-human:held`** → success, and the only terminal state a person can
|
|
1066
|
+
cause on purpose from outside the run. The hold label (section 0) was present,
|
|
1067
|
+
the latch was disarmed once and the re-read proved it, and the label was left
|
|
1068
|
+
in place. Report the PR URL, the resolved label name, and that removing the
|
|
1069
|
+
label lets a later invocation resume — for example: *"PR #123 is on hold:
|
|
1070
|
+
someone added the `lisa:hold` label, so I stopped instead of merging it.
|
|
1071
|
+
Remove that label when you want it to continue."* Not a stall and not a
|
|
1072
|
+
failure: a human asked for the PR to stop, and it stopped.
|
|
1073
|
+
- **`awaiting-human:hold-unknown`** → the hold label could not be read (twice),
|
|
1074
|
+
so whether a human asked for a stop is **unknown**. The latch is disarmed and
|
|
1075
|
+
the PR sits OPEN, on the fail-toward-hold rule in section 0. Report the read
|
|
1076
|
+
error verbatim. This is deliberately NOT reported as `awaiting-human:held`:
|
|
1077
|
+
that would file a broken permission or a rate limit as a human decision, and
|
|
1078
|
+
the one thing nobody would then do is investigate it. Usually a missing
|
|
1079
|
+
`issues: read` / label-read permission, which is fixable and invisible from
|
|
1080
|
+
the merge outcome alone.
|
|
941
1081
|
- **`CLOSED`** → report (PR was closed without merge).
|
|
942
1082
|
- **Hard block needing a human**: an unresolvable conflict, a failing check that
|
|
943
1083
|
needs design input, or genuine unresolved human objection (not a bot gate). Stop
|
|
@@ -48,6 +48,8 @@ Recognized optional hints:
|
|
|
48
48
|
|
|
49
49
|
This delegation is now load-bearing rather than a convenience: it is the only path that arms auto-merge, so skipping it leaves the PR open rather than quietly merging it. Reporting submission as complete without invoking it is a failed submit, not a partial one — say so instead of implying the PR is on its way to merged.
|
|
50
50
|
|
|
51
|
+
**The hold label needs no check here, and adding one would be dead code.** `drive-pr-to-merge` evaluates its hold gate before it arms anything, so a PR created with the label already on it is never armed — by the delegation, not by a second check in this skill. That guarantee used to require one: while step 5 armed the latch at creation, a hold that covered only the loop left the creation-time arm wide open, which is why CodySwannGT/lisa#3558 asked for a check in both places. Step 5 no longer arms, so there is nothing here for a label to gate. If you are adding one because the ticket says to, re-read step 5 first.
|
|
52
|
+
|
|
51
53
|
### Native Development Linkage
|
|
52
54
|
|
|
53
55
|
Add provider-appropriate linkage to the PR title and/or body without changing the status lifecycle:
|
|
@@ -41,6 +41,11 @@ merges" loop. Other skills delegate here instead of re-implementing it. Runs
|
|
|
41
41
|
green, open, un-merged PR is the *success* outcome of this mode, not a hang.
|
|
42
42
|
Used by learning-persistence flows whose low-confidence PRs must wait for a
|
|
43
43
|
human (`lisa-persist-learning`).
|
|
44
|
+
|
|
45
|
+
**`auto_merge` is caller-time and nothing re-reads it.** That is what the hold
|
|
46
|
+
label (section 0) exists for: the same behaviour, raisable by anyone at any
|
|
47
|
+
point while the loop runs. Both routes land in `awaiting-human`; they differ
|
|
48
|
+
only in who can take them and when.
|
|
44
49
|
- `on_blocker=<fix|report>` — what to do when a blocker needs code or review work.
|
|
45
50
|
Default `fix`.
|
|
46
51
|
- **`fix`** (the full loop): resolve conflicts, fix failing checks, address +
|
|
@@ -97,9 +102,115 @@ gh pr edit <pr> --add-label "lisa:babysitter-on-duty"
|
|
|
97
102
|
closed, or a hard block handed to a human. A crashed session that never
|
|
98
103
|
releases is why the TTL exists; do not rely on it as the normal release path.
|
|
99
104
|
|
|
105
|
+
### The hold gate — a stop anyone can raise
|
|
106
|
+
|
|
107
|
+
Section 0 owns this skill's two PR labels: the lease it **writes** to declare
|
|
108
|
+
ownership, and the hold it **reads** to be told to stop. Same mechanism, pointed
|
|
109
|
+
at different things — a lease is a claim, a hold is a decision.
|
|
110
|
+
|
|
111
|
+
**The label** is `lisa:hold` by default, overridable per project at
|
|
112
|
+
`github.labels.merge.hold`, resolved local-over-global like every other
|
|
113
|
+
configured name. Resolve it ONCE at startup and reuse the resolved string; a
|
|
114
|
+
name re-resolved each iteration can change identity mid-loop for no reason a
|
|
115
|
+
reader could reconstruct afterwards.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
HOLD_LABEL=$(
|
|
119
|
+
jq -r 'first(.github.labels.merge.hold | strings)' \
|
|
120
|
+
.lisa.config.local.json .lisa.config.json 2>/dev/null | head -n1
|
|
121
|
+
)
|
|
122
|
+
HOLD_LABEL="${HOLD_LABEL:-lisa:hold}"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**What it means:** exactly `auto_merge=false`, for as long as it is present. Not
|
|
126
|
+
a new concept and not a new terminal state — the same disarm, the same
|
|
127
|
+
fail-closed re-read, the same `awaiting-human` landing. The only new surface is
|
|
128
|
+
one label read.
|
|
129
|
+
|
|
130
|
+
**Why it exists.** `auto_merge` is decided by the caller at invocation, and
|
|
131
|
+
nothing re-reads it, so the only person who can stop the merge is the person who
|
|
132
|
+
started it. Anyone else — a reviewer who opens the PR while the loop is driving
|
|
133
|
+
it, someone holding a PR another session armed — has no lever and loses the
|
|
134
|
+
race. Measured (#3558): four explicit `gh pr merge --disable-auto` calls were
|
|
135
|
+
each followed by a re-arm 7–25 seconds later, and the PR merged two seconds
|
|
136
|
+
after its final check went green. **The hold did not fail; it lost a race.** A
|
|
137
|
+
signal the loop itself reads cannot be raced, because the loop is what reads it.
|
|
138
|
+
|
|
139
|
+
#### Evaluate it in two places, and re-read it every iteration
|
|
140
|
+
|
|
141
|
+
- **Before arming** (section 1) — a PR already carrying the label is never armed.
|
|
142
|
+
- **On every watch-loop iteration** (section 2), before acting on any blocker.
|
|
143
|
+
|
|
144
|
+
Re-reading is the entire point. A check that runs once at startup is a
|
|
145
|
+
caller-time signal wearing a loop's clothing and fixes nothing, because the case
|
|
146
|
+
that matters is a label applied **after** the loop is already running. If you
|
|
147
|
+
are changing this and it is convenient to read the label once, you have
|
|
148
|
+
reintroduced the defect.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
gh pr view <pr> --json labels --jq "[.labels[].name] | index(\"$HOLD_LABEL\") != null"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Match the resolved name exactly — never a prefix.** `lisa:babysitter-on-duty`
|
|
155
|
+
is a label this skill applies to itself and shares the `lisa:` prefix; a
|
|
156
|
+
prefix match would make the loop stop on its own lease, which is a deadlock
|
|
157
|
+
that looks like a human decision.
|
|
158
|
+
|
|
159
|
+
#### When held
|
|
160
|
+
|
|
161
|
+
1. Run the existing `auto_merge=false` disarm (section 1) — disable the latch
|
|
162
|
+
once and prove with the fail-closed re-read that it took.
|
|
163
|
+
2. Return `awaiting-human:held` (section 4).
|
|
164
|
+
3. **Do not remove the label.** Removing a human's signal is unrecoverable from
|
|
165
|
+
inside the loop; leaving a stale one costs a cycle and is visible. Only
|
|
166
|
+
whoever applied it takes it off, and a later invocation then resumes normally.
|
|
167
|
+
|
|
168
|
+
Do not keep polling while held. Held is terminal for this run, not a pause to
|
|
169
|
+
wait out — the loop has been told a human is looking, and continuing to drive is
|
|
170
|
+
the behaviour the label exists to stop.
|
|
171
|
+
|
|
172
|
+
**The disarm is once and terminal: never re-arm the latch afterwards, and never
|
|
173
|
+
resume driving in the same run.** This is what keeps a hold compatible with the
|
|
174
|
+
armed-across-fix-pushes rule in section 1, which otherwise forbids disarming on
|
|
175
|
+
the `auto_merge=true` path. That rule exists to prevent a *race* — disarm, then
|
|
176
|
+
re-arm, repeatedly, while still driving — and a hold does neither half of it: it
|
|
177
|
+
turns the latch off exactly once and then stops. A hold that re-armed, or that
|
|
178
|
+
kept driving afterwards, would be the race that rule names, wearing a label.
|
|
179
|
+
|
|
180
|
+
#### When the read fails
|
|
181
|
+
|
|
182
|
+
**Fail toward hold.** The asymmetry decides it: a false hold costs one cycle and
|
|
183
|
+
is visible in a terminal report someone can act on, while a false no-hold merges
|
|
184
|
+
past a human's objection and cannot be undone. Where the two errors are
|
|
185
|
+
unequal, take the recoverable one.
|
|
186
|
+
|
|
187
|
+
But a read that keeps failing must not become a silent deadlock, so the failure
|
|
188
|
+
is **reported as its own outcome** rather than dressed up as a decision:
|
|
189
|
+
|
|
190
|
+
- retry the read once;
|
|
191
|
+
- if it still fails, disarm as above and return **`awaiting-human:hold-unknown`**,
|
|
192
|
+
naming the error.
|
|
193
|
+
|
|
194
|
+
`awaiting-human:held` and `awaiting-human:hold-unknown` are different facts and
|
|
195
|
+
an operator needs to know which: the first says *a human stopped this*, the
|
|
196
|
+
second says *I could not tell whether a human stopped this.* Collapsing them
|
|
197
|
+
would hide a broken permission behind a human decision, and nobody would ever
|
|
198
|
+
look.
|
|
199
|
+
|
|
200
|
+
**A PR with no hold label is driven exactly as it is today.** This gate adds a
|
|
201
|
+
stop; it does not add a block. If the label is absent and the read succeeded,
|
|
202
|
+
nothing about the run changes.
|
|
203
|
+
|
|
100
204
|
## 1. Enable auto-merge
|
|
101
205
|
|
|
102
|
-
**Gate:
|
|
206
|
+
**Gate: the hold label is absent (section 0's hold gate), and `auto_merge=true`
|
|
207
|
+
(the default).** Evaluate the hold gate BEFORE anything in this section,
|
|
208
|
+
including the arm gate below and the direct-merge capability fallback: a held PR
|
|
209
|
+
is not armed, not merged directly, and not driven — it disarms once and returns
|
|
210
|
+
`awaiting-human:held`. Checking after arming would authorise the merge the label
|
|
211
|
+
exists to prevent, which is the same ordering error the arm gate itself fixes.
|
|
212
|
+
|
|
213
|
+
When `auto_merge=false`,
|
|
103
214
|
skip the enable step and its capability fallback — do not enable auto-merge,
|
|
104
215
|
and do **not** use the capability fallback below: on a repo that disallows
|
|
105
216
|
auto-merge, an `auto_merge=false` PR must stay OPEN for human triage, never be
|
|
@@ -357,6 +468,12 @@ Poll the live state each iteration:
|
|
|
357
468
|
gh pr view <pr> --json state,mergeStateStatus,mergeable,reviewDecision,statusCheckRollup,headRefName,baseRefName
|
|
358
469
|
```
|
|
359
470
|
|
|
471
|
+
**Re-read the hold label on every iteration, before handling any blocker**
|
|
472
|
+
(section 0's hold gate). It is polled separately from the line above rather than
|
|
473
|
+
folded into it, because `labels` is not part of the blocker state: the hold is a
|
|
474
|
+
question about whether to keep going at all, asked before the blocker questions,
|
|
475
|
+
and a run that answers it once at startup has not implemented it.
|
|
476
|
+
|
|
360
477
|
Handle every blocker class; after any fix, re-poll and continue. Do not stop while
|
|
361
478
|
the PR is still open and progress is possible. On each iteration, refresh the
|
|
362
479
|
babysitter lease if its last stamp is older than ~30 minutes (section 0).
|
|
@@ -393,6 +510,13 @@ and return per the input contract above. That includes (f): adjudicating a pendi
|
|
|
393
510
|
auto-fix PR (merging, closing, or deleting its branch) is destructive work, not
|
|
394
511
|
diagnosis — return its classification (`blocked:pending-auto-fix`) instead.
|
|
395
512
|
|
|
513
|
+
**The hold gate binds in report mode too**, and it binds first: a held PR
|
|
514
|
+
returns `awaiting-human:held` without arming, without the mechanical step (a)
|
|
515
|
+
nudge, and without an `update-branch`. Hold is not a blocker classification and
|
|
516
|
+
never returns as one — `blocked:*` says *this PR cannot proceed yet*, while a
|
|
517
|
+
hold says *someone asked me not to proceed*, and reporting the second as the
|
|
518
|
+
first would file a human's decision as a defect for something else to clear.
|
|
519
|
+
|
|
396
520
|
The arm gate binds in report mode too, and this is the mode where forgetting it
|
|
397
521
|
does the most damage: a diagnose-only run that arms the latch has not diagnosed
|
|
398
522
|
anything, it has authorised a merge. Under a vacuous review context, report mode
|
|
@@ -938,6 +1062,22 @@ Loop until one of:
|
|
|
938
1062
|
merge but nobody reviewed it: the review tool says it skips public
|
|
939
1063
|
repositories, and that will not change on its own. Merge it yourself or ask
|
|
940
1064
|
for a human review."*
|
|
1065
|
+
- **`awaiting-human:held`** → success, and the only terminal state a person can
|
|
1066
|
+
cause on purpose from outside the run. The hold label (section 0) was present,
|
|
1067
|
+
the latch was disarmed once and the re-read proved it, and the label was left
|
|
1068
|
+
in place. Report the PR URL, the resolved label name, and that removing the
|
|
1069
|
+
label lets a later invocation resume — for example: *"PR #123 is on hold:
|
|
1070
|
+
someone added the `lisa:hold` label, so I stopped instead of merging it.
|
|
1071
|
+
Remove that label when you want it to continue."* Not a stall and not a
|
|
1072
|
+
failure: a human asked for the PR to stop, and it stopped.
|
|
1073
|
+
- **`awaiting-human:hold-unknown`** → the hold label could not be read (twice),
|
|
1074
|
+
so whether a human asked for a stop is **unknown**. The latch is disarmed and
|
|
1075
|
+
the PR sits OPEN, on the fail-toward-hold rule in section 0. Report the read
|
|
1076
|
+
error verbatim. This is deliberately NOT reported as `awaiting-human:held`:
|
|
1077
|
+
that would file a broken permission or a rate limit as a human decision, and
|
|
1078
|
+
the one thing nobody would then do is investigate it. Usually a missing
|
|
1079
|
+
`issues: read` / label-read permission, which is fixable and invisible from
|
|
1080
|
+
the merge outcome alone.
|
|
941
1081
|
- **`CLOSED`** → report (PR was closed without merge).
|
|
942
1082
|
- **Hard block needing a human**: an unresolvable conflict, a failing check that
|
|
943
1083
|
needs design input, or genuine unresolved human objection (not a bot gate). Stop
|
|
@@ -48,6 +48,8 @@ Recognized optional hints:
|
|
|
48
48
|
|
|
49
49
|
This delegation is now load-bearing rather than a convenience: it is the only path that arms auto-merge, so skipping it leaves the PR open rather than quietly merging it. Reporting submission as complete without invoking it is a failed submit, not a partial one — say so instead of implying the PR is on its way to merged.
|
|
50
50
|
|
|
51
|
+
**The hold label needs no check here, and adding one would be dead code.** `drive-pr-to-merge` evaluates its hold gate before it arms anything, so a PR created with the label already on it is never armed — by the delegation, not by a second check in this skill. That guarantee used to require one: while step 5 armed the latch at creation, a hold that covered only the loop left the creation-time arm wide open, which is why CodySwannGT/lisa#3558 asked for a check in both places. Step 5 no longer arms, so there is nothing here for a label to gate. If you are adding one because the ticket says to, re-read step 5 first.
|
|
52
|
+
|
|
51
53
|
### Native Development Linkage
|
|
52
54
|
|
|
53
55
|
Add provider-appropriate linkage to the PR title and/or body without changing the status lifecycle:
|