@codyswann/lisa 4.45.4 → 4.45.6

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 (81) hide show
  1. package/all/copy-overwrite/scripts/lisa-cross-worktree-guard.mjs +6 -0
  2. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +74 -0
  3. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +115 -23
  4. package/all/copy-overwrite/scripts/lisa-worktree-guard.mjs +456 -0
  5. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  6. package/dist/core/lisa-owned-hash-ledger.js +28 -0
  7. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  8. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  9. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.js +21 -10
  11. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  12. package/dist/opencode/plugin-templates/parity-safety-net.sh +74 -0
  13. package/package.json +5 -4
  14. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa/hooks/cleanup-stale-worktrees.sh +42 -1
  17. package/plugins/lisa/hooks/parity-safety-net.sh +74 -0
  18. package/plugins/lisa/hooks/worktree-binding-guard.mjs +115 -23
  19. package/plugins/lisa-agy/hooks/parity-safety-net.sh +74 -0
  20. package/plugins/lisa-agy/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  24. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/hooks/cleanup-stale-worktrees.sh +42 -1
  28. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +74 -0
  29. package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +115 -23
  30. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cursor/hooks/cleanup-stale-worktrees.sh +42 -1
  32. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +74 -0
  33. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +115 -23
  34. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo-agy/plugin.json +1 -1
  37. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  57. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails-agy/plugin.json +1 -1
  62. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  67. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  72. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/src/base/hooks/cleanup-stale-worktrees.sh +42 -1
  75. package/plugins/src/base/hooks/parity-safety-net.sh +74 -0
  76. package/plugins/src/base/hooks/worktree-binding-guard.mjs +115 -23
  77. package/scripts/check-cwd-resolution-corpus.mjs +163 -0
  78. package/scripts/cleanup-worktrees.sh +36 -3
  79. package/scripts/two-channel-couplings.json +1 -1
  80. package/typescript/copy-overwrite/.prettierignore +1 -0
  81. package/typescript/create-only/.github/workflows/review-evidence.yml +20 -1
@@ -304,6 +304,11 @@ function foreignTreeIn(text, bound, cwd) {
304
304
  for (const raw of code.match(/[^\s'";|&()<>]+/gu) ?? []) {
305
305
  const token = raw.replace(/^["']|["']$/gu, "");
306
306
  if (!token.includes("/")) continue;
307
+ // A null base means an unliteral `cd` left the directory this script runs
308
+ // from unknown. An ABSOLUTE token is unambiguous either way; a relative one
309
+ // is exactly the thing that cannot be placed, so it is skipped rather than
310
+ // resolved against a directory picked for having been handy.
311
+ if (!isAbsolute(token) && cwd === null) continue;
307
312
  const candidate = isAbsolute(token) ? token : join(cwd, token);
308
313
  let root;
309
314
  try {
@@ -317,6 +322,63 @@ function foreignTreeIn(text, bound, cwd) {
317
322
  return null;
318
323
  }
319
324
 
325
+ /**
326
+ * A `cd` target this guard cannot turn into a literal directory.
327
+ *
328
+ * Mirrors `parity-safety-net.sh`'s `follow_cd` test exactly, and the mirror is
329
+ * the point: two spellings of one rule are how the guards drift.
330
+ */
331
+ const UNLITERAL_CD_TARGET = /[$`*?[{]/u;
332
+
333
+ /**
334
+ * The literal target of a leading `cd`, `""` for a bare `cd`, or null when the
335
+ * segment does not change directory.
336
+ *
337
+ * A bare `cd` is `cd "$HOME"`. Leaving the previous directory in place for it
338
+ * would resolve later tokens from a directory the shell has already left.
339
+ * @param segment - One statement from the command
340
+ * @returns The target text, or null when this segment is not a `cd`
341
+ */
342
+ function cdTarget(segment) {
343
+ const tokens = tokenise(segment);
344
+ if (tokens.length === 0 || tokens[0] !== "cd") return null;
345
+ return tokens[1] ?? "";
346
+ }
347
+
348
+ /**
349
+ * Apply one `cd` to the directory currently in force.
350
+ *
351
+ * Contract points 1-4 of `cwd-resolution-corpus.json`: a literal target
352
+ * REPLACES the directory rather than joining a list of candidates — the
353
+ * ordering was #3933's entire defect; a relative target composes against the
354
+ * directory in force; a target that is not literal text yields unknown; and
355
+ * unknown is sticky only until the next resolvable `cd`, because this returns
356
+ * a fresh answer for each one.
357
+ * @param current - Directory in force, or null when it is already unknown
358
+ * @param target - Literal `cd` target, `""` for a bare `cd`
359
+ * @returns The new directory, or null when it cannot be known
360
+ */
361
+ function applyCd(current, target) {
362
+ const home = homedir();
363
+ let value = target === "" ? home : target;
364
+ if (value === "~") value = home;
365
+ else if (value.startsWith("~/")) value = join(home, value.slice(2));
366
+ if (
367
+ value === "" ||
368
+ value.startsWith("-") ||
369
+ UNLITERAL_CD_TARGET.test(value)
370
+ ) {
371
+ return null;
372
+ }
373
+ if (!isAbsolute(value) && current === null) return null;
374
+ const next = isAbsolute(value) ? resolve(value) : resolve(current, value);
375
+ try {
376
+ return statSync(next).isDirectory() ? next : null;
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+
320
382
  /**
321
383
  * The foreign worktree an executed script reaches, or null.
322
384
  *
@@ -340,41 +402,71 @@ function foreignTreeIn(text, bound, cwd) {
340
402
  * disagreeing. The scripted form is the one neither covers, and it is the only
341
403
  * cell this arm fills.
342
404
  *
343
- * ## A relative script path is resolved against `payload.cwd`, and that is a
344
- * KNOWN limitation shared with `parity-safety-net.sh`
405
+ * ## A relative script path is resolved against the cwd the command will have
406
+ *
407
+ * A leading `cd` establishes the directory this guard resolves relative tokens
408
+ * from, because that is the directory the shell will be in once the command
409
+ * runs. Resolving against `payload.cwd` instead was CodySwannGT/lisa#3933's
410
+ * defect, inherited here knowingly and fixed in CodySwannGT/lisa#3952: on a
411
+ * machine running many worktrees of one repository, every tree holds its own
412
+ * copy of every script at the same relative path, and they differ by whatever
413
+ * each lane is doing.
345
414
  *
346
- * CodySwannGT/lisa#3933, filed against that guard, is the same defect: a
347
- * relative token is resolved against the hook process's working directory, not
348
- * against the directory the command will have once it runs its own leading
349
- * `cd`. So `cd <other tree> && bash scripts/foo.sh` reads THIS tree's copy of
350
- * `scripts/foo.sh` and reports on it with full confidence.
415
+ * ## One contract, two implementations, and why that is the honest answer
351
416
  *
352
- * It matters here for the same reason it matters there: on a machine running
353
- * many worktrees of one repository, every tree holds its own copy of every
354
- * script at the same relative path, and they differ by whatever each lane is
355
- * doing. A confident scan of the wrong copy is wrong in both directions, and
356
- * the silent one — the copy that will run reaches out, the copy that was read
357
- * does not — is the direction this arm exists to prevent.
417
+ * `parity-safety-net.sh` answers this same question — "which file will this
418
+ * command actually execute?" — in POSIX shell, where `follow_cd` mutates
419
+ * file-scope state that `follow_locate` reads. This guard is Node. **There is
420
+ * no call that reaches from here into a shell function's file-scope state**, so
421
+ * the duplication is not a factoring somebody skipped; it is one idea that
422
+ * cannot cross a language boundary as a function call.
358
423
  *
359
- * NOT fixed here on purpose. #3933 is assigned and its options (honour a
360
- * leading `cd`, or fail closed when the effective cwd is uncertain) belong in
361
- * one place rather than two: a second implementation of the same resolution is
362
- * how two guards drift into disagreeing about which file a command runs.
363
- * When that lands, this call site should adopt whatever it produces.
424
+ * What binds the two is a written contract and a corpus of rows that BOTH
425
+ * guards are driven over in CI, so a divergence fails a test on the commit that
426
+ * causes it rather than surfacing later as a wrong verdict. Contract and corpus
427
+ * live in `tests/unit/hooks/support/cwd-resolution-corpus.json`.
364
428
  *
365
- * The exposure here is narrower than there. This arm is reached only after the
366
- * session has been confirmed to be in its bound worktree, so the common case
367
- * is a relative path resolving inside the tree the session is actually in.
429
+ * This is explicitly NOT unification, and CodySwannGT/lisa#3952 records it as
430
+ * such: two implementations remain. **A behaviour with no row in the corpus is
431
+ * free to diverge — add a row when you add a branch.**
432
+ *
433
+ * ## The two guards agree on the RESOLUTION, not on the verdict
434
+ *
435
+ * When the effective directory cannot be known, both guards decline to resolve
436
+ * the token. What they then do differs, and that difference is deliberate:
437
+ * `parity-safety-net.sh` refuses, because a destructive command it cannot see
438
+ * is the thing it exists to stop. This guard fails open, which is the doctrine
439
+ * every other undecidable case in this file follows. The corpus asserts the
440
+ * resolved path, not the exit code, precisely so that one contract can bind two
441
+ * guards whose fail directions are correctly different.
368
442
  *
369
443
  * Fails OPEN and says so, like every other undecidable case in this file.
370
444
  */
371
445
  function scriptReachingForeignTree(payload, bound) {
372
446
  const command = payload.tool_input?.command;
373
447
  if (typeof command !== "string" || !command) return null;
448
+ let cwd = payload.cwd;
374
449
  for (const segment of command.split(COMMAND_SEPARATORS)) {
450
+ const target = cdTarget(segment);
451
+ if (target !== null) {
452
+ cwd = applyCd(cwd, target);
453
+ continue;
454
+ }
375
455
  const named = executedPath(segment);
376
456
  if (!named) continue;
377
- const path = isAbsolute(named) ? named : join(payload.cwd, named);
457
+ if (!isAbsolute(named) && cwd === null) {
458
+ // Contract point 3: the effective directory is unknown, so WHICH copy of
459
+ // a same-named script would run is unknown too. Declining to resolve is
460
+ // the half shared with parity-safety-net.sh; failing open rather than
461
+ // refusing is this guard's own doctrine, and the corpus binds the two
462
+ // guards on the resolution precisely so it does not have to bind them
463
+ // on a verdict one of them would be wrong to reach.
464
+ say(
465
+ `cannot tell which directory ${named} would run from; NOT enforcing on it`
466
+ );
467
+ continue;
468
+ }
469
+ const path = isAbsolute(named) ? resolve(named) : resolve(cwd, named);
378
470
  let text;
379
471
  try {
380
472
  if (statSync(path).size > SCRIPT_READ_LIMIT) {
@@ -386,7 +478,7 @@ function scriptReachingForeignTree(payload, bound) {
386
478
  // Not a readable file: nothing was executed that this can read.
387
479
  continue;
388
480
  }
389
- const foreign = foreignTreeIn(text, bound, payload.cwd);
481
+ const foreign = foreignTreeIn(text, bound, cwd);
390
482
  if (foreign) return { script: path, foreign };
391
483
  }
392
484
  return null;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.45.4",
3
+ "version": "4.45.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -16,8 +16,15 @@
16
16
  # * no modified or staged TRACKED files (real work is never deleted)
17
17
  # * its HEAD commit is reachable from some remote ref (nothing unpushed)
18
18
  # * its directory mtime is older than LISA_WORKTREE_MAX_AGE_DAYS (default 7)
19
+ # * lisa-worktree-guard reports no uncommitted bytes that exist in no commit
19
20
  # Untracked-only dirt does NOT block removal — that junk is exactly what
20
- # defeats the built-in sweep. Set LISA_WORKTREE_CLEANUP=off to disable.
21
+ # defeats the built-in sweep. But "untracked" and "junk" are not the same word:
22
+ # a brand-new source file an agent has written and not yet committed is
23
+ # untracked too, and this sweep would delete it (CodySwannGT/lisa#3863). The
24
+ # guard draws the line by CONTENT rather than by tracking state — a file whose
25
+ # exact bytes are already in some commit is free to delete, one whose bytes
26
+ # exist nowhere else is not, and ignored paths (node_modules, build output)
27
+ # never reach the question. Set LISA_WORKTREE_CLEANUP=off to disable.
21
28
 
22
29
  set -u
23
30
 
@@ -37,6 +44,33 @@ esac
37
44
  wt_root="$repo_root/.claude/worktrees"
38
45
  [ -d "$wt_root" ] || exit 0
39
46
 
47
+ # Resolve the content-reachability guard once. Three homes, in the order a host
48
+ # project actually has them: the applied copy, the installed package, and Lisa's
49
+ # own delivery tree (Lisa does not apply this script to itself).
50
+ guard=""
51
+ for candidate in \
52
+ "$repo_root/scripts/lisa-worktree-guard.mjs" \
53
+ "$repo_root/node_modules/@codyswann/lisa/all/copy-overwrite/scripts/lisa-worktree-guard.mjs" \
54
+ "$repo_root/all/copy-overwrite/scripts/lisa-worktree-guard.mjs"; do
55
+ if [ -f "$candidate" ]; then
56
+ guard="$candidate"
57
+ break
58
+ fi
59
+ done
60
+ command -v node > /dev/null 2>&1 || guard=""
61
+
62
+ # Whether a worktree's uncommitted content is safe to destroy. With the guard
63
+ # present the question is answered by content; without it (no node, no delivered
64
+ # script) the sweep falls back to the conservative shape — ANY non-ignored
65
+ # untracked file keeps the tree. A missing guard must not read as consent.
66
+ removal_allowed() {
67
+ if [ -n "$guard" ]; then
68
+ node "$guard" check "$1" > /dev/null 2>&1
69
+ return $?
70
+ fi
71
+ [ -z "$(git -C "$1" ls-files --others --exclude-standard 2> /dev/null | head -1)" ]
72
+ }
73
+
40
74
  now=$(date +%s)
41
75
  max_age_secs=$((MAX_AGE_DAYS * 86400))
42
76
  removed=0
@@ -66,6 +100,13 @@ for wt in "$wt_root"/*/; do
66
100
  sha=$(git -C "$wt" rev-parse HEAD 2>/dev/null) || continue
67
101
  [ -n "$(git -C "$wt" branch -r --contains "$sha" 2>/dev/null | head -1)" ] || continue
68
102
 
103
+ # Content gate: refuse to destroy bytes that live in no commit. The
104
+ # tracked-file check above answers a different question — a file an agent
105
+ # has written but never `git add`ed is untracked, so that check reports the
106
+ # tree clean while the whole deliverable sits in it. Only commits are shared
107
+ # between worktrees; this directory's working files and index are not.
108
+ removal_allowed "$wt" || continue
109
+
69
110
  # git worktree lock (held during live agent execution) blocks removal;
70
111
  # --force only clears untracked junk, never the gates above.
71
112
  git -C "$repo_root" worktree remove --force "$wt" 2>/dev/null && removed=$((removed + 1))