@codyswann/lisa 4.34.7 → 4.34.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +184 -5
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +11 -5
  3. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.js +6 -0
  5. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  6. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +14 -10
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/package.json +4 -4
  11. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +64 -7
  14. package/plugins/lisa/hooks/block-direct-issue-create.sh +184 -5
  15. package/plugins/lisa/hooks/block-managed-file-edits.sh +11 -5
  16. package/plugins/lisa/rules/eager/rejection-detection.md +14 -1
  17. package/plugins/lisa/rules/eager/settled-decisions.md +20 -0
  18. package/plugins/lisa/rules/reference/rejection-detection.md +69 -0
  19. package/plugins/lisa/rules/reference/settled-decisions.md +10 -0
  20. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +64 -7
  21. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +184 -5
  22. package/plugins/lisa-agy/plugin.json +1 -1
  23. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +64 -7
  24. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  27. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +184 -5
  31. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +11 -5
  32. package/plugins/lisa-copilot/rules/eager/rejection-detection.md +14 -1
  33. package/plugins/lisa-copilot/rules/eager/settled-decisions.md +20 -0
  34. package/plugins/lisa-copilot/rules/reference/rejection-detection.md +69 -0
  35. package/plugins/lisa-copilot/rules/reference/settled-decisions.md +10 -0
  36. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +64 -7
  37. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +184 -5
  39. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +11 -5
  40. package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +69 -0
  41. package/plugins/lisa-cursor/rules/rejection-detection.mdc +14 -1
  42. package/plugins/lisa-cursor/rules/settled-decisions-reference.mdc +10 -0
  43. package/plugins/lisa-cursor/rules/settled-decisions.mdc +20 -0
  44. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +64 -7
  45. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo-agy/plugin.json +1 -1
  48. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  58. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  63. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  68. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails-agy/plugin.json +1 -1
  73. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  78. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  83. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/src/base/hooks/block-direct-issue-create.sh +184 -5
  86. package/plugins/src/base/hooks/block-managed-file-edits.sh +11 -5
  87. package/plugins/src/base/rules/eager/rejection-detection.md +14 -1
  88. package/plugins/src/base/rules/eager/settled-decisions.md +20 -0
  89. package/plugins/src/base/rules/reference/rejection-detection.md +69 -0
  90. package/plugins/src/base/rules/reference/settled-decisions.md +10 -0
  91. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +64 -7
  92. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +69 -3
@@ -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 already carrying a stale `human_needed` marker, repair-intake **clears** it (the block is no longer waiting on a human).
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 already carries a stale `human_needed` label, remove it
482
- here (it is no longer waiting on a human). The `human_needed` marker is reserved for blocks a
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) always warrants a fresh
1147
- note + re-attempt — backoff suppresses only no-op repeats.
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
- """Files this command names, in the order they appear.
1580
+ """The file this command EXECUTES, if it executes one.
1404
1581
 
1405
- Every token is offered, and the ones that name a readable regular file are
1406
- kept. Deciding WHICH programs execute their operands is the unbounded
1407
- question this file already refuses to ask, so it is not asked here either.
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
- for token in argv:
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)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.34.7",
3
+ "version": "4.34.8",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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 already carrying a stale `human_needed` marker, repair-intake **clears** it (the block is no longer waiting on a human).
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 already carries a stale `human_needed` label, remove it
482
- here (it is no longer waiting on a human). The `human_needed` marker is reserved for blocks a
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) always warrants a fresh
1147
- note + re-attempt — backoff suppresses only no-op repeats.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.34.7",
3
+ "version": "4.34.8",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.34.7",
3
+ "version": "4.34.8",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.34.7",
3
+ "version": "4.34.8",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.34.7",
3
+ "version": "4.34.8",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.34.7",
3
+ "version": "4.34.8",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.34.7",
3
+ "version": "4.34.8",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"