instar 1.1.2 → 1.1.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.
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ // safe-git-allow: pre-push critical path — must stay lightweight, no SafeGitExecutor audit-log writes from the hook.
3
+ /**
4
+ * Pre-push guard: reject test-fixture commits before they ship.
5
+ *
6
+ * Git invokes pre-push hooks with GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE
7
+ * set in the env. When the hook runs `npm run test:smoke`, the test
8
+ * suite inherits those overrides, and any test that spawns `git init`
9
+ * + `git commit` in a tmpdir without sanitizing env ends up committing
10
+ * into the real repo on whichever branch is currently checked out.
11
+ *
12
+ * That's how PRs #130 and #277 picked up "Initial commit" / "seed" /
13
+ * "Worktree commit N" stowaways that overwrote README.md on main.
14
+ *
15
+ * Layer 1 (vitest-setup.ts strip) closes the root cause. This guard is
16
+ * the second line of defense: even if a future test reintroduces a raw
17
+ * git call without sanitizing env, the resulting commit can never reach
18
+ * the remote because this hook refuses to push it.
19
+ *
20
+ * Behavior:
21
+ * - Scans commits on HEAD that aren't yet on origin/main.
22
+ * - Fails the push if any of them match the fixture-pollution signature
23
+ * (author email test@instar.local / t@t.com / t@e.com, or commit
24
+ * messages "Initial commit" / "seed" / "Worktree commit <N>").
25
+ * - Prints what it found so the author can `git reset` past the bad
26
+ * commits and re-push.
27
+ *
28
+ * Escape hatch (for legitimate fixture commits in test-only branches
29
+ * that will never merge): INSTAR_PRE_PUSH_FIXTURE_GUARD_SKIP=1
30
+ *
31
+ * Exit codes: 0 = clean (or skipped), 1 = pollution detected.
32
+ */
33
+
34
+ import { execFileSync } from 'node:child_process';
35
+
36
+ const SKIP = process.env.INSTAR_PRE_PUSH_FIXTURE_GUARD_SKIP === '1';
37
+ if (SKIP) {
38
+ console.log('⏭️ INSTAR_PRE_PUSH_FIXTURE_GUARD_SKIP=1 — fixture guard bypassed.');
39
+ process.exit(0);
40
+ }
41
+
42
+ // Strip the same env overrides we accuse of causing the bug. Otherwise
43
+ // our own `git log` here would be subject to the same redirection.
44
+ const env = { ...process.env };
45
+ delete env.GIT_DIR;
46
+ delete env.GIT_WORK_TREE;
47
+ delete env.GIT_INDEX_FILE;
48
+ delete env.GIT_OBJECT_DIRECTORY;
49
+ delete env.GIT_COMMON_DIR;
50
+
51
+ function git(args) {
52
+ return execFileSync('git', args, { encoding: 'utf-8', env, stdio: ['ignore', 'pipe', 'pipe'] });
53
+ }
54
+
55
+ let baseRef;
56
+ try {
57
+ // Prefer upstream tracking branch if one is configured.
58
+ baseRef = git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']).trim();
59
+ } catch {
60
+ // No upstream — fall back to whichever remote main we can resolve.
61
+ for (const candidate of ['upstream/main', 'origin/main']) {
62
+ try {
63
+ git(['rev-parse', '--verify', '--quiet', candidate]);
64
+ baseRef = candidate;
65
+ break;
66
+ } catch { /* try next */ }
67
+ }
68
+ }
69
+
70
+ if (!baseRef) {
71
+ // No remote main to compare against — likely a fresh repo. Skip silently.
72
+ process.exit(0);
73
+ }
74
+
75
+ // `<base>..HEAD` lists commits on HEAD that aren't on base. NUL-separated
76
+ // records of "<sha>\t<author_email>\t<author_name>\t<subject>" — using \0
77
+ // between records lets us survive commit messages that contain newlines.
78
+ let log;
79
+ try {
80
+ log = git([
81
+ 'log',
82
+ `${baseRef}..HEAD`,
83
+ '--pretty=format:%H%x09%ae%x09%an%x09%s%x00',
84
+ ]);
85
+ } catch (err) {
86
+ // Couldn't compute the range. Don't block the push — a real failure
87
+ // would surface elsewhere; we'd rather miss a guard than wedge pushes
88
+ // on every fresh repo / unborn branch.
89
+ process.exit(0);
90
+ }
91
+
92
+ const records = log.split('\0').map((r) => r.trim()).filter(Boolean);
93
+
94
+ const BAD_EMAILS = new Set([
95
+ 'test@instar.local',
96
+ 't@t.com',
97
+ 't@e.com',
98
+ 'test@test.com',
99
+ ]);
100
+
101
+ const BAD_SUBJECT_PATTERNS = [
102
+ /^Initial commit$/,
103
+ /^seed$/,
104
+ /^Worktree commit \d+$/,
105
+ /^init$/,
106
+ ];
107
+
108
+ const violations = [];
109
+ for (const record of records) {
110
+ const [sha, email, name, subject] = record.split('\t');
111
+ const reasons = [];
112
+ if (BAD_EMAILS.has(email)) {
113
+ reasons.push(`fixture-identity author <${email}>`);
114
+ }
115
+ for (const pattern of BAD_SUBJECT_PATTERNS) {
116
+ if (pattern.test(subject)) {
117
+ reasons.push(`fixture commit message "${subject}"`);
118
+ break;
119
+ }
120
+ }
121
+ if (reasons.length > 0) {
122
+ violations.push({ sha: sha.slice(0, 10), name, email, subject, reasons });
123
+ }
124
+ }
125
+
126
+ if (violations.length === 0) {
127
+ process.exit(0);
128
+ }
129
+
130
+ const lines = [];
131
+ lines.push('');
132
+ lines.push('🚫 pre-push-fixture-guard: refusing to push test-fixture commits.');
133
+ lines.push('');
134
+ lines.push('These commits look like test-fixture pollution (test wrote into');
135
+ lines.push('the real repo because GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE');
136
+ lines.push('leaked from this pre-push hook into a child test process):');
137
+ lines.push('');
138
+ for (const v of violations) {
139
+ lines.push(` ${v.sha} ${v.name} <${v.email}> "${v.subject}"`);
140
+ for (const r of v.reasons) {
141
+ lines.push(` → ${r}`);
142
+ }
143
+ }
144
+ lines.push('');
145
+ lines.push('How to recover:');
146
+ lines.push(` 1. Inspect the commits: git log ${baseRef}..HEAD`);
147
+ lines.push(` 2. Drop them: git reset --hard <last good sha>`);
148
+ lines.push(' 3. Force-push the cleaned branch.');
149
+ lines.push('');
150
+ lines.push('If this is intentional (e.g. a test-only branch you do not plan');
151
+ lines.push('to merge), bypass with INSTAR_PRE_PUSH_FIXTURE_GUARD_SKIP=1.');
152
+ lines.push('');
153
+
154
+ process.stderr.write(lines.join('\n'));
155
+ process.exit(1);
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-05-20T22:26:32.053Z",
5
- "instarVersion": "1.1.2",
4
+ "generatedAt": "2026-05-21T04:11:40.504Z",
5
+ "instarVersion": "1.1.4",
6
6
  "entryCount": 190,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -752,7 +752,7 @@
752
752
  "type": "cli-command",
753
753
  "domain": "setup",
754
754
  "sourcePath": "src/cli.ts",
755
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
755
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
756
756
  "since": "2025-01-01"
757
757
  },
758
758
  "cli:setup": {
@@ -760,7 +760,7 @@
760
760
  "type": "cli-command",
761
761
  "domain": "setup",
762
762
  "sourcePath": "src/cli.ts",
763
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
763
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
764
764
  "since": "2025-01-01"
765
765
  },
766
766
  "cli:add": {
@@ -768,7 +768,7 @@
768
768
  "type": "cli-command",
769
769
  "domain": "setup",
770
770
  "sourcePath": "src/cli.ts",
771
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
771
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
772
772
  "since": "2025-01-01"
773
773
  },
774
774
  "cli:backup": {
@@ -776,7 +776,7 @@
776
776
  "type": "cli-command",
777
777
  "domain": "operations",
778
778
  "sourcePath": "src/cli.ts",
779
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
779
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
780
780
  "since": "2025-01-01"
781
781
  },
782
782
  "cli:git": {
@@ -784,7 +784,7 @@
784
784
  "type": "cli-command",
785
785
  "domain": "coordination",
786
786
  "sourcePath": "src/cli.ts",
787
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
787
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
788
788
  "since": "2025-01-01"
789
789
  },
790
790
  "cli:memory": {
@@ -792,7 +792,7 @@
792
792
  "type": "cli-command",
793
793
  "domain": "memory",
794
794
  "sourcePath": "src/cli.ts",
795
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
795
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
796
796
  "since": "2025-01-01"
797
797
  },
798
798
  "cli:knowledge": {
@@ -800,7 +800,7 @@
800
800
  "type": "cli-command",
801
801
  "domain": "memory",
802
802
  "sourcePath": "src/cli.ts",
803
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
803
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
804
804
  "since": "2025-01-01"
805
805
  },
806
806
  "cli:semantic": {
@@ -808,7 +808,7 @@
808
808
  "type": "cli-command",
809
809
  "domain": "memory",
810
810
  "sourcePath": "src/cli.ts",
811
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
811
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
812
812
  "since": "2025-01-01"
813
813
  },
814
814
  "cli:intent": {
@@ -816,7 +816,7 @@
816
816
  "type": "cli-command",
817
817
  "domain": "intent",
818
818
  "sourcePath": "src/cli.ts",
819
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
819
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
820
820
  "since": "2025-01-01"
821
821
  },
822
822
  "cli:feedback": {
@@ -824,7 +824,7 @@
824
824
  "type": "cli-command",
825
825
  "domain": "feedback",
826
826
  "sourcePath": "src/cli.ts",
827
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
827
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
828
828
  "since": "2025-01-01"
829
829
  },
830
830
  "cli:server": {
@@ -832,7 +832,7 @@
832
832
  "type": "cli-command",
833
833
  "domain": "server",
834
834
  "sourcePath": "src/cli.ts",
835
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
835
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
836
836
  "since": "2025-01-01"
837
837
  },
838
838
  "cli:status": {
@@ -840,7 +840,7 @@
840
840
  "type": "cli-command",
841
841
  "domain": "monitoring",
842
842
  "sourcePath": "src/cli.ts",
843
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
843
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
844
844
  "since": "2025-01-01"
845
845
  },
846
846
  "cli:user": {
@@ -848,7 +848,7 @@
848
848
  "type": "cli-command",
849
849
  "domain": "users",
850
850
  "sourcePath": "src/cli.ts",
851
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
851
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
852
852
  "since": "2025-01-01"
853
853
  },
854
854
  "cli:relationship": {
@@ -856,7 +856,7 @@
856
856
  "type": "cli-command",
857
857
  "domain": "relationships",
858
858
  "sourcePath": "src/cli.ts",
859
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
859
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
860
860
  "since": "2025-01-01"
861
861
  },
862
862
  "cli:job": {
@@ -864,7 +864,7 @@
864
864
  "type": "cli-command",
865
865
  "domain": "scheduling",
866
866
  "sourcePath": "src/cli.ts",
867
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
867
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
868
868
  "since": "2025-01-01"
869
869
  },
870
870
  "cli:lifeline": {
@@ -872,7 +872,7 @@
872
872
  "type": "cli-command",
873
873
  "domain": "communication",
874
874
  "sourcePath": "src/cli.ts",
875
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
875
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
876
876
  "since": "2025-01-01"
877
877
  },
878
878
  "cli:list": {
@@ -880,7 +880,7 @@
880
880
  "type": "cli-command",
881
881
  "domain": "monitoring",
882
882
  "sourcePath": "src/cli.ts",
883
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
883
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
884
884
  "since": "2025-01-01"
885
885
  },
886
886
  "cli:autostart": {
@@ -888,7 +888,7 @@
888
888
  "type": "cli-command",
889
889
  "domain": "operations",
890
890
  "sourcePath": "src/cli.ts",
891
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
891
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
892
892
  "since": "2025-01-01"
893
893
  },
894
894
  "cli:migrate": {
@@ -896,7 +896,7 @@
896
896
  "type": "cli-command",
897
897
  "domain": "updates",
898
898
  "sourcePath": "src/cli.ts",
899
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
899
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
900
900
  "since": "2025-01-01"
901
901
  },
902
902
  "cli:machines": {
@@ -904,7 +904,7 @@
904
904
  "type": "cli-command",
905
905
  "domain": "coordination",
906
906
  "sourcePath": "src/cli.ts",
907
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
907
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
908
908
  "since": "2025-01-01"
909
909
  },
910
910
  "cli:whoami": {
@@ -912,7 +912,7 @@
912
912
  "type": "cli-command",
913
913
  "domain": "identity",
914
914
  "sourcePath": "src/cli.ts",
915
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
915
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
916
916
  "since": "2025-01-01"
917
917
  },
918
918
  "cli:pair": {
@@ -920,7 +920,7 @@
920
920
  "type": "cli-command",
921
921
  "domain": "coordination",
922
922
  "sourcePath": "src/cli.ts",
923
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
923
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
924
924
  "since": "2025-01-01"
925
925
  },
926
926
  "cli:join": {
@@ -928,7 +928,7 @@
928
928
  "type": "cli-command",
929
929
  "domain": "coordination",
930
930
  "sourcePath": "src/cli.ts",
931
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
931
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
932
932
  "since": "2025-01-01"
933
933
  },
934
934
  "cli:wakeup": {
@@ -936,7 +936,7 @@
936
936
  "type": "cli-command",
937
937
  "domain": "coordination",
938
938
  "sourcePath": "src/cli.ts",
939
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
939
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
940
940
  "since": "2025-01-01"
941
941
  },
942
942
  "cli:leave": {
@@ -944,7 +944,7 @@
944
944
  "type": "cli-command",
945
945
  "domain": "coordination",
946
946
  "sourcePath": "src/cli.ts",
947
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
947
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
948
948
  "since": "2025-01-01"
949
949
  },
950
950
  "cli:doctor": {
@@ -952,7 +952,7 @@
952
952
  "type": "cli-command",
953
953
  "domain": "monitoring",
954
954
  "sourcePath": "src/cli.ts",
955
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
955
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
956
956
  "since": "2025-01-01"
957
957
  },
958
958
  "cli:review": {
@@ -960,7 +960,7 @@
960
960
  "type": "cli-command",
961
961
  "domain": "monitoring",
962
962
  "sourcePath": "src/cli.ts",
963
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
963
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
964
964
  "since": "2025-01-01"
965
965
  },
966
966
  "cli:nuke": {
@@ -968,7 +968,7 @@
968
968
  "type": "cli-command",
969
969
  "domain": "operations",
970
970
  "sourcePath": "src/cli.ts",
971
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
971
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
972
972
  "since": "2025-01-01"
973
973
  },
974
974
  "cli:playbook": {
@@ -976,7 +976,7 @@
976
976
  "type": "cli-command",
977
977
  "domain": "context",
978
978
  "sourcePath": "src/cli.ts",
979
- "contentHash": "a20f6fdbf151bdf22fb8f134fcb8bee872148b34e1f49d1fd962fb47e7200ba1",
979
+ "contentHash": "ef620787cb2d40a51831b00ba7071e67aa0d4cac86027dc3a04635d344608d29",
980
980
  "since": "2025-01-01"
981
981
  },
982
982
  "template:build-stop-hook.sh": {
@@ -1496,7 +1496,7 @@
1496
1496
  "type": "subsystem",
1497
1497
  "domain": "communication",
1498
1498
  "sourcePath": "src/lifeline/TelegramLifeline.ts",
1499
- "contentHash": "ad363d58349530646209a1ee6b97e0039449c70c6cf61c87d3c590c9e39d25e4",
1499
+ "contentHash": "45cd2b449e34134cae67859e79e378ccc131f481b4d5701faf26913b8a3a26e2",
1500
1500
  "since": "2025-01-01"
1501
1501
  },
1502
1502
  "subsystem:orphan-process-reaper": {
@@ -0,0 +1,124 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+ <!-- patch = bug fixes, refactors, test additions, doc updates -->
5
+
6
+ ## What Changed
7
+
8
+ **fix(lifeline): close the silent-drop version-skew failure class.**
9
+
10
+ Five interlocking fixes that together address the 2026-05-19 →
11
+ 2026-05-20 b2lead-insights field incident (21h silent Telegram ingress
12
+ drop after a server auto-update from v1.0.13 → v1.1.0 left the
13
+ lifeline on v1.0.13; server's `/internal/telegram-forward` rejected
14
+ the lifeline with HTTP 426 and the lifeline's drop-after-3-failures
15
+ replay policy silently lost user messages).
16
+
17
+ 1. **`rateLimitState.decide` — `versionSkew` bucket bypasses
18
+ cooldown.** A hard incompatibility cannot be cured by waiting; the
19
+ per-minute cooldown wedged every restart attempt. The daily cap
20
+ (3 per 24h) and storm detection remain as backstops.
21
+
22
+ 2. **Replay-loop drop policy is gated on a version-skew episode
23
+ flag.** When `forwardToServer` raises `ForwardVersionSkewError`,
24
+ the lifeline sets `versionSkewActive = true` and sends one
25
+ user-visible Telegram alert ("ingress paused: version skew
26
+ detected, your messages are not lost"). The replay loop checks
27
+ the flag BEFORE the `MAX_REPLAY_FAILURES` drop check and re-queues
28
+ without incrementing the failure counter. Cleared on the next
29
+ successful forward.
30
+
31
+ 3. **`instar lifeline restart` CLI uses the correct service label.**
32
+ Was `com.instar.<projectName>.lifeline` — different domain AND
33
+ different suffix from what `installMacOSLaunchAgent` writes
34
+ (`ai.instar.<projectName>`). `launchctl kickstart` always failed,
35
+ silently fell back to pkill.
36
+
37
+ 4. **pkill fallback escalates to SIGKILL; lock-acquire recognizes
38
+ wedged sleeping processes.** SIGTERM, 3s grace, SIGKILL by name
39
+ pattern in the CLI. In `acquireLockFile`, 'S' (sleeping) state
40
+ with lock-write age > 5 min is treated as a wedged lifeline:
41
+ SIGTERM → 3s poll → SIGKILL → take the lock.
42
+
43
+ 5. **`npm rebuild` for better-sqlite3 uses `--build-from-source` and
44
+ `--ignore-scripts`.** Two call sites: `ServerSupervisor`
45
+ preflight rebuild and `NativeModuleHealer.healBetterSqlite3Sync`.
46
+ Without `--build-from-source`, npm can install a cached prebuilt
47
+ with the same wrong ABI and exit 0 — producing the "rebuild
48
+ succeeded but module still fails to load" pattern in the b2lead
49
+ logs.
50
+
51
+ Files touched (excluding tests / docs / upgrade notes):
52
+ - `src/lifeline/rateLimitState.ts` — `decide()`
53
+ - `src/lifeline/TelegramLifeline.ts` — instance state,
54
+ `handleVersionSkew`, `forwardToServer`, `replayQueue`,
55
+ `acquireLockFile`
56
+ - `src/cli.ts` — `lifelineCmd.command('restart')`
57
+ - `src/lifeline/ServerSupervisor.ts` — preflight rebuild argv
58
+ - `src/memory/NativeModuleHealer.ts` — in-line rebuild argv
59
+
60
+ ## Evidence
61
+
62
+ **Repro of the original failure** (b2lead-insights, 2026-05-19 evening
63
+ → 2026-05-20 ~17:50 UTC):
64
+
65
+ 1. Server auto-updated v1.0.13 → v1.1.0.
66
+ 2. Lifeline kept running v1.0.13.
67
+ 3. User Telegram messages → lifeline forwards to server → server
68
+ returns HTTP 426 with `{ok: false, upgradeRequired: true,
69
+ serverVersion: '1.1.0', action: 'restart', reason: 'major-minor-mismatch'}`.
70
+ 4. Lifeline catches `ForwardVersionSkewError`, calls
71
+ `handleVersionSkew` → `initiateRestart('versionSkew', ...)`.
72
+ 5. `RestartOrchestrator.maybeRestart` consults `rateLimitState.decide`.
73
+ `elapsed < WATCHDOG_COOLDOWN_MS` → returns `{allowed: false, reason:
74
+ 'cooldown-active'}`. No restart.
75
+ 6. Lifeline returns false from `forwardToServer`. Replay loop
76
+ increments `replayFailures`. After 3 attempts → drop.
77
+ 7. 40+ "server-down" notifications + at least 2 explicitly-logged
78
+ dropped messages (`tg-6577`, `tg-6580`). No user-visible alert.
79
+
80
+ **Observed before:** `rateLimitState.ts:112` had no special-case for
81
+ `versionSkew` — the cooldown check fired for all buckets. The replay
82
+ loop at `TelegramLifeline.ts:1394` checked `failures >=
83
+ MAX_REPLAY_FAILURES` unconditionally. The CLI restart command at
84
+ `cli.ts:1639` hardcoded `com.instar.${projectName}.lifeline`.
85
+ `ServerSupervisor.ts:751` and `NativeModuleHealer.ts:397` both ran
86
+ `npm rebuild better-sqlite3` without `--build-from-source`.
87
+
88
+ **Observed after:**
89
+ - 26 new + extended unit tests (8 new in
90
+ `tests/unit/lifeline/version-skew-recovery.test.ts`, 1 new in
91
+ `tests/unit/lifeline/rateLimitState.test.ts`) cover all five paths.
92
+ - Source assertions verify the supervisor rebuild argv includes
93
+ `--build-from-source` AND `--ignore-scripts`, the healer in-line
94
+ rebuild includes both flags, the CLI restart command builds
95
+ `ai.instar.${...}` and never `com.instar.${...}.lifeline`, the
96
+ pkill fallback contains both `-TERM` and `-KILL`, the
97
+ `versionSkewActive` flag is set in `handleVersionSkew`, cleared in
98
+ `forwardToServer` success, checked in `replayQueue` BEFORE the
99
+ drop branch, and the lock-acquire path recognizes wedged-S.
100
+
101
+ Spec: `docs/specs/lifeline-version-skew-recovery.md`
102
+ ELI16: `docs/specs/lifeline-version-skew-recovery.eli16.md`
103
+ Side-effects review: `upgrades/side-effects/lifeline-version-skew-recovery.md`
104
+
105
+ ## What to Tell Your User
106
+
107
+ - **You will never silently lose a message during an auto-update
108
+ again**: if my server and my Telegram lifeline temporarily get out
109
+ of sync after an update, I'll send you one clear note that ingress
110
+ is paused, keep all your messages safely in the queue, and replay
111
+ them as soon as the lifeline catches up.
112
+ - **Recovery is automatic**: the bug class that needed a manual
113
+ restart to clear is now self-healing. No operator intervention.
114
+
115
+ ## Summary of New Capabilities
116
+
117
+ | Capability | How to Use |
118
+ |-----------|-----------|
119
+ | Version-skew cooldown bypass | automatic |
120
+ | Version-skew user alert (one per episode) | automatic |
121
+ | No-drop policy during version-skew episodes | automatic |
122
+ | CLI lifeline restart resolves correct service | automatic via `instar lifeline restart` |
123
+ | Wedged-lifeline lock recovery | automatic |
124
+ | Force-from-source native rebuilds | automatic |
@@ -0,0 +1,42 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+ <!-- patch = bug fixes, refactors, test additions, doc updates -->
5
+
6
+ ## What Changed
7
+
8
+ **fix(test-env): close the inherited-GIT_DIR failure class.**
9
+
10
+ When git invokes a hook (`.husky/pre-push` runs `npm run test:smoke`), it sets `GIT_DIR` / `GIT_WORK_TREE` / `GIT_INDEX_FILE` / `GIT_OBJECT_DIRECTORY` / `GIT_COMMON_DIR` on the child env. Those variables override cwd-based repo resolution for every descendant git process, so test fixtures that did `git init && git commit` in a tmpdir actually committed into the parent repo on whichever branch was checked out. That's what landed `# Test Project` over the project's `README.md` on `main` via PRs #130 and #277 — the README clobber shipped to npm in the v1.1.x series before PR #285 restored it.
11
+
12
+ This release closes the failure class in five interlocking layers:
13
+
14
+ 1. **`tests/vitest-setup.ts` strips the GIT_DIR family** from `process.env` before any test file loads. Single-line root-cause fix — no test in the suite can inherit the dangerous overrides from the parent pre-push environment.
15
+
16
+ 2. **`tests/helpers/git-test-env.ts` exports `sanitizedGitEnv()`** for fixtures that pass their own `env` object to `spawnSync` / `execFileSync`. Applied to the three fixtures whose write patterns are visible in PR #130's diff (`worktree-monitor.test.ts`, `SafeGitExecutor.test.ts`, `rich-profile-integration.test.ts`). Defense-in-depth on top of Layer 1.
17
+
18
+ 3. **`scripts/pre-push-fixture-guard.mjs`** runs in `.husky/pre-push` before the smoke tests and refuses pushes whose ahead-of-upstream commits carry the historical fixture signature — author email in `{test@instar.local, t@t.com, t@e.com, test@test.com}` or subject matching `Initial commit` / `seed` / `init` / `Worktree commit N`. Bypass for legitimate test-only branches: `INSTAR_PRE_PUSH_FIXTURE_GUARD_SKIP=1`.
19
+
20
+ 4. **`scripts/check-repo-invariants.mjs`** runs as a dedicated `Repo Invariants` CI job and fails the build if `README.md` drops below 100 lines or if `file-0.txt` / `seed` appear at repo root. Final structural safety net; threshold overridable via `INSTAR_README_MIN_LINES`.
21
+
22
+ 5. **Removed `file-0.txt` and `seed`** stowaway files from repo root — fixture droppings from prior incidents that would have falsely passed Layer 4 on the PR adding it.
23
+
24
+ Spec: `docs/specs/test-env-isolation.md`. ELI16: `docs/specs/test-env-isolation.eli16.md`. Side-effects review: `upgrades/side-effects/test-env-isolation.md`.
25
+
26
+ ## What to Tell Your User
27
+
28
+ Nothing user-visible. This release hardens instar's own development pipeline against a class of bug that, while invisible to running agents, had been quietly corrupting our published README. Agents continue to behave identically.
29
+
30
+ If a contributor asks why their push is suddenly refused with a fixture-pollution guard error, the script's own output includes recovery instructions — usually resetting past a stray commit and pushing again.
31
+
32
+ ## Summary of New Capabilities
33
+
34
+ This release is a pure hardening fix. No new runtime capabilities for agents. The new pre-push and CI guards are infrastructure for the instar-developing agent and direct contributors only.
35
+
36
+ ## Evidence
37
+
38
+ The README clobber on `main` was reproduced before the fix landed: `gh api repos/JKHeadley/instar/contents/README.md --jq .size` returned `7` (the literal text `# Test\n`) prior to PR #285. PR #285 restored the README; this PR closes the underlying class so the same regression cannot recur.
39
+
40
+ Test-fixture pollution detection was verified end-to-end: against a tmpdir repo with a commit subject `Initial commit`, `node scripts/pre-push-fixture-guard.mjs` exits 1 and names the offending commit. Against the same repo with the env bypass set, the script exits 0. Repo-invariant detection was verified end-to-end: against a repo where `README.md` has been trimmed to a single line, `node scripts/check-repo-invariants.mjs` exits 1 and reports the line count; against a healthy repo, it exits 0.
41
+
42
+ 14 new tests across the env-strip regression, the fixture-guard, and the invariants script. The three refactored fixtures (`worktree-monitor.test.ts`, `SafeGitExecutor.test.ts`, `rich-profile-integration.test.ts`) retain their pre-existing 45+ test assertions.