@codyswann/lisa 4.45.7 → 4.45.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +21 -2
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +68 -0
- package/all/merge/.claude/settings.json +10 -0
- package/dist/configs/vitest/scratch-global-setup.d.ts +9 -3
- package/dist/configs/vitest/scratch-global-setup.d.ts.map +1 -1
- package/dist/configs/vitest/scratch-global-setup.js +23 -10
- package/dist/configs/vitest/scratch-global-setup.js.map +1 -1
- package/dist/configs/vitest/scratch-tmpdir-trust.d.ts +48 -0
- package/dist/configs/vitest/scratch-tmpdir-trust.d.ts.map +1 -0
- package/dist/configs/vitest/scratch-tmpdir-trust.js +194 -0
- package/dist/configs/vitest/scratch-tmpdir-trust.js.map +1 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +13 -8
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +5 -5
- package/plugins/lisa/.claude-plugin/plugin.json +9 -1
- package/plugins/lisa/.codex-plugin/hooks.json +8 -0
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +68 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +9 -1
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +68 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/hooks.json +3 -0
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +68 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/.claude-plugin/plugin.json +8 -0
- package/plugins/src/base/hooks/worktree-binding-guard.mjs +68 -0
- package/scripts/check-shell-guard-refusal-coverage.mjs +49 -3
- package/scripts/lib/shell-guard-trace.mjs +26 -8
- package/scripts/lisa-enforcement-fallback.sh +21 -2
- 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.
|
|
179
|
+
"version": "4.45.8",
|
|
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": "
|
|
328
|
-
"gitHead": "
|
|
329
|
-
"lisaReleaseTag": "v4.45.
|
|
327
|
+
"lisaReleaseCommit": "255b3a9f7225e3c3b32bc51ba2f10490d76109db",
|
|
328
|
+
"gitHead": "255b3a9f7225e3c3b32bc51ba2f10490d76109db",
|
|
329
|
+
"lisaReleaseTag": "v4.45.8"
|
|
330
330
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa",
|
|
3
|
-
"version": "4.45.
|
|
3
|
+
"version": "4.45.8",
|
|
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": [
|
|
@@ -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.
|
|
3
|
+
"version": "4.45.8",
|
|
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,
|
|
@@ -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-openclaw",
|
|
3
|
-
"version": "4.45.
|
|
3
|
+
"version": "4.45.8",
|
|
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.
|
|
3
|
+
"version": "4.45.8",
|
|
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.
|
|
3
|
+
"version": "4.45.8",
|
|
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.
|
|
3
|
+
"version": "4.45.8",
|
|
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.
|
|
3
|
+
"version": "4.45.8",
|
|
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"
|