@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.
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +128 -50
- package/cdk/create-only/.github/workflows/deploy.yml +10 -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 +7 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +19 -4
- package/expo/create-only/.github/workflows/deploy.yml +10 -0
- package/nestjs/create-only/.github/workflows/deploy.yml +10 -0
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +128 -50
- 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 +1 -1
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +128 -50
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +128 -50
- 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/hooks/worktree-binding-guard.mjs +128 -50
- package/typescript/copy-contents/.husky/commit-msg +31 -11
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.66.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
|
@@ -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
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
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}
|
|
547
|
+
if (typeof agent !== "string" || !agent) return keyPart(payload.session_id);
|
|
548
|
+
return `${keyPart(payload.session_id)}--${keyPart(agent)}`;
|
|
532
549
|
}
|
|
533
550
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
|
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(
|
|
557
|
-
const file = stateFile(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
645
|
-
writeState(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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(
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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(
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
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(
|
|
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(
|
|
888
|
-
writeState(
|
|
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(
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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(
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
|
148
|
-
# AI-Agent/AI-Model/AI-Effort
|
|
149
|
-
#
|
|
150
|
-
#
|
|
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
|
-
|
|
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' "$
|
|
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' "$
|
|
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' "$
|
|
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' "$
|
|
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
|
|