@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.
Files changed (104) 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 +17 -12
  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-drive-pr-to-merge/SKILL.md +141 -1
  14. package/plugins/lisa/.codex-plugin/skills/lisa-git-submit-pr/SKILL.md +2 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +64 -7
  16. package/plugins/lisa/hooks/block-direct-issue-create.sh +184 -5
  17. package/plugins/lisa/hooks/block-managed-file-edits.sh +11 -5
  18. package/plugins/lisa/rules/eager/rejection-detection.md +14 -1
  19. package/plugins/lisa/rules/eager/settled-decisions.md +20 -0
  20. package/plugins/lisa/rules/reference/rejection-detection.md +69 -0
  21. package/plugins/lisa/rules/reference/settled-decisions.md +10 -0
  22. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
  23. package/plugins/lisa/skills/lisa-git-submit-pr/SKILL.md +2 -0
  24. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +64 -7
  25. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +184 -5
  26. package/plugins/lisa-agy/plugin.json +1 -1
  27. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
  28. package/plugins/lisa-agy/skills/lisa-git-submit-pr/SKILL.md +2 -0
  29. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +64 -7
  30. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  33. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +184 -5
  37. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +11 -5
  38. package/plugins/lisa-copilot/rules/eager/rejection-detection.md +14 -1
  39. package/plugins/lisa-copilot/rules/eager/settled-decisions.md +20 -0
  40. package/plugins/lisa-copilot/rules/reference/rejection-detection.md +69 -0
  41. package/plugins/lisa-copilot/rules/reference/settled-decisions.md +10 -0
  42. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
  43. package/plugins/lisa-copilot/skills/lisa-git-submit-pr/SKILL.md +2 -0
  44. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +64 -7
  45. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +184 -5
  47. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +11 -5
  48. package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +69 -0
  49. package/plugins/lisa-cursor/rules/rejection-detection.mdc +14 -1
  50. package/plugins/lisa-cursor/rules/settled-decisions-reference.mdc +10 -0
  51. package/plugins/lisa-cursor/rules/settled-decisions.mdc +20 -0
  52. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
  53. package/plugins/lisa-cursor/skills/lisa-git-submit-pr/SKILL.md +2 -0
  54. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +64 -7
  55. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-expo-agy/plugin.json +1 -1
  58. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  63. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  68. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  73. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  78. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-rails-agy/plugin.json +1 -1
  83. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  88. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  93. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/src/base/hooks/block-direct-issue-create.sh +184 -5
  96. package/plugins/src/base/hooks/block-managed-file-edits.sh +11 -5
  97. package/plugins/src/base/rules/eager/rejection-detection.md +14 -1
  98. package/plugins/src/base/rules/eager/settled-decisions.md +20 -0
  99. package/plugins/src/base/rules/reference/rejection-detection.md +69 -0
  100. package/plugins/src/base/rules/reference/settled-decisions.md +10 -0
  101. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +141 -1
  102. package/plugins/src/base/skills/lisa-git-submit-pr/SKILL.md +2 -0
  103. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +64 -7
  104. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +69 -3
@@ -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)
@@ -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 `$(python3 - <<'PY' … PY)`. Bash has to find the closing paren of
419
- # a command substitution by re-scanning its text, and a heredoc body nested
420
- # there is scanned too: the lone backtick in `re.compile(r"[$`]")` below
421
- # reads as an unterminated backtick substitution and the whole script fails
422
- # to parse. Keeping the heredoc out of the substitution is the fix.
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
@@ -167,3 +167,72 @@ Naming the decline date, the recurrence date, and the recurrence reference in bo
167
167
  ### Concurrency honesty
168
168
 
169
169
  The search-then-write here is **not** an atomic claim: two truly concurrent runs can each miss the other's in-flight proposal and file a transient duplicate. It therefore guarantees **convergence, not mutual exclusion** — the duplicate is found and closed by the next run's marker search, and because the key is a stable content hash, a duplicate never multiplies. State this rather than implying a lock; a cross-run locking protocol would be disproportionate to the risk.
170
+
171
+ ## Automation-reversal memory (repair-side — a human undoing a transition this flow made)
172
+
173
+ Both sections above remember something a **human or QA did to work**: claim-time memory reads an item bounced backward through the review lanes; proposal-time memory reads a proposal closed as *not planned*. This third section remembers something a human did **to the flow itself** — an automated repair moved an item's lifecycle role, and a human moved it straight back.
174
+
175
+ Every loop that writes a lifecycle role as a **repair** rather than as a claim consults this section before re-making a transition. `lisa-repair-intake` is the first consumer and the skill this contract was written from.
176
+
177
+ The failure it exists to stop is not forgetfulness, it is **inversion**. A loop with a state-only recurrence brake reads the human's revert as *changed state*, and changed state is normally a reason to look again — so the correction becomes the trigger. That inverts the assumption every operator brings to an unattended fleet: the more promptly a reviewer undoes a wrong transition, the sooner it comes back. A mechanism in which human correction is indistinguishable from new state converts your best reviewers into the loop's most reliable trigger.
178
+
179
+ ### Why this is a third classification, not a wider `rejection-reclaim`
180
+
181
+ `rejection-reclaim` is anchored on the **ready** lane — an item that reached a `review`/`done`-ward lane and is now back in `$READY`. A reversed repair lands in the **blocked** lane instead: automation moved `$BLOCKED → $READY`, and a human moved it back to `$BLOCKED`. Widening `rejection-reclaim` to cover that would fire it on a lane it was never written for, and its response is wrong for this case anyway — reflecting on rejection evidence and handing it into a re-implementation is what you do when the work was rejected, not when a human said *stop doing that*. The two are **orthogonal** and are **never merged into `rejection-reclaim`**: one teaches a re-implementation, the other withdraws a decision.
182
+
183
+ ### Seam — before any lifecycle write, not at claim
184
+
185
+ Detection runs at the **top of the repair walk for each candidate item, BEFORE the loop writes any lifecycle role**. The reasoning is the claim-time seam's: a write overwrites the current lane, and the signal depends on reading the current lane against history. Detection is a pure read — idempotent, headless-safe, no side effects.
186
+
187
+ ### Classification
188
+
189
+ Detection is a pure read of the item's transition history **and its authorship** (via the vendor access layers). It returns exactly one:
190
+
191
+ | Classification | Condition |
192
+ |---|---|
193
+ | `automation-reversal` | History shows a lifecycle role change **authored by** this flow, and a later **backward** move of that same role **authored by** a human, with no automation move after it. |
194
+ | `no-reversal` | History shows no such pair — the flow made no prior transition, or its transition still stands, or the later move was itself automation. |
195
+ | `unknown` | The vendor history query failed, was inconclusive, or **did not expose authorship**. |
196
+
197
+ ### Authorship is the whole signal — a backward move alone proves nothing
198
+
199
+ The state-only reading is what fails today, so a backward move is **not** sufficient: an item can return to `$BLOCKED` because a genuine new blocker appeared, which is the world moving on and is a legitimate reason to look again. What distinguishes a correction is **who moved it, and against what**. Read authorship from the same history surfaces:
200
+
201
+ - **GitHub** — timeline `LabeledEvent` / `UnlabeledEvent` nodes carry an `actor`; read them through `lisa-github-read-issue`'s Label-Event History surface.
202
+ - **JIRA** — `lisa-atlassian-access operation: changelog key:<K>`; each changelog entry carries an `author` alongside its `from`/`to` status items.
203
+ - **Linear** — `lisa-linear-access operation: history id:<ID>`; each history node carries an `actor` alongside `fromState` / `toState`.
204
+
205
+ An actor is **this flow** when it is the configured automation identity, or when the flow's own marker note (`[lisa-repair-intake]`) sits at that transition's timestamp. Any other actor is a human. Where the surface does not expose an actor at all, the result is `unknown` — never an assumed human.
206
+
207
+ **A marker and a transition on the same item say nothing until you know their order.** A hold label, a comment and a lane move routinely coexist on a reversed item, and co-occurrence reads equally well in both directions — a label applied *before* a transition guards it, the same label applied *after* records that the transition was undone. Sourcing the facts, attributing them correctly and reading the artifact all pass cleanly on an invented sequence; only ordering disconfirms it. Compare timestamps explicitly and treat an unorderable pair as `unknown`.
208
+
209
+ ### Lane names are configuration here too
210
+
211
+ The lanes compared ALWAYS come from `.lisa.config.json` — `github.labels.build.blocked` and `github.labels.build.ready` on GitHub, the JIRA status and Linear state equivalents — resolved per `config-resolution` against the `src/sync/registry.ts` `BUILD_LABEL_DEFAULTS` (`blocked: status:blocked`) fallback. **Never hardcode** a lane string; a project that renames its blocked lane must still detect reversals.
212
+
213
+ ### Never block the sweep
214
+
215
+ `unknown` is a first-class result here exactly as it is at claim time, but the safe direction is the opposite one and must be stated so it is not "simplified" later. At claim time an unreadable history degrades to *implement the item anyway*, because reflection is a bonus. Here an unreadable history degrades to **neither a reversal nor a clearance**: the item is evaluated by the loop's normal rules, unchanged, and the run **never blocks the sweep** on a failed detection. `unknown` must not be read as "no reversal, proceed to re-attempt" any more than as "reversal, suppress" — it removes the signal, it does not supply one.
216
+
217
+ ### Learning-loop exclusion
218
+
219
+ The same learning-loop exclusion applies, for the same reason and by the same markers: an item carrying `[lisa-learning-drop]`, `[lisa-learning-pr]`, `[lisa-learning-upstream-handoff]` or the `learning:needs-triage` label is never classified `automation-reversal`, no matter how it moved. The flow must not remember being corrected about its own learning artifacts.
220
+
221
+ ### The rule — a reversal raises the bar, it never resets the clock
222
+
223
+ On `automation-reversal`:
224
+
225
+ 1. **Do not re-apply the classification that was reversed.** The reversal is the strongest available evidence that the classification was wrong, and re-deriving it from the same facts reaches the same wrong answer.
226
+ 2. **Leave the item where the human put it** and ensure the `human_needed` marker is present. A decision that was overturned needs a person, not another cycle.
227
+ 3. **Name it in the run record** — the reversed transition, and the classification that produced it. A suppression nobody can see is indistinguishable from a loop that found nothing.
228
+ 4. **Re-attempt only on evidence that postdates the reversal** — a blocker state change, or a human comment supplying what was missing. This mirrors the re-proposal rule the proposal half already enforces: the memory is durable, not permanent, and new evidence reopens it. Cite that evidence in the run record.
229
+
230
+ A changed lifecycle role is therefore **no longer sufficient on its own** to warrant a fresh attempt. The consuming loop's recurrence brake must be able to tell "the state changed because the world moved" from "the state changed because a human undid me", which means carrying **authorship and an overturn bit**, not state alone.
231
+
232
+ ### The fix is signed per path — do not apply one sign globally
233
+
234
+ This is the trap in implementing this section, and it will not be caught in review because the wrong version reads as tightening.
235
+
236
+ On a **repair-transition** path, human activity must **stop** being a warrant to re-attempt — that is this whole contract. On a **staleness** path, a human comment is counted as forward progress and *suppresses* action, which means an objecting comment already protects the item. That protection is correct and a global "human activity no longer counts" switches it off while everyone believes a hole was closed.
237
+
238
+ So: apply the inversion **only** where the loop is deciding whether to re-make a transition. Leave every keep-alive and staleness reading exactly as it is. Any consumer of this section states which of its paths are signed which way.
@@ -53,4 +53,17 @@ A **loop-proposed** item that a human **closed as _not planned_** is a durable d
53
53
  - **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.`).
54
54
  - **Concurrency:** search-then-write = convergence, not mutual exclusion — a transient duplicate is closed by the next run; no cross-run lock.
55
55
 
56
- Full contract (classification table, per-vendor bindings, reflection & evidence handoff, Proposal rejection memory): [reference/rejection-detection.md](rejection-detection-reference.mdc).
56
+ ## Automation-reversal memory (repair-side — orthogonal to both halves above)
57
+
58
+ 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.
59
+
60
+ - **Seam:** top of the repair walk for each item, **before any lifecycle write** — after the write the current-lane signal is gone.
61
+ - **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`.
62
+ - **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`.
63
+ - **Lanes from config** (`github.labels.build.blocked` + the JIRA/Linear equivalents, `config-resolution`) — never hardcode.
64
+ - **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.
65
+ - **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.
66
+ - **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.
67
+ - **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.
68
+
69
+ Full contract (classification table, per-vendor bindings, reflection & evidence handoff, Proposal rejection memory, Automation-reversal memory): [reference/rejection-detection.md](rejection-detection-reference.mdc).
@@ -32,3 +32,13 @@ Asking after the research answered the question is also a violation. When gather
32
32
  ## Reporting
33
33
 
34
34
  Record settled decisions in a short clause, such as "auto-merge on, per standing preference" or "scoped app-wide, because the Android finding decides it." This gives the operator a chance to correct the decision without requiring an avoidable question first.
35
+
36
+ ## Re-read the instruction file from disk before escalating
37
+
38
+ This rule is delivered at session start; the re-read it requires happens at the escalation boundary. Re-reading at session start would re-read the same moment the context was built from.
39
+
40
+ Before forming an escalation, handing a decision back, or re-opening a question that may already be settled, open `AGENTS.md` (or `CLAUDE.md`) from disk and read its `## Standing rulings` section. If the file has no such heading, re-read the file.
41
+
42
+ The instruction is to open the file rather than to assess the context, because context completeness is not observable from inside. An instruction file is injected at session start and can arrive shortened, and a cut landing on a section boundary is indistinguishable from a document that never carried that section. Rulings are also written after sessions begin, so an injected copy is a snapshot of the moment the session started.
43
+
44
+ This is a pointer, not a copy. It stays correct as rulings accumulate and survives a shortened copy of the file it points at, because it does not travel inside that file.
@@ -58,3 +58,23 @@ honored.
58
58
  When you take a settled decision, make it auditable in one clause — "auto-merge on, per your standing
59
59
  preference", "scoped app-wide, because the Android finding decides it". That gives the operator the
60
60
  chance to correct it without requiring them to answer first.
61
+
62
+ ## Before you escalate, re-read the instruction file from disk
63
+
64
+ **This rule is delivered at session start; the re-read it demands happens at the escalation
65
+ boundary.** Re-reading at session start would re-read the same moment your context was built from
66
+ and buys nothing.
67
+
68
+ So before you form an escalation, hand a decision back, or re-open a question that may already be
69
+ settled: **open `AGENTS.md` (or `CLAUDE.md`) from disk and read its `## Standing rulings` section.**
70
+ If the file has no such heading, re-read the file.
71
+
72
+ The instruction is to open the file — not to "check whether your context looks complete", because
73
+ **context completeness is not observable from inside.** An instruction file is injected at session
74
+ start and can arrive shortened, and the cut can land on a clean section boundary, where a truncated
75
+ document is indistinguishable from one that never had that section: your copy ends tidily, so you
76
+ conclude you read all of it. Rulings are also written *after* sessions begin — your copy is a
77
+ snapshot of the moment you started, and nothing tells you when it stops being current.
78
+
79
+ This is a **pointer, not a copy**. It stays correct as rulings accumulate, and it survives a
80
+ shortened copy of the file it points at precisely because it does not travel inside that file.
@@ -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: only when `auto_merge=true` (the default).** When `auto_merge=false`,
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: