@bookedsolid/rea 0.44.0 → 0.46.0
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/dist/cli/audit-by-tool.d.ts +173 -0
- package/dist/cli/audit-by-tool.js +373 -0
- package/dist/cli/audit-timeline.d.ts +160 -0
- package/dist/cli/audit-timeline.js +481 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/init.d.ts +109 -27
- package/dist/cli/init.js +191 -34
- package/package.json +3 -1
- package/scripts/profile-hooks.mjs +767 -0
package/dist/cli/init.d.ts
CHANGED
|
@@ -60,28 +60,65 @@ export interface ResolvedConfig {
|
|
|
60
60
|
reagentNotices: string[];
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* 0.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
63
|
+
* 0.45.0 charter item 2 — derive the canonical hook filename set
|
|
64
|
+
* PRIMARILY from the packaged `hooks/` filesystem tree (the literal
|
|
65
|
+
* shipped artifact), with the two source-code registries
|
|
66
|
+
* (`EXPECTED_HOOKS` and `defaultDesiredHooks()`) layered on top as
|
|
67
|
+
* defensive fallbacks.
|
|
68
|
+
*
|
|
69
|
+
* # Why filesystem-first
|
|
70
|
+
*
|
|
71
|
+
* 0.44.0 introduced this helper as the UNION of two source-code
|
|
72
|
+
* lists. Round-2 noticed a drift hazard: if either source-code list
|
|
73
|
+
* gets out of sync with the actual `hooks/` filesystem reality
|
|
74
|
+
* (e.g. a hook is added to `hooks/` but not to `EXPECTED_HOOKS`),
|
|
75
|
+
* the install-summary lies about what's about to land on disk.
|
|
76
|
+
* The filesystem is the source of truth — what the installer
|
|
77
|
+
* actually copies into `.claude/hooks/` is the contents of
|
|
78
|
+
* `hooks/`. Pinning the canonical set to the FS catches drift at
|
|
79
|
+
* runtime; the cross-check test in `init.test.ts` catches it at
|
|
80
|
+
* build time.
|
|
81
|
+
*
|
|
82
|
+
* # Strategy
|
|
83
|
+
*
|
|
84
|
+
* 1. Try to read `PKG_ROOT/hooks/*.sh` (filtered to exclude `_lib/`).
|
|
85
|
+
* This is the authoritative list — it's literally what the
|
|
86
|
+
* installer will copy into `.claude/hooks/`.
|
|
87
|
+
* 2. Union with `EXPECTED_HOOKS` (doctor's required list) — covers
|
|
88
|
+
* the future case where the FS read fails (e.g. an unusual
|
|
89
|
+
* install layout) but the source-code registry is intact.
|
|
90
|
+
* 3. Union with `defaultDesiredHooks()` basenames — covers the
|
|
91
|
+
* symmetric case where a hook is registered in settings.json
|
|
92
|
+
* but somehow absent from `EXPECTED_HOOKS`.
|
|
93
|
+
*
|
|
94
|
+
* Steps 2 and 3 are belt-and-suspenders. The cross-check test
|
|
95
|
+
* asserts all three sources agree; a drift between the FS and either
|
|
96
|
+
* source-code list fails the test loudly. In production the FS read
|
|
97
|
+
* (step 1) is the only one that contributes anything that wouldn't
|
|
98
|
+
* already be covered by steps 2+3 IF the test stays green.
|
|
81
99
|
*
|
|
82
100
|
* Sorted + deduped so the screen is stable across orderings.
|
|
101
|
+
*
|
|
102
|
+
* Exported for testability — the cross-check test imports it
|
|
103
|
+
* directly to compare against `canonicalHooksFromFilesystem()` and
|
|
104
|
+
* the two source-code registries.
|
|
83
105
|
*/
|
|
84
106
|
export declare function canonicalInstalledHooks(): string[];
|
|
107
|
+
/**
|
|
108
|
+
* 0.45.0 charter item 2 — read the canonical hook filename set
|
|
109
|
+
* directly from the packaged `hooks/` filesystem tree. Returns
|
|
110
|
+
* basenames (e.g. `dangerous-bash-interceptor.sh`) sorted ascending.
|
|
111
|
+
* Excludes anything under `_lib/` (shared helpers, not installed
|
|
112
|
+
* shims).
|
|
113
|
+
*
|
|
114
|
+
* Returns `[]` if the directory can't be read — caller is expected
|
|
115
|
+
* to union with `EXPECTED_HOOKS` / `defaultDesiredHooks()` so a
|
|
116
|
+
* missing FS doesn't produce a zero-length canonical list.
|
|
117
|
+
*
|
|
118
|
+
* Exported so the cross-check test can compare it against the two
|
|
119
|
+
* source-code registries and fail loudly on drift.
|
|
120
|
+
*/
|
|
121
|
+
export declare function canonicalHooksFromFilesystem(): string[];
|
|
85
122
|
/**
|
|
86
123
|
* 0.43.0 UX polish: build the human-readable install summary shown
|
|
87
124
|
* BEFORE any files are written. Lists, in order: the policy file
|
|
@@ -140,26 +177,71 @@ export declare function detectTargetState(targetDir: string): TargetState;
|
|
|
140
177
|
* filesystems and still verify the more meaningful invariant: the
|
|
141
178
|
* files exist and have non-empty bytes.
|
|
142
179
|
*
|
|
143
|
-
* Detection strategy —
|
|
180
|
+
* Detection strategy — three layers, ordered cheapest-first.
|
|
144
181
|
*
|
|
145
182
|
* 1. Platform — `process.platform === 'win32'` always skips the
|
|
146
183
|
* exec-bit check (native Windows has no POSIX mode bit; node's
|
|
147
184
|
* `stat.mode` is a translation that may or may not preserve the
|
|
148
185
|
* 0o111 bit depending on the source).
|
|
149
|
-
* 2.
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
186
|
+
* 2. Unambiguous shapes via sample — sample the FIRST `.sh` file:
|
|
187
|
+
*
|
|
188
|
+
* - All 0o777 bits clear (`0o000`) — historical mode-less shape.
|
|
189
|
+
* On a genuine Unix install no shipped hook is ever 0o000,
|
|
190
|
+
* and a chmod-stripped install (the only innocuous source of
|
|
191
|
+
* 0o000) would already be unusable so a false skip there is
|
|
192
|
+
* harmless (the substitute presence + non-empty check still
|
|
193
|
+
* fires).
|
|
194
|
+
* - All 0o777 bits set (`0o777`) — "no info, everything exec";
|
|
195
|
+
* some SMB / NTFS-via-FUSE mounts surface this so file IO
|
|
196
|
+
* works regardless of source mode.
|
|
197
|
+
*
|
|
198
|
+
* 3. Active mode-bit probe (0.45.0 codex round-1 P1 fix) — for
|
|
199
|
+
* ambiguous shapes like `0o644` / `0o666` where the sample
|
|
200
|
+
* COULD be "mode-less mount surfacing as 0o644" OR "chmod-
|
|
201
|
+
* stripped genuine Unix install", do an active probe:
|
|
202
|
+
*
|
|
203
|
+
* a. Write a temporary file with mode `0o755`.
|
|
204
|
+
* b. Stat it back; if the kernel returned a value missing
|
|
205
|
+
* the exec bits we just set, the FS truly ignores mode
|
|
206
|
+
* bits — mode-less.
|
|
207
|
+
* c. If the kernel returned `0o755` (preserved the mode),
|
|
208
|
+
* the FS DOES respect mode bits — the sampled hook's
|
|
209
|
+
* lack of exec bits is a real install failure, NOT a
|
|
210
|
+
* mode-less mount. Return false so the caller emits the
|
|
211
|
+
* genuine "zero executable .sh files" error.
|
|
212
|
+
* d. If the probe itself fails (EROFS, EPERM, ENOSPC,
|
|
213
|
+
* anything), fall through to false — let the caller
|
|
214
|
+
* surface the real installation failure rather than
|
|
215
|
+
* hide it behind an advisory.
|
|
216
|
+
*
|
|
217
|
+
* Pre-fix the `0o644` branch suppressed the exec-bit check
|
|
218
|
+
* unconditionally, masking genuinely broken Unix installs.
|
|
155
219
|
*
|
|
156
220
|
* Returns true when the exec-bit check should be SKIPPED.
|
|
157
221
|
*
|
|
158
222
|
* Exported for testability — callers can stub the filesystem and
|
|
159
|
-
* exercise
|
|
160
|
-
* up an actual Windows VM.
|
|
223
|
+
* exercise all three shapes without spinning up an actual Windows VM.
|
|
161
224
|
*/
|
|
162
225
|
export declare function isModeLessFilesystem(hooksDir: string): boolean;
|
|
226
|
+
/**
|
|
227
|
+
* 0.45.0 codex round-1 P1 fix: active probe to disambiguate a
|
|
228
|
+
* mode-less filesystem from a chmod-stripped genuine Unix install.
|
|
229
|
+
*
|
|
230
|
+
* Writes a temporary file with mode `0o755` and stats it back. If
|
|
231
|
+
* the kernel returns a value that LACKS the exec bits we just set,
|
|
232
|
+
* the filesystem is ignoring mode bits — it's truly mode-less.
|
|
233
|
+
* Otherwise (kernel preserves the mode, OR the probe fails for any
|
|
234
|
+
* reason), return false so the caller surfaces the real install
|
|
235
|
+
* failure instead of hiding it behind an advisory.
|
|
236
|
+
*
|
|
237
|
+
* Probe file is written into `hooksDir` to match the exact mount
|
|
238
|
+
* the caller is checking — sampling a different directory could
|
|
239
|
+
* cross a mount boundary and lie about the target FS. The file is
|
|
240
|
+
* always unlinked, even on probe failure.
|
|
241
|
+
*
|
|
242
|
+
* Exported for testability.
|
|
243
|
+
*/
|
|
244
|
+
export declare function filesystemIgnoresModeBits(hooksDir: string): boolean;
|
|
163
245
|
/**
|
|
164
246
|
* 0.43.0 UX polish: post-install sanity check. Runs synchronously
|
|
165
247
|
* after the file-write phase to catch installs that completed
|
package/dist/cli/init.js
CHANGED
|
@@ -18,7 +18,7 @@ import { CLAUDE_MD_MANIFEST_PATH, SETTINGS_MANIFEST_PATH, enumerateCanonicalFile
|
|
|
18
18
|
import { writeManifestAtomic } from './install/manifest-io.js';
|
|
19
19
|
import { sha256OfBuffer, sha256OfFile } from './install/sha.js';
|
|
20
20
|
import { defaultReagentPath, ReagentDroppedFieldsError, translateReagentPolicy, } from './install/reagent.js';
|
|
21
|
-
import { POLICY_FILE, REA_DIR, REGISTRY_FILE, err, getPkgVersion, log, warn } from './utils.js';
|
|
21
|
+
import { PKG_ROOT, POLICY_FILE, REA_DIR, REGISTRY_FILE, err, getPkgVersion, log, warn, } from './utils.js';
|
|
22
22
|
const PROFILE_NAMES = [
|
|
23
23
|
'minimal',
|
|
24
24
|
'client-engagement',
|
|
@@ -815,29 +815,53 @@ function readExistingManifestInstalledAt(manifestPath) {
|
|
|
815
815
|
return undefined;
|
|
816
816
|
}
|
|
817
817
|
/**
|
|
818
|
-
* 0.
|
|
819
|
-
*
|
|
818
|
+
* 0.45.0 charter item 2 — derive the canonical hook filename set
|
|
819
|
+
* PRIMARILY from the packaged `hooks/` filesystem tree (the literal
|
|
820
|
+
* shipped artifact), with the two source-code registries
|
|
821
|
+
* (`EXPECTED_HOOKS` and `defaultDesiredHooks()`) layered on top as
|
|
822
|
+
* defensive fallbacks.
|
|
820
823
|
*
|
|
821
|
-
*
|
|
822
|
-
* truth for "what `.claude/hooks/` must contain after install").
|
|
823
|
-
* - The `command` paths of every entry in `defaultDesiredHooks()`
|
|
824
|
-
* (the source of truth for "what `.claude/settings.json` registers
|
|
825
|
-
* with Claude Code"). Each command path ends in
|
|
826
|
-
* `.claude/hooks/<name>.sh`; we extract `<name>.sh` so the result
|
|
827
|
-
* joins cleanly with `EXPECTED_HOOKS`.
|
|
824
|
+
* # Why filesystem-first
|
|
828
825
|
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
826
|
+
* 0.44.0 introduced this helper as the UNION of two source-code
|
|
827
|
+
* lists. Round-2 noticed a drift hazard: if either source-code list
|
|
828
|
+
* gets out of sync with the actual `hooks/` filesystem reality
|
|
829
|
+
* (e.g. a hook is added to `hooks/` but not to `EXPECTED_HOOKS`),
|
|
830
|
+
* the install-summary lies about what's about to land on disk.
|
|
831
|
+
* The filesystem is the source of truth — what the installer
|
|
832
|
+
* actually copies into `.claude/hooks/` is the contents of
|
|
833
|
+
* `hooks/`. Pinning the canonical set to the FS catches drift at
|
|
834
|
+
* runtime; the cross-check test in `init.test.ts` catches it at
|
|
835
|
+
* build time.
|
|
836
|
+
*
|
|
837
|
+
* # Strategy
|
|
838
|
+
*
|
|
839
|
+
* 1. Try to read `PKG_ROOT/hooks/*.sh` (filtered to exclude `_lib/`).
|
|
840
|
+
* This is the authoritative list — it's literally what the
|
|
841
|
+
* installer will copy into `.claude/hooks/`.
|
|
842
|
+
* 2. Union with `EXPECTED_HOOKS` (doctor's required list) — covers
|
|
843
|
+
* the future case where the FS read fails (e.g. an unusual
|
|
844
|
+
* install layout) but the source-code registry is intact.
|
|
845
|
+
* 3. Union with `defaultDesiredHooks()` basenames — covers the
|
|
846
|
+
* symmetric case where a hook is registered in settings.json
|
|
847
|
+
* but somehow absent from `EXPECTED_HOOKS`.
|
|
848
|
+
*
|
|
849
|
+
* Steps 2 and 3 are belt-and-suspenders. The cross-check test
|
|
850
|
+
* asserts all three sources agree; a drift between the FS and either
|
|
851
|
+
* source-code list fails the test loudly. In production the FS read
|
|
852
|
+
* (step 1) is the only one that contributes anything that wouldn't
|
|
853
|
+
* already be covered by steps 2+3 IF the test stays green.
|
|
836
854
|
*
|
|
837
855
|
* Sorted + deduped so the screen is stable across orderings.
|
|
856
|
+
*
|
|
857
|
+
* Exported for testability — the cross-check test imports it
|
|
858
|
+
* directly to compare against `canonicalHooksFromFilesystem()` and
|
|
859
|
+
* the two source-code registries.
|
|
838
860
|
*/
|
|
839
861
|
export function canonicalInstalledHooks() {
|
|
840
|
-
const
|
|
862
|
+
const merged = new Set(canonicalHooksFromFilesystem());
|
|
863
|
+
for (const name of EXPECTED_HOOKS)
|
|
864
|
+
merged.add(name);
|
|
841
865
|
for (const group of defaultDesiredHooks()) {
|
|
842
866
|
for (const h of group.hooks) {
|
|
843
867
|
const cmd = h.command;
|
|
@@ -847,10 +871,53 @@ export function canonicalInstalledHooks() {
|
|
|
847
871
|
const slashIdx = cmd.lastIndexOf('/');
|
|
848
872
|
const basename = slashIdx >= 0 ? cmd.slice(slashIdx + 1) : cmd;
|
|
849
873
|
if (basename.endsWith('.sh'))
|
|
850
|
-
|
|
874
|
+
merged.add(basename);
|
|
851
875
|
}
|
|
852
876
|
}
|
|
853
|
-
return Array.from(
|
|
877
|
+
return Array.from(merged).sort();
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* 0.45.0 charter item 2 — read the canonical hook filename set
|
|
881
|
+
* directly from the packaged `hooks/` filesystem tree. Returns
|
|
882
|
+
* basenames (e.g. `dangerous-bash-interceptor.sh`) sorted ascending.
|
|
883
|
+
* Excludes anything under `_lib/` (shared helpers, not installed
|
|
884
|
+
* shims).
|
|
885
|
+
*
|
|
886
|
+
* Returns `[]` if the directory can't be read — caller is expected
|
|
887
|
+
* to union with `EXPECTED_HOOKS` / `defaultDesiredHooks()` so a
|
|
888
|
+
* missing FS doesn't produce a zero-length canonical list.
|
|
889
|
+
*
|
|
890
|
+
* Exported so the cross-check test can compare it against the two
|
|
891
|
+
* source-code registries and fail loudly on drift.
|
|
892
|
+
*/
|
|
893
|
+
export function canonicalHooksFromFilesystem() {
|
|
894
|
+
const dir = path.join(PKG_ROOT, 'hooks');
|
|
895
|
+
try {
|
|
896
|
+
return fs
|
|
897
|
+
.readdirSync(dir)
|
|
898
|
+
.filter((name) => name.endsWith('.sh'))
|
|
899
|
+
.filter((name) => {
|
|
900
|
+
try {
|
|
901
|
+
// Exclude subdirectories like `_lib/`; only top-level `.sh`
|
|
902
|
+
// files are shipped shims. `readdirSync` returns names from
|
|
903
|
+
// the directory itself, but a future `_lib/foo.sh` reachable
|
|
904
|
+
// via the root listing should still be excluded — hence the
|
|
905
|
+
// explicit isFile() check.
|
|
906
|
+
return fs.statSync(path.join(dir, name)).isFile();
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
})
|
|
912
|
+
.sort();
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
// PKG_ROOT/hooks/ unreadable — fall through to the caller's
|
|
916
|
+
// source-code union. This is a defensive branch; in practice the
|
|
917
|
+
// packaged tarball always ships hooks/, and source builds always
|
|
918
|
+
// have a hooks/ checked into the repo.
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
854
921
|
}
|
|
855
922
|
/**
|
|
856
923
|
* 0.43.0 UX polish: build the human-readable install summary shown
|
|
@@ -974,24 +1041,50 @@ export function detectTargetState(targetDir) {
|
|
|
974
1041
|
* filesystems and still verify the more meaningful invariant: the
|
|
975
1042
|
* files exist and have non-empty bytes.
|
|
976
1043
|
*
|
|
977
|
-
* Detection strategy —
|
|
1044
|
+
* Detection strategy — three layers, ordered cheapest-first.
|
|
978
1045
|
*
|
|
979
1046
|
* 1. Platform — `process.platform === 'win32'` always skips the
|
|
980
1047
|
* exec-bit check (native Windows has no POSIX mode bit; node's
|
|
981
1048
|
* `stat.mode` is a translation that may or may not preserve the
|
|
982
1049
|
* 0o111 bit depending on the source).
|
|
983
|
-
* 2.
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
1050
|
+
* 2. Unambiguous shapes via sample — sample the FIRST `.sh` file:
|
|
1051
|
+
*
|
|
1052
|
+
* - All 0o777 bits clear (`0o000`) — historical mode-less shape.
|
|
1053
|
+
* On a genuine Unix install no shipped hook is ever 0o000,
|
|
1054
|
+
* and a chmod-stripped install (the only innocuous source of
|
|
1055
|
+
* 0o000) would already be unusable so a false skip there is
|
|
1056
|
+
* harmless (the substitute presence + non-empty check still
|
|
1057
|
+
* fires).
|
|
1058
|
+
* - All 0o777 bits set (`0o777`) — "no info, everything exec";
|
|
1059
|
+
* some SMB / NTFS-via-FUSE mounts surface this so file IO
|
|
1060
|
+
* works regardless of source mode.
|
|
1061
|
+
*
|
|
1062
|
+
* 3. Active mode-bit probe (0.45.0 codex round-1 P1 fix) — for
|
|
1063
|
+
* ambiguous shapes like `0o644` / `0o666` where the sample
|
|
1064
|
+
* COULD be "mode-less mount surfacing as 0o644" OR "chmod-
|
|
1065
|
+
* stripped genuine Unix install", do an active probe:
|
|
1066
|
+
*
|
|
1067
|
+
* a. Write a temporary file with mode `0o755`.
|
|
1068
|
+
* b. Stat it back; if the kernel returned a value missing
|
|
1069
|
+
* the exec bits we just set, the FS truly ignores mode
|
|
1070
|
+
* bits — mode-less.
|
|
1071
|
+
* c. If the kernel returned `0o755` (preserved the mode),
|
|
1072
|
+
* the FS DOES respect mode bits — the sampled hook's
|
|
1073
|
+
* lack of exec bits is a real install failure, NOT a
|
|
1074
|
+
* mode-less mount. Return false so the caller emits the
|
|
1075
|
+
* genuine "zero executable .sh files" error.
|
|
1076
|
+
* d. If the probe itself fails (EROFS, EPERM, ENOSPC,
|
|
1077
|
+
* anything), fall through to false — let the caller
|
|
1078
|
+
* surface the real installation failure rather than
|
|
1079
|
+
* hide it behind an advisory.
|
|
1080
|
+
*
|
|
1081
|
+
* Pre-fix the `0o644` branch suppressed the exec-bit check
|
|
1082
|
+
* unconditionally, masking genuinely broken Unix installs.
|
|
989
1083
|
*
|
|
990
1084
|
* Returns true when the exec-bit check should be SKIPPED.
|
|
991
1085
|
*
|
|
992
1086
|
* Exported for testability — callers can stub the filesystem and
|
|
993
|
-
* exercise
|
|
994
|
-
* up an actual Windows VM.
|
|
1087
|
+
* exercise all three shapes without spinning up an actual Windows VM.
|
|
995
1088
|
*/
|
|
996
1089
|
export function isModeLessFilesystem(hooksDir) {
|
|
997
1090
|
if (process.platform === 'win32')
|
|
@@ -1008,12 +1101,21 @@ export function isModeLessFilesystem(hooksDir) {
|
|
|
1008
1101
|
return false;
|
|
1009
1102
|
}
|
|
1010
1103
|
const stat = fs.statSync(path.join(hooksDir, firstSh));
|
|
1011
|
-
|
|
1012
|
-
//
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1104
|
+
const perm = stat.mode & 0o777;
|
|
1105
|
+
// (a) All 0o777 bits clear — historical mode-less detection.
|
|
1106
|
+
if (perm === 0)
|
|
1107
|
+
return true;
|
|
1108
|
+
// (b) All 0o777 bits set — some SMB / FUSE mounts surface this.
|
|
1109
|
+
if (perm === 0o777)
|
|
1016
1110
|
return true;
|
|
1111
|
+
// (c) 0.45.0 codex round-1 P1 fix: when 0o111 bits are clear
|
|
1112
|
+
// (e.g. 0o644 / 0o666), we MUST disambiguate "mode-less
|
|
1113
|
+
// mount that surfaces as 0o644" from "chmod-stripped Unix
|
|
1114
|
+
// install" via an active write-then-stat probe. The pre-fix
|
|
1115
|
+
// unconditional skip masked genuinely-broken Unix installs.
|
|
1116
|
+
if ((perm & 0o111) === 0) {
|
|
1117
|
+
return filesystemIgnoresModeBits(hooksDir);
|
|
1118
|
+
}
|
|
1017
1119
|
return false;
|
|
1018
1120
|
}
|
|
1019
1121
|
catch {
|
|
@@ -1023,6 +1125,61 @@ export function isModeLessFilesystem(hooksDir) {
|
|
|
1023
1125
|
return false;
|
|
1024
1126
|
}
|
|
1025
1127
|
}
|
|
1128
|
+
/**
|
|
1129
|
+
* 0.45.0 codex round-1 P1 fix: active probe to disambiguate a
|
|
1130
|
+
* mode-less filesystem from a chmod-stripped genuine Unix install.
|
|
1131
|
+
*
|
|
1132
|
+
* Writes a temporary file with mode `0o755` and stats it back. If
|
|
1133
|
+
* the kernel returns a value that LACKS the exec bits we just set,
|
|
1134
|
+
* the filesystem is ignoring mode bits — it's truly mode-less.
|
|
1135
|
+
* Otherwise (kernel preserves the mode, OR the probe fails for any
|
|
1136
|
+
* reason), return false so the caller surfaces the real install
|
|
1137
|
+
* failure instead of hiding it behind an advisory.
|
|
1138
|
+
*
|
|
1139
|
+
* Probe file is written into `hooksDir` to match the exact mount
|
|
1140
|
+
* the caller is checking — sampling a different directory could
|
|
1141
|
+
* cross a mount boundary and lie about the target FS. The file is
|
|
1142
|
+
* always unlinked, even on probe failure.
|
|
1143
|
+
*
|
|
1144
|
+
* Exported for testability.
|
|
1145
|
+
*/
|
|
1146
|
+
export function filesystemIgnoresModeBits(hooksDir) {
|
|
1147
|
+
const probePath = path.join(hooksDir, `.rea-modeless-probe-${process.pid}-${Date.now()}`);
|
|
1148
|
+
try {
|
|
1149
|
+
// 0.45.0 codex round-2 P2: write WITHOUT the mode option, then
|
|
1150
|
+
// explicitly chmod to 0o755. `writeFileSync({ mode })` is filtered
|
|
1151
|
+
// through the process umask, so a caller running under e.g.
|
|
1152
|
+
// `umask 0111` would have their probe land as 0o644 even on a
|
|
1153
|
+
// real Unix FS — falsely flagging mode-less and re-introducing
|
|
1154
|
+
// the bug the round-1 fix was trying to close. Explicit chmod
|
|
1155
|
+
// bypasses umask and always lands exactly the bits we asked for
|
|
1156
|
+
// (when the FS honors them, which is the property we're probing).
|
|
1157
|
+
fs.writeFileSync(probePath, '');
|
|
1158
|
+
fs.chmodSync(probePath, 0o755);
|
|
1159
|
+
const stat = fs.statSync(probePath);
|
|
1160
|
+
const perm = stat.mode & 0o777;
|
|
1161
|
+
// If the kernel preserved any of our exec bits, the FS honors
|
|
1162
|
+
// mode bits — NOT mode-less.
|
|
1163
|
+
if ((perm & 0o111) !== 0)
|
|
1164
|
+
return false;
|
|
1165
|
+
// Kernel stripped every exec bit we wrote — mode-less.
|
|
1166
|
+
return true;
|
|
1167
|
+
}
|
|
1168
|
+
catch {
|
|
1169
|
+
// Probe write/stat failed (read-only mount, EPERM, ENOSPC).
|
|
1170
|
+
// Conservative: return false so the caller emits the real error
|
|
1171
|
+
// rather than swallow it behind an advisory.
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
finally {
|
|
1175
|
+
try {
|
|
1176
|
+
fs.unlinkSync(probePath);
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
// best-effort cleanup
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1026
1183
|
/**
|
|
1027
1184
|
* 0.43.0 UX polish: post-install sanity check. Runs synchronously
|
|
1028
1185
|
* after the file-write phase to catch installs that completed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"lint": "pnpm run lint:regex && pnpm run lint:awk-quotes && eslint .",
|
|
99
99
|
"lint:regex": "node scripts/lint-safe-regex.mjs",
|
|
100
100
|
"lint:awk-quotes": "node scripts/lint-awk-shim-quotes.mjs",
|
|
101
|
+
"perf:hooks": "pnpm run build && node scripts/profile-hooks.mjs",
|
|
101
102
|
"format": "prettier --write .",
|
|
102
103
|
"format:check": "prettier --check .",
|
|
103
104
|
"test": "pnpm run build && pnpm run test:dogfood && pnpm run test:bash-syntax && node scripts/run-vitest.mjs",
|
|
@@ -105,6 +106,7 @@
|
|
|
105
106
|
"test:coverage": "vitest run --coverage",
|
|
106
107
|
"test:dogfood": "node tools/check-dogfood-drift.mjs",
|
|
107
108
|
"test:bash-syntax": "bash -c 'for f in hooks/*.sh hooks/_lib/*.sh; do bash -n \"$f\" || exit 1; done && echo \"[bash-syntax] OK — all hooks parse cleanly\"'",
|
|
109
|
+
"test:perf": "pnpm run build && REA_INCLUDE_PERF=1 vitest run __tests__/scripts/profile-hooks.test.ts",
|
|
108
110
|
"type-check": "tsc --noEmit",
|
|
109
111
|
"changeset": "changeset",
|
|
110
112
|
"changeset:version": "changeset version",
|