@codyswann/lisa 4.34.7 → 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/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +184 -5
- package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +11 -5
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -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 -12
- 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/.codex-plugin/skills/lisa-repair-intake/SKILL.md +64 -7
- package/plugins/lisa/hooks/block-direct-issue-create.sh +184 -5
- package/plugins/lisa/hooks/block-managed-file-edits.sh +11 -5
- package/plugins/lisa/rules/eager/rejection-detection.md +14 -1
- package/plugins/lisa/rules/eager/settled-decisions.md +20 -0
- package/plugins/lisa/rules/reference/rejection-detection.md +69 -0
- package/plugins/lisa/rules/reference/settled-decisions.md +10 -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/skills/lisa-repair-intake/SKILL.md +64 -7
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +184 -5
- 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-agy/skills/lisa-repair-intake/SKILL.md +64 -7
- 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 +184 -5
- package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +11 -5
- package/plugins/lisa-copilot/rules/eager/rejection-detection.md +14 -1
- package/plugins/lisa-copilot/rules/eager/settled-decisions.md +20 -0
- package/plugins/lisa-copilot/rules/reference/rejection-detection.md +69 -0
- package/plugins/lisa-copilot/rules/reference/settled-decisions.md +10 -0
- 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-copilot/skills/lisa-repair-intake/SKILL.md +64 -7
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +184 -5
- package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +11 -5
- package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +69 -0
- package/plugins/lisa-cursor/rules/rejection-detection.mdc +14 -1
- package/plugins/lisa-cursor/rules/settled-decisions-reference.mdc +10 -0
- package/plugins/lisa-cursor/rules/settled-decisions.mdc +20 -0
- 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-cursor/skills/lisa-repair-intake/SKILL.md +64 -7
- 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 +184 -5
- package/plugins/src/base/hooks/block-managed-file-edits.sh +11 -5
- package/plugins/src/base/rules/eager/rejection-detection.md +14 -1
- package/plugins/src/base/rules/eager/settled-decisions.md +20 -0
- package/plugins/src/base/rules/reference/rejection-detection.md +69 -0
- package/plugins/src/base/rules/reference/settled-decisions.md +10 -0
- 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/plugins/src/base/skills/lisa-repair-intake/SKILL.md +64 -7
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +69 -3
|
@@ -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:
|
|
@@ -186,7 +186,9 @@ rule — never hardcode status/label strings. The relevant repair roles:
|
|
|
186
186
|
|
|
187
187
|
In addition to the lifecycle roles above, the build lifecycle defines the **`human_needed` marker** — an additive label (`jira.labels.human_needed` / `github.labels.build.human_needed` / `linear.labels.build.human_needed`, default `Human Needed` / `human-needed`) that rides alongside `blocked` when the block needs human-only input no agent or retry can supply (see `config-resolution` "Build markers"). repair-intake's interaction with the marker is asymmetric and is the whole point of the distinction below:
|
|
188
188
|
|
|
189
|
-
- The blocks repair-intake **itself writes** are the auto-recoverable kind — it files a build-ready fix ticket and moves the item `blocked` *blocked by that ticket*, expecting the next cycle to self-heal. Those are **not** `human_needed`; if such an item arrives
|
|
189
|
+
- The blocks repair-intake **itself writes** are the auto-recoverable kind — it files a build-ready fix ticket and moves the item `blocked` *blocked by that ticket*, expecting the next cycle to self-heal. Those are **not** `human_needed`; if such an item arrives carrying a `human_needed` marker **this skill applied on an earlier cycle**, repair-intake **clears** it (the block is no longer waiting on a human).
|
|
190
|
+
- **Never remove a `human_needed` marker this skill did not apply.** "Stale" is a judgment about the block's kind, not about who applied the marker or when — so without this rule an operator's deliberate hold, applied *after* correcting a wrong transition, is indistinguishable from a leftover the sweep is designed to clear, and gets swept. Establish provenance from the label event's actor (`rejection-detection` **Automation-reversal memory** reads the same surfaces); if provenance is not readable, **leave the marker in place**. Removing a human's hold is unrecoverable within the loop; leaving a stale one costs a cycle and is visible.
|
|
191
|
+
- The marker is consulted **before **any** repair transition**, not only before Class C. Class C's hard stop is the strictest reading of it, but a marker that is honoured on one classification path and ignored on the other three is not a guard — and Class A, dependency clearing, is exactly the path an operator reverting a wrongly-cleared blocker is trying to protect. Match it robustly (hyphen/underscore, case-insensitive, label set and note prose) wherever it is read.
|
|
190
192
|
- The blocks the **vendor agent** writes when repair-intake re-dispatches it (its pre-flight gate) carry `human_needed` already — the agent owns that marker. repair-intake leaves it in place.
|
|
191
193
|
|
|
192
194
|
Resolve with the standard role-read pattern (local overrides global, default fallback):
|
|
@@ -305,6 +307,16 @@ Apply per candidate. Continue through the ordered list until every candidate ins
|
|
|
305
307
|
native close/archive/complete, re-dispatch, or refreshed note), be recorded read-only, or be
|
|
306
308
|
recorded under Errors. Do not stop after the first write; the cap is the batch boundary.
|
|
307
309
|
|
|
310
|
+
**Before any lifecycle write on a candidate, classify it for `automation-reversal`** per
|
|
311
|
+
`rejection-detection` **Automation-reversal memory** — a role change this skill made that a human
|
|
312
|
+
then moved back. This runs once per candidate at the top of the walk, ahead of the branches below,
|
|
313
|
+
because it wraps every repair path rather than belonging to one: a reversed rollup reconciliation
|
|
314
|
+
involves no dependency link at all, and a reversed Class-B re-check is the same shape as a reversed
|
|
315
|
+
Class-A clearing. Detection is a pure read and `unknown` **never blocks the sweep** — an item whose
|
|
316
|
+
history or authorship cannot be read is judged by the normal rules below, treated as neither a
|
|
317
|
+
reversal nor a clearance. A candidate classified `automation-reversal` skips the branches below and
|
|
318
|
+
is handled per Loop prevention.
|
|
319
|
+
|
|
308
320
|
### Build `claimed` (stalled in-progress) → diagnose blocker, else resume in place
|
|
309
321
|
|
|
310
322
|
**First check for an already-merged PR — this check is NOT gated on staleness.** Read the item's
|
|
@@ -478,9 +490,13 @@ re-issued against an unchanged conflicting head.
|
|
|
478
490
|
new fix ticket (vendor-native: JIRA issue link `is blocked by`; GitHub/Linear `Blocked by:` line
|
|
479
491
|
+ label). Post a `[lisa-repair-intake]` note naming what it is blocked by and why. This block is
|
|
480
492
|
**auto-recoverable** — the fix ticket will build and close on its own — so do **not** add the
|
|
481
|
-
`human_needed` marker, and if the item
|
|
482
|
-
here (it is no longer waiting on a human). The `human_needed` marker
|
|
483
|
-
human must clear; this one a later cycle clears automatically.
|
|
493
|
+
`human_needed` marker, and if the item carries a `human_needed` label **this skill applied on an
|
|
494
|
+
earlier cycle**, remove it here (it is no longer waiting on a human). The `human_needed` marker
|
|
495
|
+
is reserved for blocks a human must clear; this one a later cycle clears automatically.
|
|
496
|
+
**A marker this skill did not apply is never removed here** — see the marker's provenance rule
|
|
497
|
+
above. The block's kind says the *block* self-heals; it says nothing about why a human put a
|
|
498
|
+
hold on the item, and an operator's hold applied after correcting a wrong transition looks
|
|
499
|
+
identical to a leftover unless provenance is checked.
|
|
484
500
|
3. **Record it** as a repair write. Do **not** dispatch the vendor agent for this item this cycle.
|
|
485
501
|
|
|
486
502
|
The item now sits in `blocked`; once the fix ticket reaches a terminal state, the **Build
|
|
@@ -1026,6 +1042,14 @@ layer, and test ancestry against the remote.
|
|
|
1026
1042
|
"no PR found" is `no-pr` and stays blocking.
|
|
1027
1043
|
- **Human override** — an explicit human statement on this item that the dependency is satisfied
|
|
1028
1044
|
clears it and outranks a failed containment check. Name it in the run record.
|
|
1045
|
+
- **Human hold** — the override runs in both directions. `human_needed` is checked here as well as
|
|
1046
|
+
in Class C: an item carrying it is left `blocked` and recorded `skipped: human_needed`, without
|
|
1047
|
+
running the containment check. Class A is the path an operator reverting a wrongly-cleared
|
|
1048
|
+
blocker is protecting, so a marker honoured only on Class C is not honoured where it is most
|
|
1049
|
+
often meant. Match it robustly (hyphen/underscore, case-insensitive, label set and note prose).
|
|
1050
|
+
Symmetrically, an explicit human statement that the dependency is **not** satisfied keeps the
|
|
1051
|
+
item blocked and outranks a *passing* containment check — the contract has an input channel for
|
|
1052
|
+
"you may now proceed" and it needs one for "your reasoning was wrong".
|
|
1029
1053
|
|
|
1030
1054
|
**A lifecycle status name is never the test.** `done`, `on-stg`, `on-dev` and `code-review` are
|
|
1031
1055
|
labels that correlate with containment in a single-trunk workflow and come apart from it in a
|
|
@@ -1139,12 +1163,33 @@ cron tick.
|
|
|
1139
1163
|
self-block** (so a human filling in one of several missing sections changes the fingerprint and
|
|
1140
1164
|
triggers a re-check next cycle, rather than being suppressed as a no-op), terminal/open state,
|
|
1141
1165
|
rollup child tally, and a timestamp.
|
|
1166
|
+
- The fingerprint also carries **authorship and an overturn bit**: the actor class of the most
|
|
1167
|
+
recent lifecycle role change (`automation` / `human` / `unknown`), and whether that change
|
|
1168
|
+
**reversed** a transition this skill made — the `automation-reversal` classification from
|
|
1169
|
+
`rejection-detection` **Automation-reversal memory**. Without these the fingerprint records only
|
|
1170
|
+
*what* the state is, never *who* set it or *whether the last automated decision was overturned*,
|
|
1171
|
+
and those are the only fields that separate "the world moved on" from "a human told me I was
|
|
1172
|
+
wrong".
|
|
1142
1173
|
- Before writing a note or re-attempting a `blocked` item, compute the current fingerprint. If
|
|
1143
1174
|
an identical fingerprint was already posted within the **backoff window**, skip the item
|
|
1144
1175
|
silently (record as `still_blocked` / `active`, no write).
|
|
1145
1176
|
- Backoff window default = `stale_after` (2h). `force=true` bypasses backoff for a manual run.
|
|
1146
|
-
- A *changed* fingerprint (new blocker state, new answers, new verdict)
|
|
1147
|
-
|
|
1177
|
+
- A *changed* fingerprint (new blocker state, new answers, new verdict) warrants a fresh note +
|
|
1178
|
+
re-attempt — backoff suppresses only no-op repeats.
|
|
1179
|
+
- **A change whose only cause is a human overturn is not a warrant.** Where the overturn bit is
|
|
1180
|
+
set and no evidence postdates the reversal, the changed fingerprint is **not on its own a
|
|
1181
|
+
warrant** to re-attempt: do not re-apply the reversed classification, leave the item where the
|
|
1182
|
+
human put it, ensure `human_needed`, and record the outcome naming the **reversed transition**
|
|
1183
|
+
and the classification that produced it. Re-attempt only on evidence postdating the reversal —
|
|
1184
|
+
a blocker state change or a human comment supplying what was missing — cited in the run record.
|
|
1185
|
+
Without this qualifier the revert *itself* changes the fingerprint and releases backoff, so the
|
|
1186
|
+
faster a reviewer corrects a wrong transition, the sooner it returns.
|
|
1187
|
+
- **This inversion is signed per path.** It applies to the paths that decide whether to re-make a
|
|
1188
|
+
lifecycle transition — Class A/B/C/D clearing and rollup reconciliation. It does **not** touch
|
|
1189
|
+
the staleness model: there a human comment counts as forward progress and resets the staleness
|
|
1190
|
+
clock, so an objecting comment already makes the item look active and this skill leaves it
|
|
1191
|
+
alone. That reading is protective; a single global "human activity no longer counts" would
|
|
1192
|
+
switch it off while reading like a tightening.
|
|
1148
1193
|
|
|
1149
1194
|
## Lifecycle ownership guard
|
|
1150
1195
|
|
|
@@ -1261,6 +1306,12 @@ Report outcomes in these buckets:
|
|
|
1261
1306
|
- `normalized_ready` — GitHub issues missing official lifecycle labels that were classified and
|
|
1262
1307
|
given the configured PRD/build `ready` label so normal intake can claim them.
|
|
1263
1308
|
- `still_blocked` — examined and intentionally left `blocked`, with the active reason.
|
|
1309
|
+
- `reversal_suppressed` — a repair this skill previously made was reversed by a human, so the
|
|
1310
|
+
classification was **not** re-applied. Name the **reversed transition** (role, direction, and
|
|
1311
|
+
when), the classification that produced it, and that the item now carries `human_needed`. This
|
|
1312
|
+
bucket is never merged into `still_blocked`: "I declined to act because I was overruled" and "I
|
|
1313
|
+
examined it and the blocker stands" are different facts about the loop, and only the first tells
|
|
1314
|
+
an operator their correction was received.
|
|
1264
1315
|
- `active` — skipped because current work is not stale (or within backoff).
|
|
1265
1316
|
- `errors` — items that failed evaluation, with the error.
|
|
1266
1317
|
|
|
@@ -1280,7 +1331,7 @@ to each item* — the two never merge in the one-line summary.
|
|
|
1280
1331
|
| Nothing actionable — the idle case (walk step 5): examined N, all active or in backoff — including a fix ticket suppressed by a prior decline (`rejection-detection` **Proposal rejection memory**), which the summary names in its suppression count | `nothing-needed` |
|
|
1281
1332
|
| Repairs applied **and confirmed** this cycle — `resumed` / `resynced` / `recovered` / `unblocked` / `closed_out` / `rolled_up` / `relinked` / `normalized_ready` | `change-proved` |
|
|
1282
1333
|
| Repair produced new work for a human to pick up — e.g. an unmergeable PR or failed deploy filed as a **build-ready fix ticket** and left `blocked` | `candidate-proposed` |
|
|
1283
|
-
| A repair reached an autonomy boundary needing a human (a protected-deploy approval before it can proceed) | `approval-requested` |
|
|
1334
|
+
| A repair reached an autonomy boundary needing a human (a protected-deploy approval before it can proceed), **or a repair this loop previously made was reversed by a human** and is therefore suppressed and escalated to `human_needed` (`reversal_suppressed`) rather than re-attempted | `approval-requested` |
|
|
1284
1335
|
| The loop itself could not run — the queue is unreadable, tracker credentials are revoked, or an open-and-closed rejection-memory / blocker-marker search is unreadable and therefore must not fall through to `nothing-needed` | `recovery-required` |
|
|
1285
1336
|
| The runbook's **Retirement condition** tripped | `policy-obsolete` — **never reached by design for this loop** (see Retirement evaluation below) |
|
|
1286
1337
|
|
|
@@ -1343,6 +1394,12 @@ stuck work accumulates), or interleaved on a longer cadence.
|
|
|
1343
1394
|
do."
|
|
1344
1395
|
- Never re-dispatch a `blocked` build item unless every parsed blocker is cleared (conservative
|
|
1345
1396
|
dependency clearing).
|
|
1397
|
+
- Never re-make a transition a human reversed. Classify every candidate for `automation-reversal`
|
|
1398
|
+
before any lifecycle write (`rejection-detection`), and on a hit leave the item where the human
|
|
1399
|
+
put it with `human_needed`, naming the reversed transition in the run record. Re-attempt only on
|
|
1400
|
+
evidence postdating the reversal.
|
|
1401
|
+
- Never remove a `human_needed` marker this skill did not apply, and consult the marker before
|
|
1402
|
+
**any** repair transition rather than only before Class C.
|
|
1346
1403
|
- Repair every materially actionable candidate inside the `max_candidates` cap; default cap is 100.
|
|
1347
1404
|
- Default GitHub `intake_mode` is `both` when both PRD and build namespaces exist.
|
|
1348
1405
|
- Honor the backoff window — never re-post an identical `[lisa-repair-intake]` note within it
|
|
@@ -1399,12 +1399,190 @@ def nested_operands(argv):
|
|
|
1399
1399
|
inspected_files = set()
|
|
1400
1400
|
|
|
1401
1401
|
|
|
1402
|
+
# Interpreters whose OPERAND is a program they run. Broader than a shell list
|
|
1403
|
+
# because `file_creation` recognises a creation in any language — the
|
|
1404
|
+
# `node wrapper.mjs` that speaks HTTP directly is exactly the shape #3484
|
|
1405
|
+
# measured as a fail-open, so narrowing this to shells would reopen it.
|
|
1406
|
+
EXECUTING_INTERPRETERS = {
|
|
1407
|
+
"bash", "dash", "ksh", "sh", "zsh",
|
|
1408
|
+
"bun", "deno", "node", "nodejs", "perl", "php",
|
|
1409
|
+
"python", "python3", "ruby", "ts-node", "tsx",
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
# `.` and its `source` alias run the named file in the CURRENT shell.
|
|
1413
|
+
SOURCE_BUILTINS = {"source", "."}
|
|
1414
|
+
|
|
1415
|
+
# Wrappers that run what FOLLOWS them without changing what it is, mapped to
|
|
1416
|
+
# (options whose value is a SEPARATE token, positional operands consumed).
|
|
1417
|
+
# The positional count is why this is a table: `timeout 5 bash create.sh` puts
|
|
1418
|
+
# an operand between the wrapper and the interpreter, so a walk that steps over
|
|
1419
|
+
# `-flags` only stops at `5` and never reaches the interpreter.
|
|
1420
|
+
EXEC_WRAPPERS = {
|
|
1421
|
+
"builtin": (frozenset(), 0),
|
|
1422
|
+
"command": (frozenset(), 0),
|
|
1423
|
+
"exec": (frozenset({"-a"}), 0),
|
|
1424
|
+
"env": (frozenset({"-u", "--unset", "-C", "--chdir", "--argv0"}), 0),
|
|
1425
|
+
"nice": (frozenset({"-n", "--adjustment"}), 0),
|
|
1426
|
+
"nohup": (frozenset(), 0),
|
|
1427
|
+
"setsid": (frozenset(), 0),
|
|
1428
|
+
"stdbuf": (frozenset({"-i", "-o", "-e"}), 0),
|
|
1429
|
+
"sudo": (frozenset({"-u", "--user", "-g", "--group", "-C", "--close-from"}), 0),
|
|
1430
|
+
"time": (frozenset(), 0),
|
|
1431
|
+
"timeout": (frozenset({"-k", "--kill-after", "-s", "--signal"}), 1),
|
|
1432
|
+
}
|
|
1433
|
+
|
|
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
|
+
NO_SCRIPT_OPTIONS = {"-c", "-e", "-m", "--eval", "--command", "--module"}
|
|
1437
|
+
|
|
1438
|
+
# Programs that READ their operands and never execute them as programs.
|
|
1439
|
+
#
|
|
1440
|
+
# This is an allowlist of readers rather than the inverse, and the asymmetry is
|
|
1441
|
+
# deliberate. An UNKNOWN program with a file operand is still followed, because
|
|
1442
|
+
# whether it executes that operand is genuinely unknowable from the command —
|
|
1443
|
+
# and a probe in this guard's own suite drives it with a runner nobody
|
|
1444
|
+
# enumerated precisely so a fix keyed on a list of interpreters fails. Failing
|
|
1445
|
+
# closed on the unknown keeps that coverage.
|
|
1446
|
+
#
|
|
1447
|
+
# What the list buys is the measured population of #3705, every member of which
|
|
1448
|
+
# is a well-known inspector: `git diff`, `grep -n`, `wc -l`, a `git grep`
|
|
1449
|
+
# PATHSPEC, `sed -n '1,50p'`, and a test run. `git` is here wholesale because no
|
|
1450
|
+
# git subcommand executes a named file as a program — `git commit -F <file>` and
|
|
1451
|
+
# `git grep -- <pathspec>` both take the path as data.
|
|
1452
|
+
#
|
|
1453
|
+
# Test runners are here on purpose and it is the one entry worth arguing about.
|
|
1454
|
+
# `vitest <file>` does run that file, so the executes-rule would follow it — but
|
|
1455
|
+
# running a test is not filing an issue, and refusing a test run is the sharpest
|
|
1456
|
+
# harm #3705 records, because it can block the verification of a fix while
|
|
1457
|
+
# saying nothing about why.
|
|
1458
|
+
#
|
|
1459
|
+
# RESIDUAL, stated rather than hidden: a reader NOT on this list is still
|
|
1460
|
+
# followed and can still over-refuse. The set of read-only tools is unbounded,
|
|
1461
|
+
# so this closes the measured population and not the class. Add names here as
|
|
1462
|
+
# they are measured; do not invert the default to close it by fiat, or the
|
|
1463
|
+
# executed-script reach that #3484 bought is lost.
|
|
1464
|
+
READ_ONLY_PROGRAMS = {
|
|
1465
|
+
"awk", "bat", "cat", "cksum", "cmp", "column", "comm", "cut", "diff",
|
|
1466
|
+
"du", "file", "fold", "git", "grep", "head", "hexdump", "jest", "jq",
|
|
1467
|
+
"less", "ls", "md5", "md5sum", "more", "nl", "od", "pytest", "rg",
|
|
1468
|
+
"sed", "sha1sum", "sha256sum", "shellcheck", "shfmt", "sort", "stat",
|
|
1469
|
+
"strings", "tail", "tee", "uniq", "vitest", "wc", "xxd", "yamllint",
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def executing_command(argv):
|
|
1474
|
+
"""The program this command runs, and the tokens after it.
|
|
1475
|
+
|
|
1476
|
+
Steps over leading variable assignments and over wrappers that do not
|
|
1477
|
+
change what runs. Reading a wrapper as the command is how a payload hides
|
|
1478
|
+
from a classifier that inspects only a segment's first word.
|
|
1479
|
+
|
|
1480
|
+
Args:
|
|
1481
|
+
argv: One command's tokens.
|
|
1482
|
+
|
|
1483
|
+
Returns:
|
|
1484
|
+
A triple (token, program, args); (None, None, []) when none is named.
|
|
1485
|
+
The raw token is carried alongside the basename because a script run by
|
|
1486
|
+
bare path — `./create.sh`, relying on its shebang — has no interpreter
|
|
1487
|
+
to recognise, and the command word IS the thing being executed.
|
|
1488
|
+
"""
|
|
1489
|
+
index = 0
|
|
1490
|
+
while index < len(argv):
|
|
1491
|
+
token = argv[index]
|
|
1492
|
+
if "=" in token and not token.startswith(("=", "-")):
|
|
1493
|
+
index += 1
|
|
1494
|
+
continue
|
|
1495
|
+
program = basename(token)
|
|
1496
|
+
if program not in EXEC_WRAPPERS:
|
|
1497
|
+
return (token, program, argv[index + 1 :])
|
|
1498
|
+
separate, positional = EXEC_WRAPPERS[program]
|
|
1499
|
+
index += 1
|
|
1500
|
+
while index < len(argv) and argv[index].startswith("-"):
|
|
1501
|
+
option = argv[index]
|
|
1502
|
+
if option == "--":
|
|
1503
|
+
index += 1
|
|
1504
|
+
break
|
|
1505
|
+
index += 2 if option in separate else 1
|
|
1506
|
+
index += positional
|
|
1507
|
+
return (None, None, [])
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
def executed_operand(argv):
|
|
1511
|
+
"""The path this command EXECUTES, or None.
|
|
1512
|
+
|
|
1513
|
+
THIS IS THE COMMAND-POSITION QUESTION, and declining to ask it is what
|
|
1514
|
+
#3705 records: the guard opened any readable file any token named, then
|
|
1515
|
+
justified a refusal from that FILE's contents rather than from the
|
|
1516
|
+
COMMAND's behaviour. Measured refusals included `git diff`, `grep -n`,
|
|
1517
|
+
`wc -l`, a `git grep` PATHSPEC and a test run — every one of them read-only,
|
|
1518
|
+
and one of them blocked running the tests that would have proved a fix.
|
|
1519
|
+
|
|
1520
|
+
`parity-safety-net.sh` already states the rule this restores, and names the
|
|
1521
|
+
opposite as the known-wrong fix (`origin/main`, lines 337-346 and 704-705):
|
|
1522
|
+
"Only a COMMAND POSITION can execute something. A path anywhere else is an
|
|
1523
|
+
argument, and an argument is data." `block-no-verify.sh:495-513`
|
|
1524
|
+
(`strip_command_prefix`) implements the same split with a fail-closed third
|
|
1525
|
+
state. Both live in this directory; this guard was the one that declined.
|
|
1526
|
+
|
|
1527
|
+
NOT narrowed to shells, and NOT narrowed to an enumerated interpreter list.
|
|
1528
|
+
`file_creation` recognises a creation in any language, so `node wrapper.mjs`
|
|
1529
|
+
must still be followed — a measured fail-open in #3484 — and this guard's
|
|
1530
|
+
own suite drives it with a runner nobody enumerated, precisely so a fix
|
|
1531
|
+
keyed on such a list fails. So the default for an unrecognised program is to
|
|
1532
|
+
FOLLOW, and only a known reader is exempt. A script run by bare path with no
|
|
1533
|
+
interpreter at all (`./create.sh`, via its shebang) is followed too: there
|
|
1534
|
+
the command word is itself the thing being executed.
|
|
1535
|
+
|
|
1536
|
+
Reading a REQUEST PAYLOAD is a separate question with a separate answer:
|
|
1537
|
+
`payload_text` resolves `--data-binary @file` and friends by flag name, so
|
|
1538
|
+
narrowing this walk does not stop a creation whose body lives in a file
|
|
1539
|
+
from being seen.
|
|
1540
|
+
|
|
1541
|
+
Args:
|
|
1542
|
+
argv: One command's tokens.
|
|
1543
|
+
|
|
1544
|
+
Returns:
|
|
1545
|
+
The operand token naming a program that will run, or None.
|
|
1546
|
+
"""
|
|
1547
|
+
token, program, args = executing_command(argv)
|
|
1548
|
+
if program is None:
|
|
1549
|
+
return None
|
|
1550
|
+
# A reader takes its operands as data. This is the #3705 population.
|
|
1551
|
+
if program in READ_ONLY_PROGRAMS:
|
|
1552
|
+
return None
|
|
1553
|
+
if program in SOURCE_BUILTINS:
|
|
1554
|
+
return args[0] if args else None
|
|
1555
|
+
# A command word that is ITSELF a readable file is being executed by its
|
|
1556
|
+
# shebang — `./create.sh` names no interpreter and runs all the same.
|
|
1557
|
+
if program not in EXECUTING_INTERPRETERS and resolve_operand(token) is not None:
|
|
1558
|
+
return token
|
|
1559
|
+
# `bash < create.sh` runs that file as surely as `bash create.sh` does, and
|
|
1560
|
+
# `<` is not a segment boundary, so the pair survives in one argv.
|
|
1561
|
+
for index, argument in enumerate(args):
|
|
1562
|
+
if argument == "<" and index + 1 < len(args):
|
|
1563
|
+
return args[index + 1]
|
|
1564
|
+
# Reached by a known interpreter AND by a program this parser cannot name.
|
|
1565
|
+
# The unknown case is followed on purpose — see READ_ONLY_PROGRAMS.
|
|
1566
|
+
for argument in args:
|
|
1567
|
+
if argument == "--":
|
|
1568
|
+
continue
|
|
1569
|
+
if argument.startswith("-"):
|
|
1570
|
+
# A command string or a module name; no script file follows.
|
|
1571
|
+
head = argument.split("=", 1)[0]
|
|
1572
|
+
if argument in NO_SCRIPT_OPTIONS or head in NO_SCRIPT_OPTIONS:
|
|
1573
|
+
return None
|
|
1574
|
+
continue
|
|
1575
|
+
return argument
|
|
1576
|
+
return None
|
|
1577
|
+
|
|
1578
|
+
|
|
1402
1579
|
def file_operands(argv):
|
|
1403
|
-
"""
|
|
1580
|
+
"""The file this command EXECUTES, if it executes one.
|
|
1404
1581
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1582
|
+
Deciding WHICH programs execute their operands is the question this file
|
|
1583
|
+
used to refuse to ask, and refusing it is what made the guard read a path
|
|
1584
|
+
quoted as prose inside a `--body-file` markdown and attribute its contents
|
|
1585
|
+
to the command. It is answered in `executed_operand` now.
|
|
1408
1586
|
|
|
1409
1587
|
Args:
|
|
1410
1588
|
argv: One command's tokens.
|
|
@@ -1413,7 +1591,8 @@ def file_operands(argv):
|
|
|
1413
1591
|
Existing paths, deduplicated across the whole scan and capped.
|
|
1414
1592
|
"""
|
|
1415
1593
|
paths = []
|
|
1416
|
-
|
|
1594
|
+
operand = executed_operand(argv)
|
|
1595
|
+
for token in [operand] if operand is not None else []:
|
|
1417
1596
|
if len(paths) >= FILE_OPERANDS_PER_SEGMENT:
|
|
1418
1597
|
break
|
|
1419
1598
|
path = resolve_operand(token)
|
|
@@ -415,11 +415,17 @@ EOF
|
|
|
415
415
|
# what a tokenizer is for.
|
|
416
416
|
#
|
|
417
417
|
# The analyzer is read into a variable by `read -d ''` rather than run
|
|
418
|
-
# inside a
|
|
419
|
-
# a command substitution by re-scanning its text, and a
|
|
420
|
-
# there is scanned too: the lone backtick in
|
|
421
|
-
# reads as an unterminated backtick
|
|
422
|
-
#
|
|
418
|
+
# inside a command substitution wrapping a here-document. Bash finds the
|
|
419
|
+
# closing paren of a command substitution by re-scanning its text, and a
|
|
420
|
+
# here-document body nested there is scanned too: the lone backtick in
|
|
421
|
+
# `re.compile(r"[$`]")` below then reads as an unterminated backtick
|
|
422
|
+
# substitution and the whole script fails to parse. Keeping the
|
|
423
|
+
# here-document out of the substitution is the fix. This is also the shape
|
|
424
|
+
# `block-direct-issue-create.sh` already uses for its own classifier.
|
|
425
|
+
#
|
|
426
|
+
# The delimiter is deliberately not spelled out in this comment: a tool that
|
|
427
|
+
# greps for here-document starts would otherwise match the prose and read
|
|
428
|
+
# the wrong span.
|
|
423
429
|
analyzer=''
|
|
424
430
|
IFS= read -r -d '' analyzer <<'PY' || true
|
|
425
431
|
import os
|
|
@@ -48,4 +48,17 @@ A **loop-proposed** item that a human **closed as _not planned_** is a durable d
|
|
|
48
48
|
- **Outcomes:** an all-suppressed cycle ends `nothing-needed` naming the **suppression count**; a memory check that cannot read the tracker ends `recovery-required`, never a silent `nothing-needed` (e.g. `Tracker unreachable during the decline check — restore credentials; nothing was filed this run.`).
|
|
49
49
|
- **Concurrency:** search-then-write = convergence, not mutual exclusion — a transient duplicate is closed by the next run; no cross-run lock.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
## Automation-reversal memory (repair-side — orthogonal to both halves above)
|
|
52
|
+
|
|
53
|
+
A human who **undoes a lifecycle transition this flow made** is correcting the flow, not supplying new state. Every loop that writes a lifecycle role as a **repair** (`lisa-repair-intake`) consults this before re-making a transition. It is a third classification, never merged into `rejection-reclaim`: that one is anchored on the ready lane and answers a rejection with re-implementation; this one lands in `$BLOCKED` and answers a correction by withdrawing a decision.
|
|
54
|
+
|
|
55
|
+
- **Seam:** top of the repair walk for each item, **before any lifecycle write** — after the write the current-lane signal is gone.
|
|
56
|
+
- **Classify:** `automation-reversal` — a role change **authored by** this flow, then a later **backward** move of that role **authored by** a human, with no automation move after it. Otherwise `no-reversal`. Authorship unavailable or history unreadable → `unknown`.
|
|
57
|
+
- **Authorship is the signal, not the backward move.** An item can return to `$BLOCKED` because a real new blocker appeared — that is the world moving on. Read the `actor` (GitHub timeline, Linear history) or `author` (JIRA changelog) through the access layers; an unexposed actor is `unknown`, never an assumed human. **A marker and a transition on the same item say nothing until you know their order** — compare timestamps, and treat an unorderable pair as `unknown`.
|
|
58
|
+
- **Lanes from config** (`github.labels.build.blocked` + the JIRA/Linear equivalents, `config-resolution`) — never hardcode.
|
|
59
|
+
- **Degrade:** `unknown` is **neither a reversal nor a clearance** — the item is judged by the loop's normal rules and detection **never blocks the sweep**. The learning-loop exclusion above applies unchanged.
|
|
60
|
+
- **Rule:** on `automation-reversal`, do not re-apply the reversed classification; leave the item where the human put it, ensure `human_needed`, and **name the reversed transition in the run record**. Re-attempt **only** on evidence that **postdates the reversal**, cited in the run record — mirroring the re-proposal rule the proposal half enforces.
|
|
61
|
+
- **A changed lifecycle role is no longer a sufficient warrant on its own.** A consuming loop's recurrence brake must carry **authorship and an overturn bit**, not state alone.
|
|
62
|
+
- **Signed per path.** Apply the inversion only where the loop decides whether to re-make a transition. A staleness/keep-alive path where a human comment already *suppresses* action is protective — leave it. One global sign reads as tightening and removes that protection.
|
|
63
|
+
|
|
64
|
+
Full contract (classification table, per-vendor bindings, reflection & evidence handoff, Proposal rejection memory, Automation-reversal memory): [reference/rejection-detection.md](../reference/rejection-detection.md).
|