@codyswann/lisa 4.66.2 → 4.66.3

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 +128 -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 +6 -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 +128 -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 +128 -50
  25. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +128 -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 +128 -50
  68. package/typescript/copy-contents/.husky/commit-msg +31 -11
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.66.2",
3
+ "version": "4.66.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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,20 @@ 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).
518
+ * @param {string} value - One component of the binding key
519
+ * @returns {string} A component containing only `[A-Za-z0-9._]` and `%XX`
520
+ */
521
+ function keyPart(value) {
522
+ return encodeURIComponent(value).replaceAll("-", "%2D");
523
+ }
524
+
511
525
  /**
512
526
  * The identity a binding belongs to.
513
527
  *
@@ -521,22 +535,58 @@ function stateFile(bindingKey) {
521
535
  * session-scoped key it always had, which is also the fallback for runtimes
522
536
  * that never send the field.
523
537
  *
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.
538
+ * Both shapes escape every component with {@link keyPart}, so the session key
539
+ * `x` and the agent key `a--b` live in disjoint namespaces: an escaped
540
+ * component never contains `-`, a main-session key is one component, an
541
+ * agent key is two joined by the only `--` the string can hold. Session
542
+ * "s%2Da--c" cannot collide with session "s-a" + agent "c", and neither can
543
+ * collide across the two shapes (CodySwannGT/lisa#4294).
527
544
  */
528
545
  function bindingKey(payload) {
529
546
  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, "_");
547
+ if (typeof agent !== "string" || !agent) return keyPart(payload.session_id);
548
+ return `${keyPart(payload.session_id)}--${keyPart(agent)}`;
532
549
  }
533
550
 
534
- function readState(sessionId) {
535
- try {
536
- return JSON.parse(readFileSync(stateFile(sessionId), "utf8"));
537
- } catch {
538
- return null;
551
+ /**
552
+ * The keys earlier versions wrote for this payload, newest first.
553
+ *
554
+ * The v1 key was the raw `session_id` (single shared file per session). v2
555
+ * joined raw session and agent on `--`. Both are readable on upgrade so a
556
+ * mid-session deploy does not silently re-baseline an established binding —
557
+ * the ambiguity in a legacy `a--b` key is resolved toward the interpretation
558
+ * the lookup is making, and the next write lands on the current key while the
559
+ * legacy file is left to age out.
560
+ * @param {object} payload - The hook payload
561
+ * @returns {string[]} Zero or more legacy keys this payload may have written
562
+ */
563
+ function legacyKeys(payload) {
564
+ const agent = payload?.agent_id;
565
+ const session = payload.session_id;
566
+ if (typeof agent === "string" && agent) {
567
+ // v2 wrote the sanitised composite. v1 never wrote a per-agent file — an
568
+ // agent falling back to the shared session file is the #4277 bug again,
569
+ // so the list stops there.
570
+ const san = value => value.replaceAll(/[^A-Za-z0-9._-]/g, "_");
571
+ return [`${san(session)}--${san(agent)}`];
539
572
  }
573
+ // v1 and v2 both keyed the main agent on the raw session id — but only when
574
+ // that id contains no `--`. One that does spells exactly the composite
575
+ // shape agent bindings now write, so reading it could hand this session a
576
+ // stranger's binding. Losing the v1 binding for such an id re-baselines
577
+ // once; the alternative is reading a file whose owner cannot be proven.
578
+ return session.includes("--") ? [] : [session];
579
+ }
580
+
581
+ function readState(bindingKeyValue, payload) {
582
+ for (const key of [bindingKeyValue, ...legacyKeys(payload)]) {
583
+ try {
584
+ return JSON.parse(readFileSync(stateFile(key), "utf8"));
585
+ } catch {
586
+ // Not written under this key — try the next older one.
587
+ }
588
+ }
589
+ return null;
540
590
  }
541
591
 
542
592
  /**
@@ -549,17 +599,23 @@ function readState(sessionId) {
549
599
  * {@link noticeRuntimeDrift} and read by nobody else, and a clobbering write
550
600
  * would turn its "reported once" into "reported again after the next
551
601
  * acknowledgement", which is the noise the notice is designed not to be.
552
- * @param sessionId - The session whose state file this is
602
+ * @param bindingKeyValue - The key whose state file this is
553
603
  * @param state - Fields to write over the recorded state
604
+ * @param payload - The hook payload, for legacy-key reads while a binding is
605
+ * migrating to the current key format
554
606
  * @returns Whether the write succeeded
555
607
  */
556
- function writeState(sessionId, state) {
557
- const file = stateFile(sessionId);
608
+ function writeState(bindingKeyValue, state, payload) {
609
+ const file = stateFile(bindingKeyValue);
558
610
  try {
559
611
  mkdirSync(dirname(file), { recursive: true });
560
612
  writeFileSync(
561
613
  file,
562
- `${JSON.stringify({ ...readState(sessionId), ...state }, null, 2)}\n`
614
+ `${JSON.stringify(
615
+ { ...readState(bindingKeyValue, payload), ...state },
616
+ null,
617
+ 2
618
+ )}\n`
563
619
  );
564
620
  return true;
565
621
  } catch (error) {
@@ -634,19 +690,24 @@ function claimedRoot(toolInput, cwd) {
634
690
  *
635
691
  * Returns nothing on purpose, like its sibling below: SessionStart has no call
636
692
  * to refuse, so every path through it allows.
637
- * @param sessionId - The session this baseline belongs to
693
+ * @param bindingKeyValue - The key this baseline belongs to
638
694
  * @param observed - The worktree root the session started in
695
+ * @param payload - The hook payload, for legacy-key reads
639
696
  */
640
- function recordBaseline(sessionId, observed) {
697
+ function recordBaseline(bindingKeyValue, observed, payload) {
641
698
  // Never overwrite. A resumed session, a reconnect, or anything else that
642
699
  // fires the event twice must not be able to launder a displaced binding into
643
700
  // 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
- });
701
+ if (readState(bindingKeyValue, payload)?.boundRoot) return;
702
+ writeState(
703
+ bindingKeyValue,
704
+ {
705
+ boundRoot: observed,
706
+ claimedRoot: null,
707
+ updatedAt: new Date().toISOString(),
708
+ },
709
+ payload
710
+ );
650
711
  }
651
712
 
652
713
  /**
@@ -660,12 +721,16 @@ function recordBaseline(sessionId, observed) {
660
721
  function recordClaim(payload, observed) {
661
722
  const claimed = claimedRoot(payload.tool_input, payload.cwd);
662
723
  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
- });
724
+ const previous = readState(bindingKey(payload), payload);
725
+ writeState(
726
+ bindingKey(payload),
727
+ {
728
+ boundRoot: previous?.boundRoot ?? observed,
729
+ claimedRoot: claimed,
730
+ updatedAt: new Date().toISOString(),
731
+ },
732
+ payload
733
+ );
669
734
  }
670
735
 
671
736
  function refuse(lines) {
@@ -701,11 +766,15 @@ function handleAcceptance(payload, observed) {
701
766
  acceptanceLine(observed),
702
767
  ]);
703
768
  }
704
- writeState(bindingKey(payload), {
705
- boundRoot: observed,
706
- claimedRoot: null,
707
- updatedAt: new Date().toISOString(),
708
- });
769
+ writeState(
770
+ bindingKey(payload),
771
+ {
772
+ boundRoot: observed,
773
+ claimedRoot: null,
774
+ updatedAt: new Date().toISOString(),
775
+ },
776
+ payload
777
+ );
709
778
  say(`bound to ${observed}`);
710
779
  return 0;
711
780
  }
@@ -877,15 +946,16 @@ function driftNotice(version) {
877
946
  * Never refuses. The dependency is a reporting gap, not a live displacement,
878
947
  * and blocking a session over a version number would be a wall where an
879
948
  * observation belongs.
880
- * @param sessionId - The session being started
949
+ * @param bindingKeyValue - The key being started
881
950
  * @param env - Environment to read the runtime version from
951
+ * @param payload - The hook payload, for legacy-key reads
882
952
  * @returns Nothing; every path through this allows
883
953
  */
884
- function noticeRuntimeDrift(sessionId, env) {
954
+ function noticeRuntimeDrift(bindingKeyValue, env, payload) {
885
955
  const version = runtimeVersion(env);
886
956
  if (version === null || version === VERIFIED_RUNTIME_VERSION) return;
887
- if (readState(sessionId)?.runtimeNoticed === version) return;
888
- writeState(sessionId, { runtimeNoticed: version });
957
+ if (readState(bindingKeyValue, payload)?.runtimeNoticed === version) return;
958
+ writeState(bindingKeyValue, { runtimeNoticed: version }, payload);
889
959
  process.stdout.write(
890
960
  `${JSON.stringify({
891
961
  hookSpecificOutput: {
@@ -908,11 +978,11 @@ function evaluate(payload) {
908
978
  if (!observed) return 0;
909
979
 
910
980
  if (payload.hook_event_name === "SessionStart") {
911
- recordBaseline(binding, observed);
981
+ recordBaseline(binding, observed, payload);
912
982
  // After the baseline, never before: `recordBaseline` writes a fresh state
913
983
  // for a session it has not seen, and the notice's record has to go on top
914
984
  // of that write rather than under it.
915
- noticeRuntimeDrift(binding, process.env);
985
+ noticeRuntimeDrift(binding, process.env, payload);
916
986
  return 0;
917
987
  }
918
988
  if (payload.tool_name === "EnterWorktree") {
@@ -924,7 +994,7 @@ function evaluate(payload) {
924
994
  const accepted = handleAcceptance(payload, observed);
925
995
  if (accepted !== null) return accepted;
926
996
 
927
- const state = readState(binding);
997
+ const state = readState(binding, payload);
928
998
  if (!state?.boundRoot) {
929
999
  // No baseline means SessionStart did not run for this session — an older
930
1000
  // install, a runtime that fires no such event, a harness that skips it.
@@ -933,22 +1003,30 @@ function evaluate(payload) {
933
1003
  // refusing. Treating it as a refusal would turn a floor into a wall on
934
1004
  // every surface where the event does not fire, and a wall gets switched
935
1005
  // off, which costs the whole guard.
936
- writeState(binding, {
937
- boundRoot: observed,
938
- claimedRoot: null,
939
- updatedAt: new Date().toISOString(),
940
- });
1006
+ writeState(
1007
+ binding,
1008
+ {
1009
+ boundRoot: observed,
1010
+ claimedRoot: null,
1011
+ updatedAt: new Date().toISOString(),
1012
+ },
1013
+ payload
1014
+ );
941
1015
  return 0;
942
1016
  }
943
1017
  if (state.claimedRoot && state.claimedRoot !== observed) {
944
1018
  return unconfirmedSwitch(state.claimedRoot, observed);
945
1019
  }
946
1020
  if (state.claimedRoot === observed) {
947
- writeState(binding, {
948
- boundRoot: observed,
949
- claimedRoot: null,
950
- updatedAt: new Date().toISOString(),
951
- });
1021
+ writeState(
1022
+ binding,
1023
+ {
1024
+ boundRoot: observed,
1025
+ claimedRoot: null,
1026
+ updatedAt: new Date().toISOString(),
1027
+ },
1028
+ payload
1029
+ );
952
1030
  return 0;
953
1031
  }
954
1032
  if (state.boundRoot !== observed) return displaced(state.boundRoot, observed);
@@ -144,16 +144,26 @@ fi
144
144
  echo "🤖 Checking for AI co-authorship..."
145
145
 
146
146
  # Fleet agents — the runtimes Lisa ships plugin distributions for — pass on
147
- # the trailer alone. Any other named agent (e.g. Devin) must also carry
148
- # AI-Agent/AI-Model/AI-Effort trailers: the gate enforces attribution, not
149
- # agent gatekeeping, so an agent Lisa does not distribute to can still commit
150
- # truthfully instead of forcing --no-verify. CodySwannGT/lisa#4282.
147
+ # the trailer alone. Any other co-author — an agent Lisa does not distribute
148
+ # to (e.g. Devin) OR A HUMAN — also requires the AI-Agent/AI-Model/AI-Effort
149
+ # block: the gate enforces attribution, not agent gatekeeping, and a trailer
150
+ # name cannot tell a human from a non-fleet agent, so both are held to the
151
+ # same rule. The AI-* trailers describe the commit's DRIVER, not every
152
+ # co-author — a human pairing alongside an agent is covered by the agent's
153
+ # block, while a commit with ONLY a human co-author is refused by the check
154
+ # below: its AI-Agent trailer must name a listed co-author, and a human-only
155
+ # trailer block has no agent to name. (A human could still write
156
+ # "AI-Agent: <their own name>" to self-attest as the agent — the trailer is
157
+ # the attestation surface, not a proof, so the gate documents rather than
158
+ # polices that.) CodySwannGT/lisa#4282, CodySwannGT/lisa#4294.
151
159
  LISA_FLEET_AGENT_RE="Claude|Codex|Cursor|OpenCode|Antigravity|Copilot"
152
160
  # Co-authors come from the message's trailer block via git-interpret-trailers —
153
161
  # a "Co-authored-by:" string inside prose is not attribution — and every entry
154
162
  # is judged on its own, so a fleet trailer can never cover a sibling trailer
155
- # whose agent owes metadata.
156
- COAUTHOR_TRAILERS=$(git interpret-trailers --parse "$COMMIT_MSG_FILE" 2>/dev/null | grep -iE "^Co-authored-by:" || true)
163
+ # whose agent owes metadata. The AI-* checks below read the SAME parsed
164
+ # trailer block, so an "AI-Agent:" line in body prose cannot satisfy them.
165
+ ALL_TRAILERS=$(git interpret-trailers --parse "$COMMIT_MSG_FILE" 2>/dev/null || true)
166
+ COAUTHOR_TRAILERS=$(printf '%s\n' "$ALL_TRAILERS" | grep -iE "^Co-authored-by:" || true)
157
167
 
158
168
  if ! printf '%s\n' "$COAUTHOR_TRAILERS" | grep -Eiq "Co-authored-by:"; then
159
169
  no_coauthor=1
@@ -177,21 +187,31 @@ fi
177
187
  if [ "$nonfleet_coauthor" -eq 1 ]; then
178
188
  missing_agent_metadata=0
179
189
  for trailer in AI-Agent AI-Model AI-Effort; do
180
- if ! printf '%s\n' "$COMMIT_MSG" | grep -Eiq "^${trailer}:[[:space:]]*[^[:space:]]"; then
190
+ if ! printf '%s\n' "$ALL_TRAILERS" | grep -Eiq "^${trailer}:[[:space:]]*[^[:space:]]"; then
181
191
  missing_agent_metadata=1
182
192
  fi
183
193
  done
194
+ # The AI-Agent trailer must also NAME one of the listed co-authors —
195
+ # metadata attesting to nobody in the trailer block is not attribution.
196
+ ai_agent=$(printf '%s\n' "$ALL_TRAILERS" | grep -iE "^AI-Agent:" | head -1 | sed -E 's/^[^:]+:[[:space:]]*//;s/[[:space:]]*$//')
197
+ if [ -n "$ai_agent" ] && ! printf '%s\n' "$COAUTHOR_TRAILERS" | grep -Fqi "$ai_agent"; then
198
+ missing_agent_metadata=1
199
+ fi
184
200
  if [ "$missing_agent_metadata" -eq 1 ]; then
185
201
  echo ""
186
202
  echo "❌ Non-fleet agents must include AI metadata trailers!"
187
203
  echo ""
188
204
  echo "Agents outside the supported fleet (Claude, Codex, Cursor, OpenCode,"
189
205
  echo "Antigravity, Copilot) must carry provenance metadata alongside the"
190
- echo "Co-authored-by trailer:"
206
+ echo "Co-authored-by trailer — this includes a human co-author, who is"
207
+ echo "covered by the driving agent's AI-Agent/AI-Model/AI-Effort block:"
191
208
  echo " AI-Agent: <agent name>"
192
209
  echo " AI-Model: <provider/model>"
193
210
  echo " AI-Effort: <effort or runtime value>"
194
211
  echo ""
212
+ echo "The AI-Agent value must name one of the commit's Co-authored-by"
213
+ echo "entries — it attests to which listed author is the driving agent."
214
+ echo ""
195
215
  lisa_commit_gate_families ai-coauthorship
196
216
  exit 1
197
217
  fi
@@ -227,13 +247,13 @@ fi
227
247
 
228
248
  if printf '%s\n' "$COAUTHOR_TRAILERS" | grep -Eiq "Co-authored-by:.*OpenCode"; then
229
249
  missing_opencode_metadata=0
230
- if ! printf '%s\n' "$COMMIT_MSG" | grep -Eiq "^AI-Agent:[[:space:]]*OpenCode[[:space:]]*$"; then
250
+ if ! printf '%s\n' "$ALL_TRAILERS" | grep -Eiq "^AI-Agent:[[:space:]]*OpenCode[[:space:]]*$"; then
231
251
  missing_opencode_metadata=1
232
252
  fi
233
- if ! printf '%s\n' "$COMMIT_MSG" | grep -Eiq "^AI-Model:[[:space:]]*[^[:space:]].*$"; then
253
+ if ! printf '%s\n' "$ALL_TRAILERS" | grep -Eiq "^AI-Model:[[:space:]]*[^[:space:]].*$"; then
234
254
  missing_opencode_metadata=1
235
255
  fi
236
- if ! printf '%s\n' "$COMMIT_MSG" | grep -Eiq "^AI-Effort:[[:space:]]*[^[:space:]].*$"; then
256
+ if ! printf '%s\n' "$ALL_TRAILERS" | grep -Eiq "^AI-Effort:[[:space:]]*[^[:space:]].*$"; then
237
257
  missing_opencode_metadata=1
238
258
  fi
239
259