@rmyndharis/aimhooman 0.1.4 → 0.1.5
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 +45 -0
- package/README.md +1 -1
- package/bin/aimhooman.mjs +29 -5
- package/package.json +1 -1
- package/rules/secrets.json +1 -1
- package/src/atomic-write.mjs +18 -2
- package/src/githooks.mjs +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.5] - 2026-07-18
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- AWS's own published example key no longer blocks a commit. 0.1.4 began matching bare
|
|
13
|
+
`AKIA…`/`ASIA…` access key IDs, which also matched `AKIAIOSFODNN7EXAMPLE` — the value AWS
|
|
14
|
+
prints in its own documentation, and one that lands in READMEs, Terraform samples, and test
|
|
15
|
+
fixtures. Because the rule's category is `secret`, neither a rule allow nor a plain path
|
|
16
|
+
allow is accepted, so a documentation example became an unconditional block on every profile
|
|
17
|
+
with only a per-file escape. Every AWS example key ends in `EXAMPLE`, so the pattern now
|
|
18
|
+
excludes that suffix and keeps blocking real keys. Path scoping was deliberately not used:
|
|
19
|
+
excluding `docs/` or `tests/` from secret scanning would hide genuine keys in the places they
|
|
20
|
+
most often leak.
|
|
21
|
+
- `uninstall --purge-state` no longer leaves a backup behind in a linked worktree. Git writes
|
|
22
|
+
the commit message file into the per-worktree Git directory, so 0.1.4's sweep — which looked
|
|
23
|
+
only in the common directory — printed "state purged" with `COMMIT_EDITMSG.aimhooman-bak`
|
|
24
|
+
still on disk. Uninstall disarms every worktree at once, so it now sweeps the main Git
|
|
25
|
+
directory and each linked one, and matches the `.aimhooman-bak` suffix so a backup left by a
|
|
26
|
+
merge goes with it.
|
|
27
|
+
- A plain `uninstall` keeps the attribution backup. It printed "state kept" and then deleted
|
|
28
|
+
the one file holding the lines stripped from the last commit message. Only `--purge-state`,
|
|
29
|
+
which promises to remove everything, takes it now.
|
|
30
|
+
- Sweeping an empty lock queue can no longer stop a concurrent `aimhooman init`. Between its
|
|
31
|
+
`mkdir` and its first publication a lock contender owns no file in the queue, so the sweep
|
|
32
|
+
saw an empty directory and removed it, and the contender then failed on a bare `ENOENT`. The
|
|
33
|
+
window is wide enough to lose because building the candidate probes the process identity,
|
|
34
|
+
which spawns `ps` on macOS and BSD. Publication now recreates the directory and retries once.
|
|
35
|
+
- The unstage summary no longer claims the files were "kept in your working tree". That is
|
|
36
|
+
false when a path was staged and then deleted before the commit, and when a staged deletion
|
|
37
|
+
is unstaged — the file is absent either way, though aimhooman never removed it. Unstaging
|
|
38
|
+
only ever touches the index, so the note says that instead of guessing where the file is.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- An ordinary commit no longer pays a Node cold start for a phase that does nothing. Git fires
|
|
43
|
+
`reference-transaction` twice per commit, and `refcheck` returns immediately for `committed`
|
|
44
|
+
and `aborted`, so the dispatcher now short-circuits those phases in the shell — after the
|
|
45
|
+
chained-hook call, so a chained hook still sees every phase. Measured locally, an ordinary
|
|
46
|
+
commit drops from about 870ms to about 710ms. The `prepared` scan, and the `--no-verify`
|
|
47
|
+
backstop that rides on it, are unchanged. A repository installed with an earlier release
|
|
48
|
+
keeps a valid dispatcher; re-run `aimhooman init` to pick up the faster one.
|
|
49
|
+
- The attribution backup no longer lingers indefinitely. `commit-msg` clears the previous run's
|
|
50
|
+
backup before it may write its own, so at most one exists at a time and a later clean commit
|
|
51
|
+
leaves none. The recovery window is now until your next commit rather than unbounded.
|
|
52
|
+
|
|
8
53
|
## [0.1.4] - 2026-07-18
|
|
9
54
|
|
|
10
55
|
### Fixed
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
11
|
<img src="https://img.shields.io/github/actions/workflow/status/rmyndharis/aimhooman/test.yml?branch=main&label=CI" alt="CI">
|
|
12
|
-
<img src="https://img.shields.io/badge/version-v0.1.
|
|
12
|
+
<img src="https://img.shields.io/badge/version-v0.1.5-blue" alt="v0.1.5">
|
|
13
13
|
<img src="https://img.shields.io/badge/node-%E2%89%A522.8-339933?logo=node.js&logoColor=white" alt="Node 22.8+">
|
|
14
14
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero dependencies">
|
|
15
15
|
<img src="https://img.shields.io/badge/license-MIT-111111" alt="MIT">
|
package/bin/aimhooman.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { chmodSync, lstatSync, readFileSync, rmdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, lstatSync, readdirSync, readFileSync, rmdirSync, rmSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { GIT_TIMEOUT_MS } from '../src/git-environment.mjs';
|
|
@@ -372,7 +372,7 @@ function cmdPrecommit(args) {
|
|
|
372
372
|
emptied = stagedBefore !== null
|
|
373
373
|
&& stagedBefore.every((path) => unstageTargets.has(path));
|
|
374
374
|
process.stderr.write(
|
|
375
|
-
`aimhooman: unstaged ${paths.length} file(s) from this commit: ${paths.map(visible).join(', ')}${emptied ? ' — nothing else was staged, so the commit is stopped rather than left empty' : ''}\n`
|
|
375
|
+
`aimhooman: unstaged ${paths.length} file(s) from this commit: ${paths.map(visible).join(', ')} (index only; nothing on disk was deleted)${emptied ? ' — nothing else was staged, so the commit is stopped rather than left empty' : ''}\n`
|
|
376
376
|
);
|
|
377
377
|
} catch (e) {
|
|
378
378
|
process.stderr.write(
|
|
@@ -409,6 +409,10 @@ function cmdCommitmsg(args) {
|
|
|
409
409
|
const file = positionals[0];
|
|
410
410
|
const repo = tryRepo();
|
|
411
411
|
if (!repo) { console.error('aimhooman: not a git repository'); return 30; }
|
|
412
|
+
// A .aimhooman-bak from an earlier commit went stale the moment that commit
|
|
413
|
+
// finished, and git reuses this message path. Clear the previous run's backup
|
|
414
|
+
// now, before this run may write its own, so at most one ever lingers.
|
|
415
|
+
try { rmSync(`${file}.aimhooman-bak`, { force: true }); } catch { /* best effort */ }
|
|
412
416
|
// Frictionless profiles (clean/compliance) never cancel a commit merely
|
|
413
417
|
// because the message file cannot be read; only strict fails closed.
|
|
414
418
|
let hookProfile = 'clean';
|
|
@@ -1696,9 +1700,29 @@ function cmdUninstall(args) {
|
|
|
1696
1700
|
]) {
|
|
1697
1701
|
try { rmdirSync(queue); } catch { /* held by another aimhooman, or already gone */ }
|
|
1698
1702
|
}
|
|
1699
|
-
//
|
|
1700
|
-
|
|
1701
|
-
|
|
1703
|
+
// The attribution backup is the user's only copy of the lines stripped from
|
|
1704
|
+
// their last message — commit-msg already clears the previous one, so what
|
|
1705
|
+
// survives here is current, not stale. A plain uninstall says "state kept"
|
|
1706
|
+
// and must not delete it; only --purge-state, which promises to remove
|
|
1707
|
+
// everything, sweeps it. Git names the message file per operation
|
|
1708
|
+
// (COMMIT_EDITMSG, MERGE_MSG, ...) and the backup inherits that name, so
|
|
1709
|
+
// match the suffix rather than one filename. It lands beside that file, in
|
|
1710
|
+
// the per-worktree git directory rather than the common one, and uninstall
|
|
1711
|
+
// disarms every worktree at once — so cover the main one and each linked.
|
|
1712
|
+
if (purge) {
|
|
1713
|
+
const messageDirs = [repo.commonDir];
|
|
1714
|
+
try {
|
|
1715
|
+
const linked = join(repo.commonDir, 'worktrees');
|
|
1716
|
+
for (const name of readdirSync(linked)) messageDirs.push(join(linked, name));
|
|
1717
|
+
} catch { /* no linked worktrees */ }
|
|
1718
|
+
for (const dir of messageDirs) {
|
|
1719
|
+
try {
|
|
1720
|
+
for (const name of readdirSync(dir)) {
|
|
1721
|
+
if (name.endsWith('.aimhooman-bak')) rmSync(join(dir, name), { force: true });
|
|
1722
|
+
}
|
|
1723
|
+
} catch { /* directory missing or unreadable */ }
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1702
1726
|
return exitStatus;
|
|
1703
1727
|
}
|
|
1704
1728
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmyndharis/aimhooman",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "AI works. Hoomans ship. Keep AI session files, secrets, and attribution out of your commits.",
|
|
5
5
|
"homepage": "https://github.com/rmyndharis/aimhooman#readme",
|
|
6
6
|
"repository": {
|
package/rules/secrets.json
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"match": {
|
|
54
54
|
"content": [
|
|
55
55
|
"(?i)\\baws_(?:secret_access_key|session_token)\\b\\s*[:=]\\s*[\\\"']?[A-Za-z0-9/+=]{32,}(?![A-Za-z0-9/+=])",
|
|
56
|
-
"\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b"
|
|
56
|
+
"\\b(?:AKIA|ASIA)(?![A-Z0-9]{9}EXAMPLE\\b)[A-Z0-9]{16}\\b"
|
|
57
57
|
]
|
|
58
58
|
},
|
|
59
59
|
"actions": {
|
package/src/atomic-write.mjs
CHANGED
|
@@ -259,18 +259,34 @@ export function withLock(lockPath, fn, options = {}) {
|
|
|
259
259
|
let held = false;
|
|
260
260
|
let published = false;
|
|
261
261
|
let primaryError = null;
|
|
262
|
+
// Between the mkdir above and the first publication below this contender owns
|
|
263
|
+
// no file in the queue, so a concurrent cleanup that removes empty queue
|
|
264
|
+
// directories — `aimhooman uninstall` sweeps them — can delete it out from
|
|
265
|
+
// under us. The gap is wide enough to lose: building the candidate probes the
|
|
266
|
+
// process identity, which spawns `ps` on macOS and BSD. Recreate and retry
|
|
267
|
+
// once rather than failing the caller with a bare ENOENT. Once a candidate is
|
|
268
|
+
// published the directory is no longer empty, so no cleanup can remove it.
|
|
269
|
+
const publish = () => {
|
|
270
|
+
try {
|
|
271
|
+
publishCandidate(candidatePath, candidate, options.candidateOperations);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
274
|
+
mkdirSync(queueDir, { recursive: true });
|
|
275
|
+
publishCandidate(candidatePath, candidate, options.candidateOperations);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
262
278
|
try {
|
|
263
279
|
// Own the UUID pathname before publication begins. If the rename lands
|
|
264
280
|
// but its directory fsync fails, finally still removes the candidate;
|
|
265
281
|
// unlinking ENOENT is harmless when publication failed earlier.
|
|
266
282
|
published = true;
|
|
267
|
-
|
|
283
|
+
publish();
|
|
268
284
|
const observed = queueCandidates(queueDir, staleMs);
|
|
269
285
|
candidate.ticket = observed.reduce((maximum, peer) => (
|
|
270
286
|
peer.ticket === null ? maximum : Math.max(maximum, peer.ticket)
|
|
271
287
|
), 0) + 1;
|
|
272
288
|
candidate.choosing = false;
|
|
273
|
-
|
|
289
|
+
publish();
|
|
274
290
|
|
|
275
291
|
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
276
292
|
// A file at the pre-queue lock path may belong to a concurrently
|
package/src/githooks.mjs
CHANGED
|
@@ -642,6 +642,14 @@ function hookScript(name, cmd, cliPath, chainedPath) {
|
|
|
642
642
|
const aimInvocation = name === 'reference-transaction'
|
|
643
643
|
? `printf '%s\\n' "$AIMHOOMAN_REF_UPDATES" | run_aimhooman ${aimCommand} || exit $?`
|
|
644
644
|
: `run_aimhooman ${aimCommand} || exit $?`;
|
|
645
|
+
// committed/aborted fire only after refs are locked in, and refcheck can do
|
|
646
|
+
// nothing but return 0 for them (see cmdRefcheck). Short-circuit in the shell
|
|
647
|
+
// so an ordinary commit no longer pays a Node cold start for the committed
|
|
648
|
+
// phase. Placed after the chained-hook call, so a chained hook still sees
|
|
649
|
+
// every phase.
|
|
650
|
+
const phaseShortCircuit = name === 'reference-transaction'
|
|
651
|
+
? 'case "$1" in committed|aborted) exit 0 ;; esac\n'
|
|
652
|
+
: '';
|
|
645
653
|
const template = `#!/bin/sh -p
|
|
646
654
|
${MARKER} (${name})
|
|
647
655
|
# aimhooman-hook-version: ${HOOK_FORMAT_VERSION}
|
|
@@ -691,7 +699,7 @@ fi
|
|
|
691
699
|
if [ -x "$CHAINED" ]; then
|
|
692
700
|
${chainedInvocation}
|
|
693
701
|
fi
|
|
694
|
-
${aimInvocation}
|
|
702
|
+
${phaseShortCircuit}${aimInvocation}
|
|
695
703
|
`;
|
|
696
704
|
const fingerprint = hookFingerprint(template);
|
|
697
705
|
return template.replace(FINGERPRINT_PLACEHOLDER, fingerprint);
|