@davesheffer/hunch 0.38.2 โ 0.38.3
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/README.md +1 -0
- package/dist/cli/index.js +58 -33
- package/dist/extractors/git.js +53 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# ๐ง Hunch โ Architectural Conformance for AI code
|
|
2
2
|
|
|
3
|
+
[](https://github.com/davesheffer/hunch)
|
|
3
4
|
[](https://www.npmjs.com/package/@davesheffer/hunch)
|
|
4
5
|
[](https://www.npmjs.com/package/@davesheffer/hunch)
|
|
5
6
|
[](LICENSE)
|
package/dist/cli/index.js
CHANGED
|
@@ -79,7 +79,8 @@ program
|
|
|
79
79
|
.option("--no-providers", "skip scaffolding non-Claude assistant configs (Cursor / VS Code / Codex / AGENTS.md)")
|
|
80
80
|
.option("--no-agent-hooks", "skip installing the Claude Code agent hooks (.claude/settings.json)")
|
|
81
81
|
.option("--firmness <level>", "agent-hook firmness: off | advisory | firm | strict")
|
|
82
|
-
.option("--private-sync", "post-commit synthesis writes captured decisions into the
|
|
82
|
+
.option("--private-sync", "post-commit synthesis writes captured decisions into the overlay repo (HUNCH_PRIVATE_DIR), never the public store")
|
|
83
|
+
.option("--shared-sync", "alias of --private-sync (for teams using one shared overlay repo for any code repo)")
|
|
83
84
|
.option("--auto-commit", "opt-in: the post-commit hook also git add+commit+pushes the captured decision (the repo it landed in)")
|
|
84
85
|
.action((opts) => {
|
|
85
86
|
// Validate --firmness up front, before any side effects (indexing, git hooks,
|
|
@@ -109,8 +110,9 @@ program
|
|
|
109
110
|
console.log(` โ ${res.skipped} file(s) could not be parsed (skipped)`);
|
|
110
111
|
}
|
|
111
112
|
if (isGitRepo(root)) {
|
|
112
|
-
const
|
|
113
|
-
|
|
113
|
+
const syncToOverlay = !!(opts.privateSync || opts.sharedSync);
|
|
114
|
+
const h = installPostCommitHook(root, inv.shell, { private: syncToOverlay, commit: opts.autoCommit });
|
|
115
|
+
console.log(` โ post-commit hook ${h.action} (learning loop)${syncToOverlay ? " โ syncs to the shared overlay" : ""}${opts.autoCommit ? " โ auto-commit+push on" : ""}`);
|
|
114
116
|
const m = installMergeDriver(root, inv.shell);
|
|
115
117
|
console.log(` โ team merge driver ${m.action}`);
|
|
116
118
|
// Auto-install the pre-commit guard by default (advisory: flags invariants
|
|
@@ -266,7 +268,8 @@ program
|
|
|
266
268
|
.option("--from-hook", "invoked by the git hook")
|
|
267
269
|
.option("--quiet", "minimal output")
|
|
268
270
|
.option("--force", "re-synthesize even if a decision already exists for the commit")
|
|
269
|
-
.option("--private", "write the synthesized decision into the
|
|
271
|
+
.option("--private", "write the synthesized decision into the configured overlay (HUNCH_PRIVATE_DIR), not the public store")
|
|
272
|
+
.option("--overlay", "alias of --private")
|
|
270
273
|
.option("--commit", "after a capture, also git add+commit+push the repo the decision landed in (opt-in; best-effort) โ the private store under --private, else this repo")
|
|
271
274
|
.option("--deep", "Deep Synthesis: ensemble every available subscription CLI and reconcile their drafts (agreement-weighted, advisory). Slower; subscription-only")
|
|
272
275
|
.option("--verify", "Critic pass: audit the draft against its commit, prune unsupported alternatives/consequences, down-weight weak grounding (extra subscription call; advisory)")
|
|
@@ -275,12 +278,13 @@ program
|
|
|
275
278
|
const { store, root } = storeFor();
|
|
276
279
|
if (!isGitRepo(root))
|
|
277
280
|
return opts.quiet ? undefined : fail("sync needs a git repo");
|
|
278
|
-
|
|
281
|
+
const toOverlay = !!(opts.private || opts.overlay);
|
|
282
|
+
if (toOverlay && !store.hasPrivate) {
|
|
279
283
|
store.close();
|
|
280
|
-
return opts.quiet ? undefined : fail("--private needs HUNCH_PRIVATE_DIR set to
|
|
284
|
+
return opts.quiet ? undefined : fail("--private/--overlay needs HUNCH_PRIVATE_DIR set to an overlay store");
|
|
281
285
|
}
|
|
282
286
|
store.json.ensureDirs();
|
|
283
|
-
const r = await syncCommit(store, root, sha ?? headSha(root), { force: opts.force, private:
|
|
287
|
+
const r = await syncCommit(store, root, sha ?? headSha(root), { force: opts.force, private: toOverlay, deep: opts.deep, verify: opts.verify, samples: parseSamples(opts.samples) });
|
|
284
288
|
if (r.status === "written") {
|
|
285
289
|
store.reindex();
|
|
286
290
|
// Don't rewrite grounding from the hook โ it would dirty the working tree on
|
|
@@ -296,7 +300,7 @@ program
|
|
|
296
300
|
// just no-ops. Stage ONLY the hunch dir (never sweep unrelated working-tree
|
|
297
301
|
// changes), and set HUNCH_SYNC=1 so the commit we create can't re-trigger this
|
|
298
302
|
// hook (no recursion, including on a manual `hunch sync --commit`).
|
|
299
|
-
const commitTarget = opts.commit ? (
|
|
303
|
+
const commitTarget = opts.commit ? (toOverlay ? store.privateDir : hunchPaths(root).hunch) : undefined;
|
|
300
304
|
if (commitTarget) {
|
|
301
305
|
commitAndPushHunch(commitTarget, `hunch: capture ${r.decision?.id ?? "decision"}`);
|
|
302
306
|
if (!opts.quiet)
|
|
@@ -310,29 +314,21 @@ program
|
|
|
310
314
|
}
|
|
311
315
|
store.close();
|
|
312
316
|
});
|
|
313
|
-
|
|
314
|
-
program
|
|
315
|
-
.command("private [dir]")
|
|
316
|
-
.description("Enable a PRIVATE memory overlay โ sensitive decisions/bugs/constraints kept in a separate location, unioned into local queries, never committed here. Writes a gitignored .hunch/local.json so it's auto-detected (no env var needed).")
|
|
317
|
-
.option("--repo <url>", "clone a private git repo to use as the store (into ./.hunch-private)")
|
|
318
|
-
.option("--no-hook", "don't switch the post-commit hook to private sync")
|
|
319
|
-
.option("--no-auto-commit", "DON'T auto commit+push the overlay after each capture (default: ON โ fully automated two-way sync, never push by hand)")
|
|
320
|
-
.option("--sync", "flush the configured private store now (git add+commit+push) โ catches records made via MCP between commits")
|
|
321
|
-
.option("--migrate", "ONE-TIME: move this repo's EXISTING public .hunch memory into the overlay, then make the public repo code-only โ untrack + gitignore the memory tree and regenerate grounding so no memory is published here")
|
|
322
|
-
.action((dir, opts) => {
|
|
317
|
+
function configureOverlay(dir, opts, mode) {
|
|
323
318
|
const root = findRoot();
|
|
319
|
+
const paths = hunchPaths(root);
|
|
320
|
+
const commandName = mode === "private" ? "private" : "shared";
|
|
324
321
|
if (opts.sync) {
|
|
325
322
|
const s = new HunchStore(hunchPaths(root));
|
|
326
323
|
const target = s.privateDir;
|
|
327
324
|
s.close();
|
|
328
325
|
if (!target)
|
|
329
|
-
return fail(
|
|
330
|
-
commitAndPushHunch(target, "hunch: sync
|
|
331
|
-
console.log(`โ flushed
|
|
326
|
+
return fail(`no overlay configured โ run \`hunch ${commandName}\` first`);
|
|
327
|
+
commitAndPushHunch(target, "hunch: sync overlay memory");
|
|
328
|
+
console.log(`โ flushed overlay store โ ${target}`);
|
|
332
329
|
return;
|
|
333
330
|
}
|
|
334
|
-
|
|
335
|
-
// 1) resolve the private store's hunch dir (holds decisions/, bugs/, โฆ)
|
|
331
|
+
// 1) resolve the overlay store's hunch dir (holds decisions/, bugs/, โฆ)
|
|
336
332
|
let hunchDir;
|
|
337
333
|
if (opts.repo) {
|
|
338
334
|
const dest = join(root, ".hunch-private");
|
|
@@ -353,6 +349,13 @@ program
|
|
|
353
349
|
// multiple machines/worktrees merge by RECORD ID (no manual conflict resolution) when the
|
|
354
350
|
// two-way auto-sync pulls before pushing. The overlay repo root is the parent of its .hunch.
|
|
355
351
|
const overlayRoot = hunchPathsForDir(hunchDir).root;
|
|
352
|
+
// CRITICAL (bug_overlay_clobber): with --auto-commit, the overlay MUST be its own git repo.
|
|
353
|
+
// Otherwise the post-commit auto-commit (commitAndPushHunch) runs `git -C overlayDir โฆ` which
|
|
354
|
+
// walks UP to the PROJECT repo and can commit memory over your code. Initialize a standalone
|
|
355
|
+
// repo when one isn't there (a local repo with no remote just accumulates commits โ safe).
|
|
356
|
+
if (opts.autoCommit && !isGitRepo(overlayRoot)) {
|
|
357
|
+
spawnSync("git", ["init", "-q", overlayRoot], { stdio: "ignore" });
|
|
358
|
+
}
|
|
356
359
|
if (isGitRepo(overlayRoot))
|
|
357
360
|
installMergeDriver(overlayRoot, inv.shell);
|
|
358
361
|
// 3) record the path in a GITIGNORED local config โ auto-detected, no env var, and
|
|
@@ -371,7 +374,7 @@ program
|
|
|
371
374
|
// only an absolute path survives the move. Lives under .git/ (never tracked; nothing to ignore).
|
|
372
375
|
let worktreeNote = "";
|
|
373
376
|
if (ensureSharedOverlayPointer(root, hunchDir, !!opts.autoCommit)) {
|
|
374
|
-
worktreeNote =
|
|
377
|
+
worktreeNote = " โ registered in the git common dir โ shared by every worktree of this repo, on any branch\n";
|
|
375
378
|
}
|
|
376
379
|
// 4) route post-commit synthesis to the overlay (local hook, never committed)
|
|
377
380
|
let hookNote = "";
|
|
@@ -405,18 +408,40 @@ program
|
|
|
405
408
|
` next: review, then commit the PUBLIC repo:\n` +
|
|
406
409
|
` git add -A && git commit -m "chore: move engineering memory to a private overlay" && git push\n`;
|
|
407
410
|
}
|
|
408
|
-
|
|
409
|
-
|
|
411
|
+
const lead = mode === "private"
|
|
412
|
+
? `โ private overlay enabled โ ${hunchDir}\n`
|
|
413
|
+
: `โ shared overlay enabled โ ${hunchDir}\n`;
|
|
414
|
+
const tail = mode === "private"
|
|
415
|
+
? " record sensitive items with private:true (hunch_record_decision / hunch_record_correction)\n override per-shell with HUNCH_PRIVATE_DIR; CI / public PR comments stay public-only."
|
|
416
|
+
: " this works for any repo (public or private): one shared memory source across teammates, branches, and worktrees.\n override per-shell with HUNCH_PRIVATE_DIR if needed.";
|
|
417
|
+
console.log(lead +
|
|
418
|
+
" โ recorded in .hunch/local.json (gitignored) โ auto-detected, no env var or shell-profile edit\n" +
|
|
410
419
|
worktreeNote +
|
|
411
420
|
hookNote +
|
|
412
421
|
migrateNote +
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
422
|
+
tail);
|
|
423
|
+
}
|
|
424
|
+
program
|
|
425
|
+
.command("private [dir]")
|
|
426
|
+
.description("Enable a PRIVATE memory overlay โ sensitive decisions/bugs/constraints kept in a separate location, unioned into local queries, never committed here. Writes a gitignored .hunch/local.json so it's auto-detected (no env var needed).")
|
|
427
|
+
.option("--repo <url>", "clone a private git repo to use as the store (into ./.hunch-private)")
|
|
428
|
+
.option("--no-hook", "don't switch the post-commit hook to private sync")
|
|
429
|
+
.option("--no-auto-commit", "DON'T auto commit+push the overlay after each capture (default: ON โ fully automated two-way sync, never push by hand)")
|
|
430
|
+
.option("--sync", "flush the configured private store now (git add+commit+push) โ catches records made via MCP between commits")
|
|
431
|
+
.option("--migrate", "ONE-TIME: move this repo's EXISTING public .hunch memory into the overlay, then make the public repo code-only โ untrack + gitignore the memory tree and regenerate grounding so no memory is published here")
|
|
432
|
+
.action((dir, opts) => configureOverlay(dir, opts, "private"));
|
|
433
|
+
program
|
|
434
|
+
.command("shared [dir]")
|
|
435
|
+
.description("Enable a SHARED memory overlay repo for this project (works for any repo: private or public). Memory stays in one location, shared across teammates, branches, and worktrees.")
|
|
436
|
+
.option("--repo <url>", "clone a git repo to use as the shared memory store (into ./.hunch-private)")
|
|
437
|
+
.option("--no-hook", "don't switch the post-commit hook to overlay sync")
|
|
438
|
+
.option("--no-auto-commit", "DON'T auto commit+push the overlay after each capture (default: ON โ fully automated two-way sync)")
|
|
439
|
+
.option("--sync", "flush the configured overlay store now (git add+commit+push)")
|
|
440
|
+
.action((dir, opts) => configureOverlay(dir, opts, "shared"));
|
|
416
441
|
// ---- worktree (one-command worktree wired into Hunch) ----------------------
|
|
417
442
|
program
|
|
418
443
|
.command("worktree <path>")
|
|
419
|
-
.description("Create a git worktree already wired into Hunch โ it shares this repo's memory
|
|
444
|
+
.description("Create a git worktree already wired into Hunch โ it shares this repo's memory overlay, with zero per-worktree setup.")
|
|
420
445
|
.option("-b, --branch <name>", "create the worktree on a NEW branch")
|
|
421
446
|
.option("--no-share", "don't register the overlay at the git common dir (the worktree won't see private memory)")
|
|
422
447
|
.option("--no-index", "don't build the new worktree's code graph (skip if you'll index later)")
|
|
@@ -448,7 +473,7 @@ program
|
|
|
448
473
|
shareNote = ` ยท could not register the shared overlay pointer (no git common dir?)`;
|
|
449
474
|
}
|
|
450
475
|
else {
|
|
451
|
-
shareNote = ` ยท no
|
|
476
|
+
shareNote = ` ยท no overlay configured โ run \`hunch shared\` (or \`hunch private\`) to share memory across worktrees`;
|
|
452
477
|
}
|
|
453
478
|
// 3) build the new worktree's CODE GRAPH (symbols/edges โ blast-radius / dependents).
|
|
454
479
|
// Indexed IN-PROCESS (uses THIS install's tree-sitter, so the worktree needs no
|
|
@@ -1753,7 +1778,7 @@ program
|
|
|
1753
1778
|
console.log(`hunch: ${c.symbols} symbols, ${c.edges} edges, ${c.components} components, ${c.decisions} decisions, ${c.bugs} bugs, ${c.constraints} constraints`);
|
|
1754
1779
|
console.log(store.privateDir
|
|
1755
1780
|
? `private: on โ ${store.privateDir} (local overlay โ unioned into queries; never committed or posted publicly)`
|
|
1756
|
-
: dim(`private: off โ run \`hunch private\` to
|
|
1781
|
+
: dim(`private: off โ run \`hunch shared\` (or \`hunch private\`) to use one overlay repo across teammates/worktrees (or set HUNCH_PRIVATE_DIR)`));
|
|
1757
1782
|
// Worktree posture: linked worktrees share ONE memory via the git common dir. Only
|
|
1758
1783
|
// surfaced in a linked worktree (no noise in a normal single checkout), so a
|
|
1759
1784
|
// "memory missing here" symptom has an obvious cause + fix.
|
|
@@ -1762,7 +1787,7 @@ program
|
|
|
1762
1787
|
const sharedPtr = !!common && existsSync(join(common, "hunch", "local.json"));
|
|
1763
1788
|
console.log(store.privateDir
|
|
1764
1789
|
? `worktree: linked โ sharing the repo's memory${sharedPtr ? " via the git common dir" : ""}`
|
|
1765
|
-
: dim(`worktree: linked, but no overlay resolved here โ run \`hunch private\` once (any worktree) so all worktrees share it`));
|
|
1790
|
+
: dim(`worktree: linked, but no overlay resolved here โ run \`hunch shared\` (or \`hunch private\`) once (any worktree) so all worktrees share it`));
|
|
1766
1791
|
}
|
|
1767
1792
|
// Semantic search is opt-in and local. Report availability + coverage without
|
|
1768
1793
|
// loading the model (selectEmbedder only probes; embeddingStats just counts rows).
|
package/dist/extractors/git.js
CHANGED
|
@@ -43,13 +43,30 @@ export function commitAndPushHunch(hunchDir, message) {
|
|
|
43
43
|
catch { /* best-effort: nothing staged / not a repo / offline */ }
|
|
44
44
|
};
|
|
45
45
|
run(["add", "--", "."]);
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
// SAFETY BACKSTOP (critical โ bug_overlay_clobber): a memory sync is PURELY ADDITIVE small
|
|
47
|
+
// JSON. If the staged set contains a DELETION, rename, or any non-.json file, hunchDir is NOT
|
|
48
|
+
// a clean overlay store โ most dangerously, the overlay was never its own git repo so `git -C`
|
|
49
|
+
// walked UP to the PROJECT repo. Committing/pushing there would overwrite/delete the user's
|
|
50
|
+
// code (we shipped exactly this). Refuse hard: unstage and bail without committing or pushing.
|
|
51
|
+
if (!stagedIsMemoryOnly(hunchDir, env)) {
|
|
52
|
+
try {
|
|
53
|
+
execFileSync("git", ["-C", hunchDir, "reset", "-q", "--", "."], { stdio: "ignore", env });
|
|
54
|
+
}
|
|
55
|
+
catch { /* best-effort unstage */ }
|
|
56
|
+
console.error(`hunch: refusing to auto-commit memory at "${hunchDir}" โ the staged change includes deletions or non-memory files, so this is not a clean overlay repo. Nothing was committed or pushed. (Use \`hunch shared --repo <url>\` so the overlay is its OWN git repo.)`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Only sync+push when a memory commit was actually created โ never run pull/push against the
|
|
60
|
+
// enclosing repo on an empty stage. Two-way sync: MERGE the remote BEFORE pushing so a push
|
|
61
|
+
// can't be rejected non-fast-forward; the .hunch merge driver resolves same-record conflicts
|
|
62
|
+
// by id. On conflict/offline, mergeRemote aborts to a clean tree and we skip the push.
|
|
63
|
+
let committed = false;
|
|
64
|
+
try {
|
|
65
|
+
execFileSync("git", ["-C", hunchDir, "commit", "-m", message], { stdio: "ignore", env });
|
|
66
|
+
committed = true;
|
|
67
|
+
}
|
|
68
|
+
catch { /* nothing staged / not a repo */ }
|
|
69
|
+
if (committed && mergeRemote(hunchDir, env))
|
|
53
70
|
run(["push"]);
|
|
54
71
|
}
|
|
55
72
|
finally {
|
|
@@ -59,6 +76,35 @@ export function commitAndPushHunch(hunchDir, message) {
|
|
|
59
76
|
catch { /* released best-effort */ }
|
|
60
77
|
}
|
|
61
78
|
}
|
|
79
|
+
/** Is the staged set a clean, MEMORY-ONLY change โ only JSON record adds/updates, nothing else?
|
|
80
|
+
* The overlay store is entirely JSON (decisions/, bugs/, โฆ, manifest.json). A real memory sync
|
|
81
|
+
* is purely additive; a DELETION, rename, or any non-.json staged path means hunchDir is NOT a
|
|
82
|
+
* clean overlay repo (e.g. it resolved to the project repo), so committing there would clobber
|
|
83
|
+
* code. Empty stage โ false (nothing to commit). The transient mkdir lock is ignored. */
|
|
84
|
+
function stagedIsMemoryOnly(hunchDir, env) {
|
|
85
|
+
let out = "";
|
|
86
|
+
try {
|
|
87
|
+
out = execFileSync("git", ["-C", hunchDir, "diff", "--cached", "--name-status"], { encoding: "utf8", env });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
93
|
+
if (!lines.length)
|
|
94
|
+
return false;
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const parts = line.split("\t");
|
|
97
|
+
const status = (parts[0] ?? "").trim();
|
|
98
|
+
const path = (parts[parts.length - 1] ?? "").trim();
|
|
99
|
+
if (path.includes(".hunch-commit.lock"))
|
|
100
|
+
continue; // transient lock dir, never a record
|
|
101
|
+
if (!/^[AM]$/.test(status))
|
|
102
|
+
return false; // only Add / Modify โ any D/R/C/T โ not a memory sync
|
|
103
|
+
if (!path.endsWith(".json"))
|
|
104
|
+
return false; // the store is entirely JSON records
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
62
108
|
/** Merge the overlay's remote into the local branch (pull, no rebase), leaving a CLEAN tree
|
|
63
109
|
* whether it succeeds or not. Returns true when the branch is safe to push (merged, or there's
|
|
64
110
|
* no upstream to merge), false when a conflict was aborted (caller skips the push and retries
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
6
6
|
"description": "Architectural Conformance for AI-generated code: a git-native graph that deterministically blocks AI changes which break your architecture โ the semantic invariants (layering, must-reach, dependency direction) pattern-SAST can't express โ grounded in the decisions and bugs behind each rule, across any MCP assistant (Claude Code, Cursor, Copilot, Windsurf, Codex).",
|