@rmyndharis/aimhooman 0.1.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.
Files changed (54) hide show
  1. package/.agents/rules/aimhooman.md +53 -0
  2. package/.claude-plugin/marketplace.json +23 -0
  3. package/.claude-plugin/plugin.json +9 -0
  4. package/.clinerules/aimhooman.md +53 -0
  5. package/.codex-plugin/plugin.json +37 -0
  6. package/.cursor/rules/aimhooman.mdc +59 -0
  7. package/.gemini/settings.json +7 -0
  8. package/.github/copilot-instructions.md +53 -0
  9. package/.github/hooks/aimhooman.json +22 -0
  10. package/.kiro/steering/aimhooman.md +53 -0
  11. package/.windsurf/rules/aimhooman.md +57 -0
  12. package/AGENTS.md +53 -0
  13. package/CHANGELOG.md +153 -0
  14. package/CODE_OF_CONDUCT.md +128 -0
  15. package/CONTRIBUTING.md +144 -0
  16. package/GEMINI.md +53 -0
  17. package/LICENSE +21 -0
  18. package/README.md +472 -0
  19. package/SECURITY.md +74 -0
  20. package/bin/aimhooman.mjs +1589 -0
  21. package/docs/design/agent-portability.md +73 -0
  22. package/docs/design/frictionless-enforcement.md +135 -0
  23. package/docs/hosts.json +164 -0
  24. package/docs/logo/aimhooman-logo.png +0 -0
  25. package/docs/logo/aimhooman.png +0 -0
  26. package/hooks/hooks.json +29 -0
  27. package/package.json +77 -0
  28. package/rules/attribution.json +142 -0
  29. package/rules/markers.json +88 -0
  30. package/rules/paths.json +407 -0
  31. package/rules/secrets.json +90 -0
  32. package/schemas/overrides.schema.json +134 -0
  33. package/schemas/project-policy.schema.json +12 -0
  34. package/schemas/rule-pack.schema.json +126 -0
  35. package/schemas/scan-report.schema.json +165 -0
  36. package/skills/aimhooman/SKILL.md +65 -0
  37. package/src/args.mjs +72 -0
  38. package/src/atomic-write.mjs +285 -0
  39. package/src/codex-manifest.mjs +65 -0
  40. package/src/exclude.mjs +125 -0
  41. package/src/git-environment.mjs +5 -0
  42. package/src/git-path.mjs +5 -0
  43. package/src/githooks.mjs +813 -0
  44. package/src/gitx.mjs +721 -0
  45. package/src/history-scan.mjs +295 -0
  46. package/src/hook.mjs +2602 -0
  47. package/src/policy-resolver.mjs +200 -0
  48. package/src/report.mjs +63 -0
  49. package/src/rules.mjs +509 -0
  50. package/src/ruleset-text.mjs +29 -0
  51. package/src/scan-session.mjs +152 -0
  52. package/src/scan-target.mjs +697 -0
  53. package/src/scan.mjs +325 -0
  54. package/src/state.mjs +360 -0
@@ -0,0 +1,295 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { GitRevisionError } from './gitx.mjs';
5
+ import { gitEnvironment } from './git-environment.mjs';
6
+
7
+ export const EMPTY_HISTORY_OID = '0'.repeat(40);
8
+
9
+ function gitBuffer(repo, args, input) {
10
+ return execFileSync('git', args, {
11
+ cwd: repo.root,
12
+ env: gitEnvironment(),
13
+ encoding: 'buffer',
14
+ maxBuffer: 128 * 1024 * 1024,
15
+ ...(input === undefined ? {} : { input }),
16
+ });
17
+ }
18
+
19
+ function gitString(repo, args) {
20
+ return gitBuffer(repo, args).toString('utf8').trim();
21
+ }
22
+
23
+ function gitStringQuiet(repo, args) {
24
+ return execFileSync('git', args, {
25
+ cwd: repo.root,
26
+ env: gitEnvironment(),
27
+ encoding: 'utf8',
28
+ maxBuffer: 128 * 1024 * 1024,
29
+ stdio: ['ignore', 'pipe', 'pipe'],
30
+ }).trim();
31
+ }
32
+
33
+ function assertRevision(value, label) {
34
+ if (typeof value !== 'string' || !value || value.startsWith('-') || /[\u0000-\u001f\u007f]/.test(value)) {
35
+ throw new TypeError(`${label} must be a non-empty Git revision`);
36
+ }
37
+ }
38
+
39
+ export function parseHistoryRange(value) {
40
+ assertRevision(value, 'range');
41
+ const match = /^(.+?)(\.\.\.?)(.+)$/.exec(value);
42
+ if (!match || match[1].includes('..') || match[3].includes('..')) {
43
+ throw new TypeError('range must contain both endpoints exactly once, for example base..head');
44
+ }
45
+ const [, base, operator, head] = match;
46
+ assertRevision(base, 'range base');
47
+ assertRevision(head, 'range head');
48
+ return { base, head, operator };
49
+ }
50
+
51
+ export function resolveCommit(repo, revision) {
52
+ assertRevision(revision, 'commit');
53
+ let commit;
54
+ try {
55
+ commit = gitStringQuiet(repo, [
56
+ 'rev-parse', '--verify', '--quiet', '--end-of-options', `${revision}^{commit}`,
57
+ ]);
58
+ } catch (error) {
59
+ if (error?.status === 1) {
60
+ throw new GitRevisionError(revision, 'does not resolve to a commit', error);
61
+ }
62
+ throw error;
63
+ }
64
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(commit)) {
65
+ throw new GitRevisionError(revision, 'Git returned an invalid commit object ID');
66
+ }
67
+ return commit;
68
+ }
69
+
70
+ export function commitMessage(repo, revision, resolvedCommit) {
71
+ const commit = resolvedCommit || resolveCommit(repo, revision);
72
+ const object = gitBuffer(repo, ['cat-file', 'commit', commit]);
73
+ const separator = object.indexOf(Buffer.from('\n\n'));
74
+ return {
75
+ commit,
76
+ message: separator < 0 ? '' : object.subarray(separator + 2).toString('utf8'),
77
+ };
78
+ }
79
+
80
+ export function commitParents(repo, revision) {
81
+ const commit = resolveCommit(repo, revision);
82
+ const line = gitString(repo, ['rev-list', '--parents', '-n', '1', commit]);
83
+ const [, ...parents] = line.split(' ');
84
+ return { commit, parents, shallowBoundary: isShallowBoundary(repo, commit) };
85
+ }
86
+
87
+ export function commitChanges(repo, revision, resolvedCommit, resolvedParents) {
88
+ // `resolvedParents || []` conflated "omitted" with "root commit": a merge
89
+ // passed via the 3-arg form would diff against nothing and silently scan as
90
+ // empty. Re-derive parents when the caller passed a commit but no parents.
91
+ const { commit, parents } = resolvedCommit
92
+ ? { commit: resolvedCommit, parents: resolvedParents ?? commitParents(repo, revision).parents }
93
+ : commitParents(repo, revision);
94
+ const comparisons = parents.length ? parents : [null];
95
+ const grouped = new Map();
96
+
97
+ for (const parent of comparisons) {
98
+ const args = [
99
+ 'diff-tree', '--no-commit-id', '--raw', '--no-abbrev', '-r', '-z',
100
+ '--find-renames', '--diff-filter=ACMRTD',
101
+ ];
102
+ if (!parent) args.push('--root', commit);
103
+ else args.push(parent, commit);
104
+ args.push('--');
105
+ for (const entry of parseRawDiff(gitBuffer(repo, args))) {
106
+ const key = [entry.path, entry.sourcePath || '', entry.oid || '', entry.mode || '', entry.status].join('\0');
107
+ const current = grouped.get(key);
108
+ if (current) {
109
+ if (parent && !current.parents.includes(parent)) current.parents.push(parent);
110
+ } else {
111
+ grouped.set(key, {
112
+ ...entry,
113
+ commit,
114
+ parents: parent ? [parent] : [],
115
+ });
116
+ }
117
+ }
118
+ }
119
+
120
+ const entries = [...grouped.values()].sort(compareEntries);
121
+ const sizes = objectSizes(repo, entries.filter((entry) => entry.type === 'blob').map((entry) => entry.oid));
122
+ for (const entry of entries) entry.size = entry.type === 'blob' ? sizes.get(entry.oid) ?? null : null;
123
+ return {
124
+ commit,
125
+ parents,
126
+ entries,
127
+ };
128
+ }
129
+
130
+ export function commitSnapshot(repo, revision) {
131
+ const { commit, parents, shallowBoundary } = commitParents(repo, revision);
132
+ const records = gitBuffer(repo, ['ls-tree', '-r', '-z', '--full-tree', commit, '--'])
133
+ .toString('utf8').split('\0').filter(Boolean);
134
+ const entries = records.map((record) => {
135
+ const tab = record.indexOf('\t');
136
+ const match = /^(\d+) (\w+) ([0-9a-f]+)$/.exec(record.slice(0, tab));
137
+ if (tab < 0 || !match) throw new Error('unexpected output from git ls-tree');
138
+ return {
139
+ mode: match[1],
140
+ type: match[2],
141
+ oid: match[3],
142
+ path: record.slice(tab + 1),
143
+ status: 'S',
144
+ commit,
145
+ parents,
146
+ };
147
+ });
148
+ const sizes = objectSizes(repo, entries.filter((entry) => entry.type === 'blob').map((entry) => entry.oid));
149
+ for (const entry of entries) entry.size = entry.type === 'blob' ? sizes.get(entry.oid) ?? null : null;
150
+ const { message } = commitMessage(repo, commit, commit);
151
+ const changes = commitChanges(repo, commit, commit, parents).entries;
152
+ return { commit, parents, shallowBoundary, message, entries, changes };
153
+ }
154
+
155
+ export function historyRange(repo, value) {
156
+ const parsed = parseHistoryRange(value);
157
+ const bootstrap = isZeroObjectId(parsed.base);
158
+ const baseCommit = bootstrap ? parsed.base : resolveCommit(repo, parsed.base);
159
+ const headCommit = resolveCommit(repo, parsed.head);
160
+ let scanBase = baseCommit;
161
+ if (!bootstrap && parsed.operator === '...') {
162
+ try {
163
+ scanBase = gitStringQuiet(repo, ['merge-base', baseCommit, headCommit]);
164
+ } catch (error) {
165
+ if (error?.status === 1) {
166
+ throw new GitRevisionError(value, 'range endpoints do not share a merge base', error);
167
+ }
168
+ throw error;
169
+ }
170
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(scanBase)) {
171
+ throw new GitRevisionError(value, 'Git returned an invalid merge base');
172
+ }
173
+ }
174
+ const revision = bootstrap ? headCommit : `${scanBase}..${headCommit}`;
175
+ const lines = gitString(repo, ['rev-list', '--reverse', '--topo-order', '--parents', revision]);
176
+ // rev-list --parents emits "<commit> <parent1> <parent2> ..." per line, so
177
+ // each commit's parents arrive with the walk at no extra cost. Commit bodies
178
+ // (changes, message) are fetched lazily by the scanner per commit rather
179
+ // than held in memory for the whole range up front.
180
+ const commits = lines ? lines.split('\n').map((line) => {
181
+ const [commit, ...parents] = line.split(' ');
182
+ return { commit, parents };
183
+ }) : [];
184
+ // A shallow clone (e.g. CI fetch-depth: 1) may not contain every commit in
185
+ // the range, so completeness cannot be proven. Callers warn or fail closed.
186
+ const shallow = gitStringQuiet(repo, ['rev-parse', '--is-shallow-repository']) === 'true';
187
+ const reversed = !bootstrap
188
+ && baseCommit !== headCommit
189
+ && commits.length === 0
190
+ && isAncestor(repo, headCommit, scanBase);
191
+ return {
192
+ input: value,
193
+ operator: parsed.operator,
194
+ base: baseCommit,
195
+ scanBase,
196
+ head: headCommit,
197
+ bootstrap,
198
+ shallow,
199
+ reversed,
200
+ commits,
201
+ };
202
+ }
203
+
204
+ function isShallowBoundary(repo, commit) {
205
+ if (gitStringQuiet(repo, ['rev-parse', '--is-shallow-repository']) !== 'true') return false;
206
+ let contents;
207
+ try {
208
+ contents = readFileSync(join(repo.commonDir, 'shallow'), 'utf8');
209
+ } catch (error) {
210
+ if (error?.code === 'ENOENT') {
211
+ throw new Error('Git reports a shallow repository but its shallow boundary file is missing', {
212
+ cause: error,
213
+ });
214
+ }
215
+ throw error;
216
+ }
217
+ return contents.split(/\r?\n/).includes(commit);
218
+ }
219
+
220
+ function isAncestor(repo, ancestor, descendant) {
221
+ try {
222
+ execFileSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
223
+ cwd: repo.root,
224
+ env: gitEnvironment(),
225
+ stdio: ['ignore', 'ignore', 'pipe'],
226
+ });
227
+ return true;
228
+ } catch (error) {
229
+ if (error?.status === 1) return false;
230
+ throw error;
231
+ }
232
+ }
233
+
234
+ function isZeroObjectId(value) {
235
+ return /^(?:0{40}|0{64})$/.test(value);
236
+ }
237
+
238
+ function parseRawDiff(buffer) {
239
+ const fields = buffer.toString('utf8').split('\0').filter(Boolean);
240
+ const entries = [];
241
+ for (let index = 0; index < fields.length;) {
242
+ const header = fields[index++];
243
+ const match = /^:(\d+) (\d+) ([0-9a-f]+) ([0-9a-f]+) ([ACMRTD])(\d*)$/.exec(header);
244
+ if (!match || index >= fields.length) throw new Error('unexpected output from git diff-tree');
245
+ const sourcePath = fields[index++];
246
+ let path = sourcePath;
247
+ if (match[5] === 'R' || match[5] === 'C') {
248
+ if (index >= fields.length) throw new Error('unexpected rename output from git diff-tree');
249
+ path = fields[index++];
250
+ }
251
+ const deleted = match[5] === 'D';
252
+ entries.push({
253
+ path,
254
+ ...(path === sourcePath ? {} : { sourcePath }),
255
+ oid: deleted ? null : match[4],
256
+ mode: deleted ? null : match[2],
257
+ type: deleted ? 'deleted' : typeForMode(match[2]),
258
+ status: match[5],
259
+ size: null,
260
+ });
261
+ }
262
+ return entries;
263
+ }
264
+
265
+ function objectSizes(repo, objectIds) {
266
+ const unique = [...new Set(objectIds)];
267
+ if (!unique.length) return new Map();
268
+ const lines = gitBuffer(
269
+ repo,
270
+ ['cat-file', '--batch-check=%(objectname) %(objecttype) %(objectsize)'],
271
+ Buffer.from(unique.join('\n') + '\n'),
272
+ ).toString('utf8').trimEnd().split('\n');
273
+ const sizes = new Map();
274
+ for (let index = 0; index < unique.length; index++) {
275
+ const fields = (lines[index] || '').split(' ');
276
+ if (fields[1] === 'blob' && /^\d+$/.test(fields[2])) sizes.set(unique[index], Number(fields[2]));
277
+ }
278
+ return sizes;
279
+ }
280
+
281
+ function typeForMode(mode) {
282
+ if (mode === '160000') return 'commit';
283
+ if (mode === '040000') return 'tree';
284
+ return 'blob';
285
+ }
286
+
287
+ function compareEntries(left, right) {
288
+ return compareText(left.path, right.path)
289
+ || compareText(String(left.oid), String(right.oid))
290
+ || compareText(left.status, right.status);
291
+ }
292
+
293
+ function compareText(left, right) {
294
+ return left < right ? -1 : left > right ? 1 : 0;
295
+ }