@codyswann/lisa 4.39.0 → 4.40.1

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 (88) hide show
  1. package/all/copy-overwrite/scripts/lisa-cross-worktree-guard.mjs +333 -0
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +17 -0
  3. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +14 -0
  4. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +28 -0
  5. package/dist/codex/scripts/block-no-verify.sh +21 -0
  6. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  7. package/dist/core/lisa-owned-hash-ledger.js +22 -0
  8. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  9. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  10. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  11. package/dist/core/upstream-evidence-manifest.js +19 -13
  12. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  13. package/dist/opencode/plugin-templates/block-managed-file-edits.sh +14 -0
  14. package/dist/opencode/plugin-templates/block-no-verify.sh +28 -0
  15. package/npm-package/create-only/.github/workflows/publish-to-npm.yml +48 -19
  16. package/package.json +4 -4
  17. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa/hooks/block-direct-issue-create.sh +17 -0
  20. package/plugins/lisa/hooks/block-managed-file-edits.sh +14 -0
  21. package/plugins/lisa/hooks/block-no-verify.agy.sh +21 -0
  22. package/plugins/lisa/hooks/block-no-verify.sh +28 -0
  23. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +17 -0
  24. package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +14 -0
  25. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +21 -0
  26. package/plugins/lisa-agy/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  30. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +17 -0
  34. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +14 -0
  35. package/plugins/lisa-copilot/hooks/block-no-verify.sh +28 -0
  36. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +17 -0
  38. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +14 -0
  39. package/plugins/lisa-cursor/hooks/block-no-verify.sh +28 -0
  40. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-expo-agy/plugin.json +1 -1
  43. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  63. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails-agy/plugin.json +1 -1
  68. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  73. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  78. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/src/base/hooks/block-direct-issue-create.sh +17 -0
  81. package/plugins/src/base/hooks/block-managed-file-edits.sh +14 -0
  82. package/plugins/src/base/hooks/block-no-verify.agy.sh +21 -0
  83. package/plugins/src/base/hooks/block-no-verify.sh +28 -0
  84. package/rails/copy-contents/scripts/lisa-mutation.sh +8 -1
  85. package/scripts/two-channel-couplings.json +1 -1
  86. package/typescript/copy-contents/.husky/pre-commit +40 -0
  87. package/typescript/copy-overwrite/.prettierignore +1 -0
  88. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +20 -14
@@ -961,6 +961,10 @@ def resolve_script(token):
961
961
 
962
962
  SHELL_COMMAND_STRING = object()
963
963
  SHELL_STDIN = object()
964
+ # `-n` / `--noexec`: the shell READS and parses its operand, then exits without
965
+ # executing a line of it. Distinct from the two above because nothing runs at
966
+ # all — there is no payload to scan and no stdin script to wait for.
967
+ SHELL_NOEXEC = object()
964
968
 
965
969
 
966
970
  def shell_execution_operand(args):
@@ -991,12 +995,25 @@ def shell_execution_operand(args):
991
995
  index += 1
992
996
  continue
993
997
  if token.startswith("--"):
998
+ if token == "--noexec":
999
+ return SHELL_NOEXEC
994
1000
  if token not in SHELL_LONG_OPTIONS_NO_VALUE:
995
1001
  opaque_option_seen = True
996
1002
  index += 1
997
1003
  continue
998
1004
  if "c" in token[1:]:
999
1005
  return SHELL_COMMAND_STRING
1006
+ # `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
1007
+ # a command position while provably executing nothing, so it is the
1008
+ # exact seam between reading and running — and reach got it wrong in
1009
+ # BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
1010
+ # of any file merely DISCUSSING the bypass was refused, which is most of
1011
+ # this repository's own guards and every husky hook.
1012
+ #
1013
+ # `bash <file>` still executes and is still refused. Noexec is the
1014
+ # distinction, not the program: this is not a read-only exemption.
1015
+ if "n" in token[1:]:
1016
+ return SHELL_NOEXEC
1000
1017
  index += 1
1001
1018
  return args[index] if index < len(args) else SHELL_STDIN
1002
1019
 
@@ -1041,6 +1058,10 @@ def executed_scripts(tokens):
1041
1058
  continue
1042
1059
  if operand is SHELL_COMMAND_STRING:
1043
1060
  continue
1061
+ if operand is SHELL_NOEXEC:
1062
+ # Nothing runs, so there is nothing to follow and nothing
1063
+ # to fail closed about.
1064
+ continue
1044
1065
  else:
1045
1066
  continue
1046
1067
  if operand is None:
@@ -24,6 +24,13 @@
24
24
  # `source`, `.`, `bash < nv.sh`, and an interpreter a `find -exec` or
25
25
  # `xargs` puts at a command position.
26
26
  #
27
+ # NOT vector 11: `bash -n nv.sh`. `-n` is noexec — the shell reads and parses
28
+ # the file, then exits without running a line — so a syntax check is the one
29
+ # shape that puts a path at a command position while executing nothing. It used
30
+ # to be refused, which made `bash -n` unusable on this file, on every husky
31
+ # hook, and on most of this repository (#3781). The distinction is noexec, not
32
+ # the program: `bash nv.sh` still executes and is still refused.
33
+ #
27
34
  # The line below is what lets `lisa apply` tell a downstream copy of this guard
28
35
  # that is BEHIND from one that is AHEAD. Byte comparison cannot: both look like
29
36
  # "differs from mine", and guessing "behind" is how a fork's stronger guard gets
@@ -1031,6 +1038,10 @@ def resolve_script(token):
1031
1038
 
1032
1039
  SHELL_COMMAND_STRING = object()
1033
1040
  SHELL_STDIN = object()
1041
+ # `-n` / `--noexec`: the shell READS and parses its operand, then exits without
1042
+ # executing a line of it. Distinct from the two above because nothing runs at
1043
+ # all — there is no payload to scan and no stdin script to wait for.
1044
+ SHELL_NOEXEC = object()
1034
1045
 
1035
1046
 
1036
1047
  def shell_execution_operand(args):
@@ -1061,12 +1072,25 @@ def shell_execution_operand(args):
1061
1072
  index += 1
1062
1073
  continue
1063
1074
  if token.startswith("--"):
1075
+ if token == "--noexec":
1076
+ return SHELL_NOEXEC
1064
1077
  if token not in SHELL_LONG_OPTIONS_NO_VALUE:
1065
1078
  opaque_option_seen = True
1066
1079
  index += 1
1067
1080
  continue
1068
1081
  if "c" in token[1:]:
1069
1082
  return SHELL_COMMAND_STRING
1083
+ # `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
1084
+ # a command position while provably executing nothing, so it is the
1085
+ # exact seam between reading and running — and reach got it wrong in
1086
+ # BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
1087
+ # of any file merely DISCUSSING the bypass was refused, which is most of
1088
+ # this repository's own guards and every husky hook.
1089
+ #
1090
+ # `bash <file>` still executes and is still refused. Noexec is the
1091
+ # distinction, not the program: this is not a read-only exemption.
1092
+ if "n" in token[1:]:
1093
+ return SHELL_NOEXEC
1070
1094
  index += 1
1071
1095
  return args[index] if index < len(args) else SHELL_STDIN
1072
1096
 
@@ -1111,6 +1135,10 @@ def executed_scripts(tokens):
1111
1135
  continue
1112
1136
  if operand is SHELL_COMMAND_STRING:
1113
1137
  continue
1138
+ if operand is SHELL_NOEXEC:
1139
+ # Nothing runs, so there is nothing to follow and nothing
1140
+ # to fail closed about.
1141
+ continue
1114
1142
  else:
1115
1143
  continue
1116
1144
  if operand is None:
@@ -70,11 +70,18 @@ if [ -z "$BASE" ]; then
70
70
  exit 0
71
71
  fi
72
72
 
73
- CHANGED="$(git diff --name-only --diff-filter=ACMR "${BASE}...HEAD" -- 'app/**/*.rb' 'lib/**/*.rb' || true)"
73
+ # Working tree, not "${BASE}...HEAD": mutant reads the files on disk, so a
74
+ # subject whose only change is uncommitted is invisible to committed state and
75
+ # this pre-check would report "nothing to mutate" while a changed subject sat
76
+ # right there. Parity with the TypeScript gate's selectChangedTargets.
77
+ CHANGED="$(git diff --name-only --diff-filter=ACMR "${BASE}" -- 'app/**/*.rb' 'lib/**/*.rb' || true)"
74
78
  if [ -z "$CHANGED" ]; then
75
79
  echo "⚪ Mutation gate: no changed Ruby files under app/ or lib/ vs ${SINCE}. Nothing to mutate."
76
80
  exit 0
77
81
  fi
78
82
 
83
+ # Known gap, deliberately not papered over: mutant's own --since scoping is
84
+ # committed-based, so the working-tree correction above reaches the pre-check
85
+ # but not mutant's subject selection. There is no mutant flag for it.
79
86
  echo "🧬 Mutation gate: running mutant (--since ${SINCE}) on changed subjects..."
80
87
  exec bundle exec mutant run --since "$SINCE"
@@ -7,7 +7,7 @@
7
7
  "workflows": 22,
8
8
  "steps": 716,
9
9
  "couplings": 34,
10
- "inventory": 73
10
+ "inventory": 74
11
11
  },
12
12
  "counts": {
13
13
  "package-backed": 10,
@@ -107,6 +107,46 @@ if [ -f ".lintstagedrc.json" ]; then
107
107
  fi
108
108
  fi
109
109
 
110
+ # ---------------------------------------------------------------------------
111
+ # Cross-worktree staged-blob guard.
112
+ #
113
+ # Refuses a commit that stages a NEW file byte-identical to uncommitted work
114
+ # sitting in a different linked worktree. Where several agents share one
115
+ # repository through worktrees, one agent's in-progress file can be committed
116
+ # by another: the file is real, the branch is real, the commit succeeds and
117
+ # every other gate passes, so nothing reports it. Confirmed once — a 284-line
118
+ # test file staged in one worktree, committed an hour later from elsewhere,
119
+ # byte-identical, under a reworded subject.
120
+ #
121
+ # HERE RATHER THAN IN THE GATE REGISTRY, deliberately. This is a property of
122
+ # the machine — which other worktrees exist and what is sitting in them —
123
+ # not of the project, so there is no task a project could declare to prove it.
124
+ # It also has to run on EVERY commit: the whole failure mode is that nobody
125
+ # notices, so a check that waits to be invoked is a check that was not running
126
+ # on the commit where it mattered.
127
+ #
128
+ # Fails OPEN when the script is missing, unlike the lint-staged preflight
129
+ # above. That preflight guards a tool that reports success for work it did not
130
+ # do, so skipping it cannot be read as a pass. This one only ever adds a
131
+ # refusal — a repository without it is exactly as safe as it was before the
132
+ # guard existed, and blocking every commit in a project that has not yet
133
+ # re-applied templates would be a worse trade than the window it closes.
134
+ # ---------------------------------------------------------------------------
135
+ CROSS_WORKTREE_GUARD="node_modules/@codyswann/lisa/all/copy-overwrite/scripts/lisa-cross-worktree-guard.mjs"
136
+ if [ ! -f "$CROSS_WORKTREE_GUARD" ]; then
137
+ CROSS_WORKTREE_GUARD="scripts/lisa-cross-worktree-guard.mjs"
138
+ fi
139
+ if [ ! -f "$CROSS_WORKTREE_GUARD" ]; then
140
+ CROSS_WORKTREE_GUARD="all/copy-overwrite/scripts/lisa-cross-worktree-guard.mjs"
141
+ fi
142
+ if [ -f "$CROSS_WORKTREE_GUARD" ] && command -v node >/dev/null 2>&1; then
143
+ node "$CROSS_WORKTREE_GUARD"
144
+ CROSS_WORKTREE_GUARD_STATUS=$?
145
+ if [ $CROSS_WORKTREE_GUARD_STATUS -ne 0 ]; then
146
+ exit $CROSS_WORKTREE_GUARD_STATUS
147
+ fi
148
+ fi
149
+
110
150
  # ---------------------------------------------------------------------------
111
151
  # Gate-driven checks.
112
152
  #
@@ -80,6 +80,7 @@ scripts/check-verification-coverage.mjs
80
80
  scripts/classify-maestro-failures.mjs
81
81
  scripts/lisa-command-envelope.mjs
82
82
  scripts/lisa-commit-msg-gates.mjs
83
+ scripts/lisa-cross-worktree-guard.mjs
83
84
  scripts/lisa-destructive-guard.mjs
84
85
  scripts/lisa-environment-prepare.mjs
85
86
  scripts/lisa-floor-collisions.mjs
@@ -1388,6 +1388,15 @@ export const parseChangedLineRanges = patch => {
1388
1388
 
1389
1389
  /**
1390
1390
  * Stryker mutation ranges for one changed file.
1391
+ *
1392
+ * The diff runs against the WORKING TREE, not `base...HEAD`. Stryker mutates
1393
+ * the files on disk, so a window derived from committed state describes lines
1394
+ * that may no longer be where it says they are: any uncommitted edit above the
1395
+ * change shifts the numbering out from under the window, and the run then
1396
+ * mutates whatever now occupies those lines — measured once as 37 lines of pure
1397
+ * comment, scored, and reported as a verdict about the change. It could as
1398
+ * easily have produced a pass. Diffing what Stryker actually reads is what
1399
+ * makes the window and the subject the same thing.
1391
1400
  * @param {string} cwd - Project root.
1392
1401
  * @param {string} base - Merge-base sha.
1393
1402
  * @param {string} file - Repository-relative path.
@@ -1395,14 +1404,7 @@ export const parseChangedLineRanges = patch => {
1395
1404
  */
1396
1405
  export const selectChangedLineRanges = (cwd, base, file) =>
1397
1406
  parseChangedLineRanges(
1398
- git(cwd, [
1399
- "diff",
1400
- "--unified=0",
1401
- "--diff-filter=ACMR",
1402
- `${base}...HEAD`,
1403
- "--",
1404
- file,
1405
- ])
1407
+ git(cwd, ["diff", "--unified=0", "--diff-filter=ACMR", base, "--", file])
1406
1408
  ).map(({ start, end }) => `${file}:${start}-${end}`);
1407
1409
 
1408
1410
  /**
@@ -1416,12 +1418,11 @@ export const selectChangedLineRanges = (cwd, base, file) =>
1416
1418
  * files no mutation tool here reaches.
1417
1419
  */
1418
1420
  export const selectChangedTargets = (cwd, base, patterns) => {
1419
- const changed = git(cwd, [
1420
- "diff",
1421
- "--name-only",
1422
- "--diff-filter=ACMRD",
1423
- `${base}...HEAD`,
1424
- ])
1421
+ // Working tree, not `base...HEAD`, for the same reason the line ranges are:
1422
+ // a target whose only change is uncommitted is invisible to committed state,
1423
+ // so the gate printed "0 mutate targets" and exited 0 while Stryker, pointed
1424
+ // at the same tree, had a changed target in front of it.
1425
+ const changed = git(cwd, ["diff", "--name-only", "--diff-filter=ACMRD", base])
1425
1426
  .split("\n")
1426
1427
  .map(file => file.trim())
1427
1428
  .filter(Boolean);
@@ -2115,6 +2116,11 @@ export const reportRun = (cwd, result) => {
2115
2116
  export const runGate = (cwd = process.cwd(), argv = []) => {
2116
2117
  const gate = readGate(cwd);
2117
2118
  const enabled = envFlag("MUTATION_ENABLED") ?? gate.enabled === true;
2119
+ // Reads like the LOCAL branch and is not: `resolveDiffBase` probes
2120
+ // `origin/<since>` first and only falls back to the local name, so a stale
2121
+ // local `main` does not drag every commit it is missing into the diff. Said
2122
+ // here because the line alone has already been read the other way and filed
2123
+ // as a defect; the behaviour is pinned by a test rather than by this note.
2118
2124
  const since = process.env.MUTATION_SINCE || gate.since || "main";
2119
2125
 
2120
2126
  if (!enabled) {