@codyswann/lisa 4.66.2 → 4.66.4

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 (68) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +135 -50
  2. package/cdk/create-only/.github/workflows/deploy.yml +10 -0
  3. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.js +10 -0
  5. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  6. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +7 -6
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +19 -4
  11. package/expo/create-only/.github/workflows/deploy.yml +10 -0
  12. package/nestjs/create-only/.github/workflows/deploy.yml +10 -0
  13. package/package.json +4 -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/worktree-binding-guard.mjs +135 -50
  17. package/plugins/lisa-agy/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  21. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +135 -50
  25. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +135 -50
  27. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-agy/plugin.json +1 -1
  30. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  50. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails-agy/plugin.json +1 -1
  55. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  60. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  65. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/src/base/hooks/worktree-binding-guard.mjs +135 -50
  68. package/typescript/copy-contents/.husky/commit-msg +48 -11
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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.66.2",
3
+ "version": "4.66.4",
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"
@@ -508,6 +508,27 @@ function stateFile(bindingKey) {
508
508
  return join(home, "worktree-binding", `${bindingKey}.json`);
509
509
  }
510
510
 
511
+ /**
512
+ * Percent-escape every byte that is not filename-safe, so the output cannot
513
+ * contain `-` at all and two escaped parts can be joined on `--` without the
514
+ * joiner ever being smuggled in from either side.
515
+ *
516
+ * `:` and friends are folded too — the key lands in a filename and `:` is
517
+ * illegal on Windows/NTFS (CodySwannGT/lisa#4277). `encodeURIComponent`
518
+ * leaves `! ' ( ) *` alone, and `*` is equally illegal on Windows, so they
519
+ * are escaped by hand.
520
+ * @param {string} value - One component of the binding key
521
+ * @returns {string} A component containing only `[A-Za-z0-9._~]` and `%XX`
522
+ */
523
+ function keyPart(value) {
524
+ return encodeURIComponent(value)
525
+ .replaceAll(
526
+ /[!'()*]/g,
527
+ c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
528
+ )
529
+ .replaceAll("-", "%2D");
530
+ }
531
+
511
532
  /**
512
533
  * The identity a binding belongs to.
513
534
  *
@@ -521,22 +542,58 @@ function stateFile(bindingKey) {
521
542
  * session-scoped key it always had, which is also the fallback for runtimes
522
543
  * that never send the field.
523
544
  *
524
- * The key lands in a filename, so the composite is folded into a set that is
525
- * legal on every filesystem — `:` is not, which rules it out as a separator
526
- * and as tolerated session_id content on Windows hosts.
545
+ * Both shapes escape every component with {@link keyPart}, so the session key
546
+ * `x` and the agent key `a--b` live in disjoint namespaces: an escaped
547
+ * component never contains `-`, a main-session key is one component, an
548
+ * agent key is two joined by the only `--` the string can hold. Session
549
+ * "s%2Da--c" cannot collide with session "s-a" + agent "c", and neither can
550
+ * collide across the two shapes (CodySwannGT/lisa#4294).
527
551
  */
528
552
  function bindingKey(payload) {
529
553
  const agent = payload?.agent_id;
530
- if (typeof agent !== "string" || !agent) return payload.session_id;
531
- return `${payload.session_id}--${agent}`.replaceAll(/[^A-Za-z0-9._-]/g, "_");
554
+ if (typeof agent !== "string" || !agent) return keyPart(payload.session_id);
555
+ return `${keyPart(payload.session_id)}--${keyPart(agent)}`;
532
556
  }
533
557
 
534
- function readState(sessionId) {
535
- try {
536
- return JSON.parse(readFileSync(stateFile(sessionId), "utf8"));
537
- } catch {
538
- return null;
558
+ /**
559
+ * The keys earlier versions wrote for this payload, newest first.
560
+ *
561
+ * The v1 key was the raw `session_id` (single shared file per session). v2
562
+ * joined raw session and agent on `--`. Both are readable on upgrade so a
563
+ * mid-session deploy does not silently re-baseline an established binding —
564
+ * the ambiguity in a legacy `a--b` key is resolved toward the interpretation
565
+ * the lookup is making, and the next write lands on the current key while the
566
+ * legacy file is left to age out.
567
+ * @param {object} payload - The hook payload
568
+ * @returns {string[]} Zero or more legacy keys this payload may have written
569
+ */
570
+ function legacyKeys(payload) {
571
+ const agent = payload?.agent_id;
572
+ const session = payload.session_id;
573
+ if (typeof agent === "string" && agent) {
574
+ // v2 wrote the sanitised composite. v1 never wrote a per-agent file — an
575
+ // agent falling back to the shared session file is the #4277 bug again,
576
+ // so the list stops there.
577
+ const san = value => value.replaceAll(/[^A-Za-z0-9._-]/g, "_");
578
+ return [`${san(session)}--${san(agent)}`];
539
579
  }
580
+ // v1 and v2 both keyed the main agent on the raw session id — but only when
581
+ // that id contains no `--`. One that does spells exactly the composite
582
+ // shape agent bindings now write, so reading it could hand this session a
583
+ // stranger's binding. Losing the v1 binding for such an id re-baselines
584
+ // once; the alternative is reading a file whose owner cannot be proven.
585
+ return session.includes("--") ? [] : [session];
586
+ }
587
+
588
+ function readState(bindingKeyValue, payload) {
589
+ for (const key of [bindingKeyValue, ...legacyKeys(payload)]) {
590
+ try {
591
+ return JSON.parse(readFileSync(stateFile(key), "utf8"));
592
+ } catch {
593
+ // Not written under this key — try the next older one.
594
+ }
595
+ }
596
+ return null;
540
597
  }
541
598
 
542
599
  /**
@@ -549,17 +606,23 @@ function readState(sessionId) {
549
606
  * {@link noticeRuntimeDrift} and read by nobody else, and a clobbering write
550
607
  * would turn its "reported once" into "reported again after the next
551
608
  * acknowledgement", which is the noise the notice is designed not to be.
552
- * @param sessionId - The session whose state file this is
609
+ * @param bindingKeyValue - The key whose state file this is
553
610
  * @param state - Fields to write over the recorded state
611
+ * @param payload - The hook payload, for legacy-key reads while a binding is
612
+ * migrating to the current key format
554
613
  * @returns Whether the write succeeded
555
614
  */
556
- function writeState(sessionId, state) {
557
- const file = stateFile(sessionId);
615
+ function writeState(bindingKeyValue, state, payload) {
616
+ const file = stateFile(bindingKeyValue);
558
617
  try {
559
618
  mkdirSync(dirname(file), { recursive: true });
560
619
  writeFileSync(
561
620
  file,
562
- `${JSON.stringify({ ...readState(sessionId), ...state }, null, 2)}\n`
621
+ `${JSON.stringify(
622
+ { ...readState(bindingKeyValue, payload), ...state },
623
+ null,
624
+ 2
625
+ )}\n`
563
626
  );
564
627
  return true;
565
628
  } catch (error) {
@@ -634,19 +697,24 @@ function claimedRoot(toolInput, cwd) {
634
697
  *
635
698
  * Returns nothing on purpose, like its sibling below: SessionStart has no call
636
699
  * to refuse, so every path through it allows.
637
- * @param sessionId - The session this baseline belongs to
700
+ * @param bindingKeyValue - The key this baseline belongs to
638
701
  * @param observed - The worktree root the session started in
702
+ * @param payload - The hook payload, for legacy-key reads
639
703
  */
640
- function recordBaseline(sessionId, observed) {
704
+ function recordBaseline(bindingKeyValue, observed, payload) {
641
705
  // Never overwrite. A resumed session, a reconnect, or anything else that
642
706
  // fires the event twice must not be able to launder a displaced binding into
643
707
  // a fresh baseline — which is the whole failure this exists to close.
644
- if (readState(sessionId)?.boundRoot) return;
645
- writeState(sessionId, {
646
- boundRoot: observed,
647
- claimedRoot: null,
648
- updatedAt: new Date().toISOString(),
649
- });
708
+ if (readState(bindingKeyValue, payload)?.boundRoot) return;
709
+ writeState(
710
+ bindingKeyValue,
711
+ {
712
+ boundRoot: observed,
713
+ claimedRoot: null,
714
+ updatedAt: new Date().toISOString(),
715
+ },
716
+ payload
717
+ );
650
718
  }
651
719
 
652
720
  /**
@@ -660,12 +728,16 @@ function recordBaseline(sessionId, observed) {
660
728
  function recordClaim(payload, observed) {
661
729
  const claimed = claimedRoot(payload.tool_input, payload.cwd);
662
730
  if (!claimed || claimed === observed) return;
663
- const previous = readState(bindingKey(payload));
664
- writeState(bindingKey(payload), {
665
- boundRoot: previous?.boundRoot ?? observed,
666
- claimedRoot: claimed,
667
- updatedAt: new Date().toISOString(),
668
- });
731
+ const previous = readState(bindingKey(payload), payload);
732
+ writeState(
733
+ bindingKey(payload),
734
+ {
735
+ boundRoot: previous?.boundRoot ?? observed,
736
+ claimedRoot: claimed,
737
+ updatedAt: new Date().toISOString(),
738
+ },
739
+ payload
740
+ );
669
741
  }
670
742
 
671
743
  function refuse(lines) {
@@ -701,11 +773,15 @@ function handleAcceptance(payload, observed) {
701
773
  acceptanceLine(observed),
702
774
  ]);
703
775
  }
704
- writeState(bindingKey(payload), {
705
- boundRoot: observed,
706
- claimedRoot: null,
707
- updatedAt: new Date().toISOString(),
708
- });
776
+ writeState(
777
+ bindingKey(payload),
778
+ {
779
+ boundRoot: observed,
780
+ claimedRoot: null,
781
+ updatedAt: new Date().toISOString(),
782
+ },
783
+ payload
784
+ );
709
785
  say(`bound to ${observed}`);
710
786
  return 0;
711
787
  }
@@ -877,15 +953,16 @@ function driftNotice(version) {
877
953
  * Never refuses. The dependency is a reporting gap, not a live displacement,
878
954
  * and blocking a session over a version number would be a wall where an
879
955
  * observation belongs.
880
- * @param sessionId - The session being started
956
+ * @param bindingKeyValue - The key being started
881
957
  * @param env - Environment to read the runtime version from
958
+ * @param payload - The hook payload, for legacy-key reads
882
959
  * @returns Nothing; every path through this allows
883
960
  */
884
- function noticeRuntimeDrift(sessionId, env) {
961
+ function noticeRuntimeDrift(bindingKeyValue, env, payload) {
885
962
  const version = runtimeVersion(env);
886
963
  if (version === null || version === VERIFIED_RUNTIME_VERSION) return;
887
- if (readState(sessionId)?.runtimeNoticed === version) return;
888
- writeState(sessionId, { runtimeNoticed: version });
964
+ if (readState(bindingKeyValue, payload)?.runtimeNoticed === version) return;
965
+ writeState(bindingKeyValue, { runtimeNoticed: version }, payload);
889
966
  process.stdout.write(
890
967
  `${JSON.stringify({
891
968
  hookSpecificOutput: {
@@ -908,11 +985,11 @@ function evaluate(payload) {
908
985
  if (!observed) return 0;
909
986
 
910
987
  if (payload.hook_event_name === "SessionStart") {
911
- recordBaseline(binding, observed);
988
+ recordBaseline(binding, observed, payload);
912
989
  // After the baseline, never before: `recordBaseline` writes a fresh state
913
990
  // for a session it has not seen, and the notice's record has to go on top
914
991
  // of that write rather than under it.
915
- noticeRuntimeDrift(binding, process.env);
992
+ noticeRuntimeDrift(binding, process.env, payload);
916
993
  return 0;
917
994
  }
918
995
  if (payload.tool_name === "EnterWorktree") {
@@ -924,7 +1001,7 @@ function evaluate(payload) {
924
1001
  const accepted = handleAcceptance(payload, observed);
925
1002
  if (accepted !== null) return accepted;
926
1003
 
927
- const state = readState(binding);
1004
+ const state = readState(binding, payload);
928
1005
  if (!state?.boundRoot) {
929
1006
  // No baseline means SessionStart did not run for this session — an older
930
1007
  // install, a runtime that fires no such event, a harness that skips it.
@@ -933,22 +1010,30 @@ function evaluate(payload) {
933
1010
  // refusing. Treating it as a refusal would turn a floor into a wall on
934
1011
  // every surface where the event does not fire, and a wall gets switched
935
1012
  // off, which costs the whole guard.
936
- writeState(binding, {
937
- boundRoot: observed,
938
- claimedRoot: null,
939
- updatedAt: new Date().toISOString(),
940
- });
1013
+ writeState(
1014
+ binding,
1015
+ {
1016
+ boundRoot: observed,
1017
+ claimedRoot: null,
1018
+ updatedAt: new Date().toISOString(),
1019
+ },
1020
+ payload
1021
+ );
941
1022
  return 0;
942
1023
  }
943
1024
  if (state.claimedRoot && state.claimedRoot !== observed) {
944
1025
  return unconfirmedSwitch(state.claimedRoot, observed);
945
1026
  }
946
1027
  if (state.claimedRoot === observed) {
947
- writeState(binding, {
948
- boundRoot: observed,
949
- claimedRoot: null,
950
- updatedAt: new Date().toISOString(),
951
- });
1028
+ writeState(
1029
+ binding,
1030
+ {
1031
+ boundRoot: observed,
1032
+ claimedRoot: null,
1033
+ updatedAt: new Date().toISOString(),
1034
+ },
1035
+ payload
1036
+ );
952
1037
  return 0;
953
1038
  }
954
1039
  if (state.boundRoot !== observed) return displaced(state.boundRoot, observed);