@codyswann/lisa 4.45.7 → 4.45.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 (81) hide show
  1. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +21 -2
  2. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +68 -0
  3. package/all/copy-overwrite/scripts/lisa-work-item.mjs +56 -13
  4. package/all/merge/.claude/settings.json +10 -0
  5. package/dist/configs/vitest/scratch-global-setup.d.ts +9 -3
  6. package/dist/configs/vitest/scratch-global-setup.d.ts.map +1 -1
  7. package/dist/configs/vitest/scratch-global-setup.js +23 -10
  8. package/dist/configs/vitest/scratch-global-setup.js.map +1 -1
  9. package/dist/configs/vitest/scratch-tmpdir-trust.d.ts +48 -0
  10. package/dist/configs/vitest/scratch-tmpdir-trust.d.ts.map +1 -0
  11. package/dist/configs/vitest/scratch-tmpdir-trust.js +194 -0
  12. package/dist/configs/vitest/scratch-tmpdir-trust.js.map +1 -0
  13. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  14. package/dist/core/lisa-owned-hash-ledger.js +14 -0
  15. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  16. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  17. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  18. package/dist/core/upstream-evidence-manifest.js +14 -9
  19. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  20. package/package.json +5 -5
  21. package/plugins/lisa/.claude-plugin/plugin.json +9 -1
  22. package/plugins/lisa/.codex-plugin/hooks.json +8 -0
  23. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa/hooks/worktree-binding-guard.mjs +68 -0
  25. package/plugins/lisa-agy/plugin.json +1 -1
  26. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  29. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/.claude-plugin/plugin.json +9 -1
  32. package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +68 -0
  33. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cursor/hooks/hooks.json +3 -0
  35. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +68 -0
  36. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-expo-agy/plugin.json +1 -1
  39. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  54. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  59. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-agy/plugin.json +1 -1
  64. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  69. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  74. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/src/base/.claude-plugin/plugin.json +8 -0
  77. package/plugins/src/base/hooks/worktree-binding-guard.mjs +68 -0
  78. package/scripts/check-shell-guard-refusal-coverage.mjs +49 -3
  79. package/scripts/lib/shell-guard-trace.mjs +26 -8
  80. package/scripts/lisa-enforcement-fallback.sh +21 -2
  81. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +244 -3
package/package.json CHANGED
@@ -65,7 +65,7 @@
65
65
  "check:empty-subject-guards": "node scripts/check-empty-subject-guards.mjs",
66
66
  "check:cwd-resolution-corpus": "node scripts/check-cwd-resolution-corpus.mjs",
67
67
  "check:probe-absence-direction": "node scripts/check-probe-absence-direction.mjs",
68
- "check:shell-guard-refusals": "bun run build && $npm_execpath run lisa-test-run -- --adapter direct -- node scripts/check-shell-guard-refusal-coverage.mjs",
68
+ "check:shell-guard-refusals": "bun run build && $npm_execpath run lisa-test-run -- --adapter direct -- node scripts/check-shell-guard-refusal-coverage.mjs --trace-out .lisa/shell-guard-trace.jsonl",
69
69
  "check:workflow-inputs": "node scripts/detect-stale-workflow-inputs.mjs --project . --project typescript/create-only --project rails/create-only --json",
70
70
  "check:conflict-markers": "node all/copy-overwrite/scripts/check-conflict-markers.mjs",
71
71
  "check:vacuous-required-checks": "node typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs --vacuity",
@@ -176,7 +176,7 @@
176
176
  "zod-validation-error": "^4.0.0"
177
177
  },
178
178
  "name": "@codyswann/lisa",
179
- "version": "4.45.7",
179
+ "version": "4.45.9",
180
180
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
181
181
  "main": "dist/index.js",
182
182
  "exports": {
@@ -324,7 +324,7 @@
324
324
  "test": "tests"
325
325
  },
326
326
  "types": "./dist/index.d.ts",
327
- "lisaReleaseCommit": "5de1e912eae997ab749d9af9df76095e2be3232b",
328
- "gitHead": "5de1e912eae997ab749d9af9df76095e2be3232b",
329
- "lisaReleaseTag": "v4.45.7"
327
+ "lisaReleaseCommit": "c0d381c7a25074565c6c1ec3fb10dde1af36b937",
328
+ "gitHead": "c0d381c7a25074565c6c1ec3fb10dde1af36b937",
329
+ "lisaReleaseTag": "v4.45.9"
330
330
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -293,6 +293,14 @@
293
293
  "command": "command -v entire >/dev/null 2>&1 && entire hooks claude-code session-start || true"
294
294
  }
295
295
  ]
296
+ },
297
+ {
298
+ "hooks": [
299
+ {
300
+ "type": "command",
301
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
302
+ }
303
+ ]
296
304
  }
297
305
  ],
298
306
  "SubagentStart": [
@@ -206,6 +206,14 @@
206
206
  "command": "${PLUGIN_ROOT}/hooks/secrets-preflight.sh"
207
207
  }
208
208
  ]
209
+ },
210
+ {
211
+ "hooks": [
212
+ {
213
+ "type": "command",
214
+ "command": "${PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
215
+ }
216
+ ]
209
217
  }
210
218
  ],
211
219
  "SubagentStart": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -547,6 +547,63 @@ function claimedRoot(toolInput, cwd) {
547
547
  return null;
548
548
  }
549
549
 
550
+ /**
551
+ * Record where the session STARTED, so its first guarded call is checked
552
+ * against a baseline rather than establishing one.
553
+ *
554
+ * ## Why the first call could not be checked before
555
+ *
556
+ * It established the baseline instead of testing one, which is
557
+ * trust-on-first-use — and TOFU is only as safe as its first use. Here the
558
+ * first use is assigned by the harness, unasked (CodySwannGT/lisa#3864), so the
559
+ * one moment the guard trusted was the moment the session had least control
560
+ * over. A session already sitting in a foreign worktree when it first acted
561
+ * bound to that tree and was never refused, and every later call was then
562
+ * checked faithfully against the wrong baseline — defending the session INTO
563
+ * the wrong worktree rather than out of it (CodySwannGT/lisa#3955).
564
+ *
565
+ * ## Why an earlier observation separates what a cleverer check cannot
566
+ *
567
+ * At the first guarded call, "I was displaced before I acted" and "I
568
+ * legitimately started here" present identically: one session id, one observed
569
+ * root, no prior state. No comparison can tell them apart, because the
570
+ * information is not in the hook's inputs at that instant.
571
+ *
572
+ * At session start it is. The displacement mechanism actually observed is a
573
+ * PEER's `EnterWorktree` moving this session (CodySwannGT/lisa#3712), which
574
+ * happens during the session — after it has started. A baseline taken at start
575
+ * is therefore recorded BEFORE the event that moves it, and the first guarded
576
+ * call becomes an ordinary comparison against a baseline that already exists.
577
+ * It reaches the same refusal a later displacement produces, because by then it
578
+ * is one.
579
+ *
580
+ * ## What this does NOT achieve
581
+ *
582
+ * #3955 asks for a baseline from the ASSIGNER rather than from observation.
583
+ * That is not what this is and Lisa cannot build it: Lisa does not own
584
+ * `EnterWorktree`, and nothing the harness writes names a session's assigned
585
+ * worktree in a form a hook can read. This is still an observation, just an
586
+ * earlier one. It closes the window between session start and the first guarded
587
+ * call; it leaves open a session LAUNCHED already displaced, where start
588
+ * observes the foreign tree and trust-on-first-use still applies.
589
+ *
590
+ * Returns nothing on purpose, like its sibling below: SessionStart has no call
591
+ * to refuse, so every path through it allows.
592
+ * @param sessionId - The session this baseline belongs to
593
+ * @param observed - The worktree root the session started in
594
+ */
595
+ function recordBaseline(sessionId, observed) {
596
+ // Never overwrite. A resumed session, a reconnect, or anything else that
597
+ // fires the event twice must not be able to launder a displaced binding into
598
+ // a fresh baseline — which is the whole failure this exists to close.
599
+ if (readState(sessionId)?.boundRoot) return;
600
+ writeState(sessionId, {
601
+ boundRoot: observed,
602
+ claimedRoot: null,
603
+ updatedAt: new Date().toISOString(),
604
+ });
605
+ }
606
+
550
607
  /**
551
608
  * Record what `EnterWorktree` just claimed, for the next tool call to check.
552
609
  *
@@ -648,6 +705,10 @@ function evaluate(payload) {
648
705
  const observed = worktreeRoot(cwd);
649
706
  if (!observed) return 0;
650
707
 
708
+ if (payload.hook_event_name === "SessionStart") {
709
+ recordBaseline(sessionId, observed);
710
+ return 0;
711
+ }
651
712
  if (payload.tool_name === "EnterWorktree") {
652
713
  recordClaim(payload, observed);
653
714
  return 0;
@@ -659,6 +720,13 @@ function evaluate(payload) {
659
720
 
660
721
  const state = readState(sessionId);
661
722
  if (!state?.boundRoot) {
723
+ // No baseline means SessionStart did not run for this session — an older
724
+ // install, a runtime that fires no such event, a harness that skips it.
725
+ // Absence of a baseline is absence of evidence, not evidence of
726
+ // displacement, so this records one exactly as it always did rather than
727
+ // refusing. Treating it as a refusal would turn a floor into a wall on
728
+ // every surface where the event does not fire, and a wall gets switched
729
+ // off, which costs the whole guard.
662
730
  writeState(sessionId, {
663
731
  boundRoot: observed,
664
732
  claimedRoot: null,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -212,6 +212,14 @@
212
212
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/secrets-preflight.sh"
213
213
  }
214
214
  ]
215
+ },
216
+ {
217
+ "hooks": [
218
+ {
219
+ "type": "command",
220
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
221
+ }
222
+ ]
215
223
  }
216
224
  ],
217
225
  "sessionEnd": [
@@ -547,6 +547,63 @@ function claimedRoot(toolInput, cwd) {
547
547
  return null;
548
548
  }
549
549
 
550
+ /**
551
+ * Record where the session STARTED, so its first guarded call is checked
552
+ * against a baseline rather than establishing one.
553
+ *
554
+ * ## Why the first call could not be checked before
555
+ *
556
+ * It established the baseline instead of testing one, which is
557
+ * trust-on-first-use — and TOFU is only as safe as its first use. Here the
558
+ * first use is assigned by the harness, unasked (CodySwannGT/lisa#3864), so the
559
+ * one moment the guard trusted was the moment the session had least control
560
+ * over. A session already sitting in a foreign worktree when it first acted
561
+ * bound to that tree and was never refused, and every later call was then
562
+ * checked faithfully against the wrong baseline — defending the session INTO
563
+ * the wrong worktree rather than out of it (CodySwannGT/lisa#3955).
564
+ *
565
+ * ## Why an earlier observation separates what a cleverer check cannot
566
+ *
567
+ * At the first guarded call, "I was displaced before I acted" and "I
568
+ * legitimately started here" present identically: one session id, one observed
569
+ * root, no prior state. No comparison can tell them apart, because the
570
+ * information is not in the hook's inputs at that instant.
571
+ *
572
+ * At session start it is. The displacement mechanism actually observed is a
573
+ * PEER's `EnterWorktree` moving this session (CodySwannGT/lisa#3712), which
574
+ * happens during the session — after it has started. A baseline taken at start
575
+ * is therefore recorded BEFORE the event that moves it, and the first guarded
576
+ * call becomes an ordinary comparison against a baseline that already exists.
577
+ * It reaches the same refusal a later displacement produces, because by then it
578
+ * is one.
579
+ *
580
+ * ## What this does NOT achieve
581
+ *
582
+ * #3955 asks for a baseline from the ASSIGNER rather than from observation.
583
+ * That is not what this is and Lisa cannot build it: Lisa does not own
584
+ * `EnterWorktree`, and nothing the harness writes names a session's assigned
585
+ * worktree in a form a hook can read. This is still an observation, just an
586
+ * earlier one. It closes the window between session start and the first guarded
587
+ * call; it leaves open a session LAUNCHED already displaced, where start
588
+ * observes the foreign tree and trust-on-first-use still applies.
589
+ *
590
+ * Returns nothing on purpose, like its sibling below: SessionStart has no call
591
+ * to refuse, so every path through it allows.
592
+ * @param sessionId - The session this baseline belongs to
593
+ * @param observed - The worktree root the session started in
594
+ */
595
+ function recordBaseline(sessionId, observed) {
596
+ // Never overwrite. A resumed session, a reconnect, or anything else that
597
+ // fires the event twice must not be able to launder a displaced binding into
598
+ // a fresh baseline — which is the whole failure this exists to close.
599
+ if (readState(sessionId)?.boundRoot) return;
600
+ writeState(sessionId, {
601
+ boundRoot: observed,
602
+ claimedRoot: null,
603
+ updatedAt: new Date().toISOString(),
604
+ });
605
+ }
606
+
550
607
  /**
551
608
  * Record what `EnterWorktree` just claimed, for the next tool call to check.
552
609
  *
@@ -648,6 +705,10 @@ function evaluate(payload) {
648
705
  const observed = worktreeRoot(cwd);
649
706
  if (!observed) return 0;
650
707
 
708
+ if (payload.hook_event_name === "SessionStart") {
709
+ recordBaseline(sessionId, observed);
710
+ return 0;
711
+ }
651
712
  if (payload.tool_name === "EnterWorktree") {
652
713
  recordClaim(payload, observed);
653
714
  return 0;
@@ -659,6 +720,13 @@ function evaluate(payload) {
659
720
 
660
721
  const state = readState(sessionId);
661
722
  if (!state?.boundRoot) {
723
+ // No baseline means SessionStart did not run for this session — an older
724
+ // install, a runtime that fires no such event, a harness that skips it.
725
+ // Absence of a baseline is absence of evidence, not evidence of
726
+ // displacement, so this records one exactly as it always did rather than
727
+ // refusing. Treating it as a refusal would turn a floor into a wall on
728
+ // every surface where the event does not fire, and a wall gets switched
729
+ // off, which costs the whole guard.
662
730
  writeState(sessionId, {
663
731
  boundRoot: observed,
664
732
  claimedRoot: null,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -105,6 +105,9 @@
105
105
  },
106
106
  {
107
107
  "command": "${CURSOR_PLUGIN_ROOT}/hooks/secrets-preflight.sh"
108
+ },
109
+ {
110
+ "command": "${CURSOR_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
108
111
  }
109
112
  ],
110
113
  "subagentStart": [
@@ -547,6 +547,63 @@ function claimedRoot(toolInput, cwd) {
547
547
  return null;
548
548
  }
549
549
 
550
+ /**
551
+ * Record where the session STARTED, so its first guarded call is checked
552
+ * against a baseline rather than establishing one.
553
+ *
554
+ * ## Why the first call could not be checked before
555
+ *
556
+ * It established the baseline instead of testing one, which is
557
+ * trust-on-first-use — and TOFU is only as safe as its first use. Here the
558
+ * first use is assigned by the harness, unasked (CodySwannGT/lisa#3864), so the
559
+ * one moment the guard trusted was the moment the session had least control
560
+ * over. A session already sitting in a foreign worktree when it first acted
561
+ * bound to that tree and was never refused, and every later call was then
562
+ * checked faithfully against the wrong baseline — defending the session INTO
563
+ * the wrong worktree rather than out of it (CodySwannGT/lisa#3955).
564
+ *
565
+ * ## Why an earlier observation separates what a cleverer check cannot
566
+ *
567
+ * At the first guarded call, "I was displaced before I acted" and "I
568
+ * legitimately started here" present identically: one session id, one observed
569
+ * root, no prior state. No comparison can tell them apart, because the
570
+ * information is not in the hook's inputs at that instant.
571
+ *
572
+ * At session start it is. The displacement mechanism actually observed is a
573
+ * PEER's `EnterWorktree` moving this session (CodySwannGT/lisa#3712), which
574
+ * happens during the session — after it has started. A baseline taken at start
575
+ * is therefore recorded BEFORE the event that moves it, and the first guarded
576
+ * call becomes an ordinary comparison against a baseline that already exists.
577
+ * It reaches the same refusal a later displacement produces, because by then it
578
+ * is one.
579
+ *
580
+ * ## What this does NOT achieve
581
+ *
582
+ * #3955 asks for a baseline from the ASSIGNER rather than from observation.
583
+ * That is not what this is and Lisa cannot build it: Lisa does not own
584
+ * `EnterWorktree`, and nothing the harness writes names a session's assigned
585
+ * worktree in a form a hook can read. This is still an observation, just an
586
+ * earlier one. It closes the window between session start and the first guarded
587
+ * call; it leaves open a session LAUNCHED already displaced, where start
588
+ * observes the foreign tree and trust-on-first-use still applies.
589
+ *
590
+ * Returns nothing on purpose, like its sibling below: SessionStart has no call
591
+ * to refuse, so every path through it allows.
592
+ * @param sessionId - The session this baseline belongs to
593
+ * @param observed - The worktree root the session started in
594
+ */
595
+ function recordBaseline(sessionId, observed) {
596
+ // Never overwrite. A resumed session, a reconnect, or anything else that
597
+ // fires the event twice must not be able to launder a displaced binding into
598
+ // a fresh baseline — which is the whole failure this exists to close.
599
+ if (readState(sessionId)?.boundRoot) return;
600
+ writeState(sessionId, {
601
+ boundRoot: observed,
602
+ claimedRoot: null,
603
+ updatedAt: new Date().toISOString(),
604
+ });
605
+ }
606
+
550
607
  /**
551
608
  * Record what `EnterWorktree` just claimed, for the next tool call to check.
552
609
  *
@@ -648,6 +705,10 @@ function evaluate(payload) {
648
705
  const observed = worktreeRoot(cwd);
649
706
  if (!observed) return 0;
650
707
 
708
+ if (payload.hook_event_name === "SessionStart") {
709
+ recordBaseline(sessionId, observed);
710
+ return 0;
711
+ }
651
712
  if (payload.tool_name === "EnterWorktree") {
652
713
  recordClaim(payload, observed);
653
714
  return 0;
@@ -659,6 +720,13 @@ function evaluate(payload) {
659
720
 
660
721
  const state = readState(sessionId);
661
722
  if (!state?.boundRoot) {
723
+ // No baseline means SessionStart did not run for this session — an older
724
+ // install, a runtime that fires no such event, a harness that skips it.
725
+ // Absence of a baseline is absence of evidence, not evidence of
726
+ // displacement, so this records one exactly as it always did rather than
727
+ // refusing. Treating it as a refusal would turn a floor into a wall on
728
+ // every surface where the event does not fire, and a wall gets switched
729
+ // off, which costs the whole guard.
662
730
  writeState(sessionId, {
663
731
  boundRoot: observed,
664
732
  claimedRoot: null,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.45.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
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.7",
3
+ "version": "4.45.9",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"