@bookedsolid/rea 0.13.0 → 0.13.1
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.
|
@@ -147,6 +147,39 @@ export declare function isLegacyReaManagedHuskyGate(content: string): boolean;
|
|
|
147
147
|
* — we don't overwrite consumer-authored hooks that respect the gate.
|
|
148
148
|
*/
|
|
149
149
|
export declare function referencesReviewGate(content: string): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* True when `content` is a husky 9 auto-generated stub that delegates to
|
|
152
|
+
* the canonical hook body via `.husky/_/h`.
|
|
153
|
+
*
|
|
154
|
+
* Husky 9 layout when `core.hooksPath=.husky/_`:
|
|
155
|
+
* .husky/<hookname> — user/rea-authored body (committed, source of truth)
|
|
156
|
+
* .husky/_/<hookname> — auto-generated stub git actually fires
|
|
157
|
+
* .husky/_/h — runner that exec's `.husky/<hookname>`
|
|
158
|
+
*
|
|
159
|
+
* The stub body is a single non-comment line, e.g.
|
|
160
|
+
* . "${0%/*}/h"
|
|
161
|
+
* . "$(dirname -- "$0")/h"
|
|
162
|
+
*
|
|
163
|
+
* Without this detection `rea doctor` would classify `.husky/_/pre-push`
|
|
164
|
+
* as foreign (no rea marker, no `rea hook push-gate` reference) even
|
|
165
|
+
* though the hook git fires sources the canonical body that DOES carry
|
|
166
|
+
* governance. The stub is the husky 9 indirection contract — treating it
|
|
167
|
+
* as the active hook and following the source chain to the parent hook
|
|
168
|
+
* resolves the false positive.
|
|
169
|
+
*
|
|
170
|
+
* Detection is conservative: any non-comment, non-blank, non-source line
|
|
171
|
+
* disqualifies. We anchor on `$0` in the dirname expansion to confirm the
|
|
172
|
+
* stub references self, and on the trailing `/h` segment to confirm it
|
|
173
|
+
* sources the husky 9 runner. A user-authored shell script that happens
|
|
174
|
+
* to dot-source a file does NOT match.
|
|
175
|
+
*/
|
|
176
|
+
export declare function isHusky9Stub(content: string): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a husky 9 stub at `<dir>/<hookname>` to the canonical hook
|
|
179
|
+
* body at `<parent-of-dir>/<hookname>`. Returns null when the stub
|
|
180
|
+
* lives at the filesystem root (no parent to walk to).
|
|
181
|
+
*/
|
|
182
|
+
export declare function resolveHusky9StubTarget(stubPath: string): string | null;
|
|
150
183
|
export declare function resolveHooksDir(targetDir: string): Promise<{
|
|
151
184
|
dir: string | null;
|
|
152
185
|
configured: boolean;
|
|
@@ -167,7 +200,9 @@ export type ClassifyExistingHook = {
|
|
|
167
200
|
kind: 'foreign';
|
|
168
201
|
reason: string;
|
|
169
202
|
};
|
|
170
|
-
export declare function classifyExistingHook(hookPath: string
|
|
203
|
+
export declare function classifyExistingHook(hookPath: string, options?: {
|
|
204
|
+
followHusky9Stub?: boolean;
|
|
205
|
+
}): Promise<ClassifyExistingHook>;
|
|
171
206
|
export type InstallDecision = {
|
|
172
207
|
action: 'skip';
|
|
173
208
|
reason: 'active-pre-push-present';
|
|
@@ -349,6 +349,71 @@ export function referencesReviewGate(content) {
|
|
|
349
349
|
}
|
|
350
350
|
return false;
|
|
351
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* True when `content` is a husky 9 auto-generated stub that delegates to
|
|
354
|
+
* the canonical hook body via `.husky/_/h`.
|
|
355
|
+
*
|
|
356
|
+
* Husky 9 layout when `core.hooksPath=.husky/_`:
|
|
357
|
+
* .husky/<hookname> — user/rea-authored body (committed, source of truth)
|
|
358
|
+
* .husky/_/<hookname> — auto-generated stub git actually fires
|
|
359
|
+
* .husky/_/h — runner that exec's `.husky/<hookname>`
|
|
360
|
+
*
|
|
361
|
+
* The stub body is a single non-comment line, e.g.
|
|
362
|
+
* . "${0%/*}/h"
|
|
363
|
+
* . "$(dirname -- "$0")/h"
|
|
364
|
+
*
|
|
365
|
+
* Without this detection `rea doctor` would classify `.husky/_/pre-push`
|
|
366
|
+
* as foreign (no rea marker, no `rea hook push-gate` reference) even
|
|
367
|
+
* though the hook git fires sources the canonical body that DOES carry
|
|
368
|
+
* governance. The stub is the husky 9 indirection contract — treating it
|
|
369
|
+
* as the active hook and following the source chain to the parent hook
|
|
370
|
+
* resolves the false positive.
|
|
371
|
+
*
|
|
372
|
+
* Detection is conservative: any non-comment, non-blank, non-source line
|
|
373
|
+
* disqualifies. We anchor on `$0` in the dirname expansion to confirm the
|
|
374
|
+
* stub references self, and on the trailing `/h` segment to confirm it
|
|
375
|
+
* sources the husky 9 runner. A user-authored shell script that happens
|
|
376
|
+
* to dot-source a file does NOT match.
|
|
377
|
+
*/
|
|
378
|
+
export function isHusky9Stub(content) {
|
|
379
|
+
const lines = content.split(/\r?\n/);
|
|
380
|
+
// Match a `.` (source) command whose argument is the husky 9 runner `h`
|
|
381
|
+
// resolved relative to the stub's own location. The two shapes husky 9
|
|
382
|
+
// generates:
|
|
383
|
+
// . "${0%/*}/h" — POSIX param expansion (current default)
|
|
384
|
+
// . "$(dirname -- "$0")/h" — older variant still in the wild
|
|
385
|
+
// We do NOT accept arbitrary `. <path>/h` because that would false-match
|
|
386
|
+
// a user-authored hook that happens to source a file named `h`.
|
|
387
|
+
const sourceLine = /^\.\s+(?:"\$\{0%\/\*\}\/h"|"\$\(dirname[^)]*\$0[^)]*\)\/h")\s*$/;
|
|
388
|
+
let sawSource = false;
|
|
389
|
+
for (const rawLine of lines) {
|
|
390
|
+
const line = rawLine.trim();
|
|
391
|
+
if (line === '')
|
|
392
|
+
continue;
|
|
393
|
+
if (line.startsWith('#'))
|
|
394
|
+
continue; // shebang, comment
|
|
395
|
+
if (sawSource)
|
|
396
|
+
return false; // any line after the source line disqualifies
|
|
397
|
+
if (sourceLine.test(line)) {
|
|
398
|
+
sawSource = true;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return sawSource;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Resolve a husky 9 stub at `<dir>/<hookname>` to the canonical hook
|
|
407
|
+
* body at `<parent-of-dir>/<hookname>`. Returns null when the stub
|
|
408
|
+
* lives at the filesystem root (no parent to walk to).
|
|
409
|
+
*/
|
|
410
|
+
export function resolveHusky9StubTarget(stubPath) {
|
|
411
|
+
const dir = path.dirname(stubPath);
|
|
412
|
+
const parent = path.dirname(dir);
|
|
413
|
+
if (parent === dir)
|
|
414
|
+
return null;
|
|
415
|
+
return path.join(parent, path.basename(stubPath));
|
|
416
|
+
}
|
|
352
417
|
// ---------------------------------------------------------------------------
|
|
353
418
|
// Hook resolution
|
|
354
419
|
// ---------------------------------------------------------------------------
|
|
@@ -408,7 +473,8 @@ async function resolveTargetHookPath(targetDir) {
|
|
|
408
473
|
hooksPathConfigured: false,
|
|
409
474
|
};
|
|
410
475
|
}
|
|
411
|
-
export async function classifyExistingHook(hookPath) {
|
|
476
|
+
export async function classifyExistingHook(hookPath, options = {}) {
|
|
477
|
+
const followStub = options.followHusky9Stub ?? true;
|
|
412
478
|
let stat;
|
|
413
479
|
try {
|
|
414
480
|
stat = await fsPromises.lstat(hookPath);
|
|
@@ -442,6 +508,17 @@ export async function classifyExistingHook(hookPath) {
|
|
|
442
508
|
return { kind: 'rea-managed-legacy-v1' };
|
|
443
509
|
if (referencesReviewGate(content))
|
|
444
510
|
return { kind: 'gate-delegating' };
|
|
511
|
+
// Husky 9 indirection: when git fires `.husky/_/<hookname>` (auto-generated
|
|
512
|
+
// stub) the body just sources `.husky/_/h` which exec's the canonical
|
|
513
|
+
// `.husky/<hookname>`. Without this branch, doctor false-positives the stub
|
|
514
|
+
// as foreign even though governance is intact via the source chain.
|
|
515
|
+
// One level of follow only — never recurse stubs-of-stubs.
|
|
516
|
+
if (followStub && isHusky9Stub(content)) {
|
|
517
|
+
const target = resolveHusky9StubTarget(hookPath);
|
|
518
|
+
if (target !== null) {
|
|
519
|
+
return classifyExistingHook(target, { followHusky9Stub: false });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
445
522
|
return { kind: 'foreign', reason: 'no-marker' };
|
|
446
523
|
}
|
|
447
524
|
export async function classifyPrePushInstall(targetDir) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
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)",
|