@rmyndharis/aimhooman 0.1.1 → 0.1.2
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +147 -1
- package/CONTRIBUTING.md +36 -3
- package/README.md +5 -3
- package/bin/aimhooman.mjs +43 -23
- package/package.json +1 -1
- package/rules/attribution.json +7 -7
- package/src/atomic-write.mjs +7 -0
- package/src/git-environment.mjs +10 -0
- package/src/githooks.mjs +113 -19
- package/src/gitx.mjs +48 -265
- package/src/history-scan.mjs +4 -1
- package/src/hook.mjs +28 -6
- package/src/scan-session.mjs +2 -1
- package/src/scan-target.mjs +19 -2
package/src/hook.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { GIT_TIMEOUT_MS } from './git-environment.mjs';
|
|
6
7
|
import { newEngineWithDiagnostics } from './scan.mjs';
|
|
7
8
|
import { openRepo, stagedEntries } from './gitx.mjs';
|
|
8
9
|
import { applyExclude, patternsForRules } from './exclude.mjs';
|
|
@@ -330,7 +331,11 @@ function hookPreToolUse(input) {
|
|
|
330
331
|
}
|
|
331
332
|
}
|
|
332
333
|
let eng;
|
|
334
|
+
// diagnosticWarning is about the rule pack and drives the messages below;
|
|
335
|
+
// hygieneWarning is housekeeping that never touches the decision. Kept apart
|
|
336
|
+
// so the "invalid local pack skipped" wording stays true.
|
|
333
337
|
let diagnosticWarning = '';
|
|
338
|
+
let hygieneWarning = '';
|
|
334
339
|
try {
|
|
335
340
|
const loaded = repo
|
|
336
341
|
? engineForPolicy(repo, policy, policy.head)
|
|
@@ -343,7 +348,6 @@ function hookPreToolUse(input) {
|
|
|
343
348
|
return emitDecision('deny', `aimhooman strict policy could not load local rules: ${diagnosticWarning}`);
|
|
344
349
|
}
|
|
345
350
|
}
|
|
346
|
-
if (repo) applyExclude(repo.excludeFile, patternsForRules(eng.rules));
|
|
347
351
|
} catch (e) {
|
|
348
352
|
const reason = e?.name === 'LocalOverridesError'
|
|
349
353
|
? `aimhooman cannot load local overrides: ${e.message}`
|
|
@@ -352,6 +356,17 @@ function hookPreToolUse(input) {
|
|
|
352
356
|
if (profile === 'strict') return emitDecision('deny', reason);
|
|
353
357
|
return emitDecision('allow', `${reason}; continuing because profile=${profile}`);
|
|
354
358
|
}
|
|
359
|
+
// Refreshing the excludes is gitignore hygiene, not part of the verdict:
|
|
360
|
+
// pre-commit never writes them and still answers. Kept out of the block
|
|
361
|
+
// above so a read-only .git/info (CI checkout, read-only volume, a
|
|
362
|
+
// repository owned by another user) cannot decide what is allowed.
|
|
363
|
+
if (repo) {
|
|
364
|
+
try {
|
|
365
|
+
applyExclude(repo.excludeFile, patternsForRules(eng.rules));
|
|
366
|
+
} catch (e) {
|
|
367
|
+
hygieneWarning = `could not refresh ${repo.excludeFile}: ${e.message}`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
355
370
|
// A strict policy cannot make a meaningful guarantee if Git's own guards
|
|
356
371
|
// are explicitly bypassed. Deny before the shell can stage-and-commit in a
|
|
357
372
|
// single command, when inspecting the future index would be impossible.
|
|
@@ -468,6 +483,9 @@ function hookPreToolUse(input) {
|
|
|
468
483
|
if (diagnosticWarning) {
|
|
469
484
|
return emitDecision('allow', `aimhooman warning: ${diagnosticWarning}; invalid local pack skipped`);
|
|
470
485
|
}
|
|
486
|
+
if (hygieneWarning) {
|
|
487
|
+
return emitDecision('allow', `aimhooman warning: ${hygieneWarning}`);
|
|
488
|
+
}
|
|
471
489
|
return 0;
|
|
472
490
|
}
|
|
473
491
|
|
|
@@ -495,10 +513,8 @@ function hookPreToolUse(input) {
|
|
|
495
513
|
}
|
|
496
514
|
const guarded = repo && !bypassed && installedHooks(repo).includes('pre-commit');
|
|
497
515
|
const advisory = advisoryReason(blocks, guarded, bypassed ? bypassContext : '');
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
diagnosticWarning ? `${advisory} Warning: ${diagnosticWarning}.` : advisory
|
|
501
|
-
);
|
|
516
|
+
const notes = [diagnosticWarning, hygieneWarning].filter(Boolean).join('; ');
|
|
517
|
+
return emitDecision('allow', notes ? `${advisory} Warning: ${notes}.` : advisory);
|
|
502
518
|
}
|
|
503
519
|
|
|
504
520
|
function toolName(i) {
|
|
@@ -1213,7 +1229,12 @@ function configuredPushReceiver(repo) {
|
|
|
1213
1229
|
return execFileSync(
|
|
1214
1230
|
'git',
|
|
1215
1231
|
['config', '--get-regexp', '^remote\\..*\\.receivepack$'],
|
|
1216
|
-
{
|
|
1232
|
+
{
|
|
1233
|
+
cwd: repo.root,
|
|
1234
|
+
encoding: 'utf8',
|
|
1235
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1236
|
+
timeout: GIT_TIMEOUT_MS,
|
|
1237
|
+
},
|
|
1217
1238
|
).trim().length > 0;
|
|
1218
1239
|
} catch (error) {
|
|
1219
1240
|
if (error?.status === 1) return false;
|
|
@@ -1448,6 +1469,7 @@ function readGitAlias(cwd, name) {
|
|
|
1448
1469
|
cwd,
|
|
1449
1470
|
encoding: 'utf8',
|
|
1450
1471
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1472
|
+
timeout: GIT_TIMEOUT_MS,
|
|
1451
1473
|
}).trim();
|
|
1452
1474
|
} catch {
|
|
1453
1475
|
return null;
|
package/src/scan-session.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { gitEnvironment } from './git-environment.mjs';
|
|
2
|
+
import { gitEnvironment, GIT_TIMEOUT_MS } from './git-environment.mjs';
|
|
3
3
|
|
|
4
4
|
export const DEFAULT_SCAN_LIMITS = Object.freeze({
|
|
5
5
|
maxFileBytes: 2 << 20,
|
|
@@ -112,6 +112,7 @@ function readObjects(repo, objectIds, expectedBytes = 0) {
|
|
|
112
112
|
input: Buffer.from(unique.join('\n') + '\n'),
|
|
113
113
|
encoding: 'buffer',
|
|
114
114
|
maxBuffer: Math.max(2 * 1024 * 1024, expectedBytes + unique.length * 256 + 1024),
|
|
115
|
+
timeout: GIT_TIMEOUT_MS,
|
|
115
116
|
});
|
|
116
117
|
const objects = new Map();
|
|
117
118
|
const failures = [];
|
package/src/scan-target.mjs
CHANGED
|
@@ -49,6 +49,7 @@ export function scanMessage(repo, text, options = {}) {
|
|
|
49
49
|
const { policy, head } = resolved;
|
|
50
50
|
const loaded = engineForPolicy(repo, policy, options.overrideHead || head);
|
|
51
51
|
const accumulator = createAccumulator(options.limits);
|
|
52
|
+
accumulator.addSkipped(loaded.skipped);
|
|
52
53
|
accumulator.add(loaded.engine.checkMessage(text).map((finding) => decorate(finding, null, policy)));
|
|
53
54
|
accumulator.addSkipped(loaded.engine.takeSkipped());
|
|
54
55
|
return {
|
|
@@ -68,6 +69,7 @@ function scanIndex(repo, options) {
|
|
|
68
69
|
const { rawPolicy, floor, policy, head, acknowledged } = resolveStagedPolicy(repo, options.explicitProfile);
|
|
69
70
|
const loaded = engineForPolicy(repo, policy, options.overrideHead || head);
|
|
70
71
|
const accumulator = createAccumulator(options.limits);
|
|
72
|
+
accumulator.addSkipped(loaded.skipped);
|
|
71
73
|
let entries = kind === 'tracked' ? trackedEntries(repo) : stagedEntries(repo);
|
|
72
74
|
const conflicts = unmergedPaths(repo);
|
|
73
75
|
const diagnostics = [...loaded.diagnostics];
|
|
@@ -189,6 +191,7 @@ function scanCommit(repo, options) {
|
|
|
189
191
|
reviewContexts,
|
|
190
192
|
);
|
|
191
193
|
const accumulator = createAccumulator(options.limits);
|
|
194
|
+
accumulator.addSkipped(loaded.skipped);
|
|
192
195
|
const diagnostics = [...loaded.diagnostics];
|
|
193
196
|
|
|
194
197
|
if (snapshot.shallowBoundary) {
|
|
@@ -261,6 +264,9 @@ function scanRange(repo, options) {
|
|
|
261
264
|
// only constructs an Engine and applies overrides (see engineForPolicy).
|
|
262
265
|
const preloaded = loadRulesWithDiagnostics(repo.stateDir);
|
|
263
266
|
const preloadedOverrides = loadOverrides(repo.stateDir);
|
|
267
|
+
// Counted once for the range, not once per commit: the same packs back every
|
|
268
|
+
// commit's engine, so the per-commit loaded.skipped would inflate the tally.
|
|
269
|
+
accumulator.addSkipped(packSkipped(preloaded.errors));
|
|
264
270
|
|
|
265
271
|
for (const commit of history.commits) {
|
|
266
272
|
const changes = commitChanges(repo, commit.commit, commit.commit, commit.parents);
|
|
@@ -452,7 +458,14 @@ export function engineForPolicy(
|
|
|
452
458
|
[...reviewedInstructions, ...reviewedPolicies],
|
|
453
459
|
);
|
|
454
460
|
const diagnostics = loaded.errors.map((error) => ({ level: 'warning', message: `${error.message}; pack skipped` }));
|
|
455
|
-
return { engine: loaded.engine, diagnostics };
|
|
461
|
+
return { engine: loaded.engine, diagnostics, skipped: packSkipped(loaded.errors) };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// packSkipped turns rule-pack load failures into a counted skip reason. A pack
|
|
465
|
+
// that never loaded is a hole in coverage, not an empty result: strict throws
|
|
466
|
+
// above, and clean/compliance proceed but must not report a complete scan.
|
|
467
|
+
function packSkipped(errors) {
|
|
468
|
+
return errors.length ? { 'local-pack-error': errors.length } : {};
|
|
456
469
|
}
|
|
457
470
|
|
|
458
471
|
function reviewedEngineEntry(entry, ruleId, transition) {
|
|
@@ -589,6 +602,10 @@ function rangeReportPolicy(basePolicy, policies, usedStrictFloor, explicitProfil
|
|
|
589
602
|
};
|
|
590
603
|
}
|
|
591
604
|
|
|
605
|
+
// Skip reasons that mean a rule never ran, as opposed to a rule running and
|
|
606
|
+
// matching nothing. Either way the scan cannot claim to have covered its input.
|
|
607
|
+
const INCOMPLETE_SKIP_REASONS = new Set(['local-input-limit', 'local-pack-error']);
|
|
608
|
+
|
|
592
609
|
function createAccumulator(limits = {}) {
|
|
593
610
|
const effectiveLimits = { ...DEFAULT_SCAN_LIMITS, ...limits };
|
|
594
611
|
const findings = [];
|
|
@@ -644,7 +661,7 @@ function createAccumulator(limits = {}) {
|
|
|
644
661
|
addSkipped(skipped = {}) {
|
|
645
662
|
for (const [reason, count] of Object.entries(skipped)) {
|
|
646
663
|
stats.skipped[reason] = (stats.skipped[reason] || 0) + count;
|
|
647
|
-
if (reason
|
|
664
|
+
if (INCOMPLETE_SKIP_REASONS.has(reason)) complete = false;
|
|
648
665
|
}
|
|
649
666
|
},
|
|
650
667
|
markIncomplete() {
|