@davesheffer/hunch 0.33.0 → 0.34.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.
- package/dist/cli/index.js +33 -3
- package/dist/extractors/git.js +45 -1
- package/dist/integrations/sync.js +8 -0
- package/dist/mcp/server.js +10 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { renderText, renderMarkdown, reportFailsStrict } from "../core/checkrepo
|
|
|
35
35
|
import { partitionReview, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
|
|
36
36
|
import { installPostCommitHook, installPreCommitHook } from "../integrations/hooks.js";
|
|
37
37
|
import { ensureSharedOverlayPointer } from "../integrations/worktree.js";
|
|
38
|
+
import { flushPrivate } from "../integrations/sync.js";
|
|
38
39
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
39
40
|
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS } from "../integrations/gitignore.js";
|
|
40
41
|
import { writeCiWorkflow } from "../integrations/ciAction.js";
|
|
@@ -312,7 +313,7 @@ program
|
|
|
312
313
|
.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).")
|
|
313
314
|
.option("--repo <url>", "clone a private git repo to use as the store (into ./.hunch-private)")
|
|
314
315
|
.option("--no-hook", "don't switch the post-commit hook to private sync")
|
|
315
|
-
.option("--auto-commit", "
|
|
316
|
+
.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)")
|
|
316
317
|
.option("--sync", "flush the configured private store now (git add+commit+push) — catches records made via MCP between commits")
|
|
317
318
|
.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")
|
|
318
319
|
.action((dir, opts) => {
|
|
@@ -344,6 +345,13 @@ program
|
|
|
344
345
|
}
|
|
345
346
|
// 2) create the layout (decisions/, manifest, …) so it's queryable immediately
|
|
346
347
|
new JsonStore(hunchPathsForDir(hunchDir)).ensureDirs();
|
|
348
|
+
const inv = resolveInvocation();
|
|
349
|
+
// Install the structured merge driver IN the overlay repo so concurrent captures from
|
|
350
|
+
// multiple machines/worktrees merge by RECORD ID (no manual conflict resolution) when the
|
|
351
|
+
// two-way auto-sync pulls before pushing. The overlay repo root is the parent of its .hunch.
|
|
352
|
+
const overlayRoot = hunchPathsForDir(hunchDir).root;
|
|
353
|
+
if (isGitRepo(overlayRoot))
|
|
354
|
+
installMergeDriver(overlayRoot, inv.shell);
|
|
347
355
|
// 3) record the path in a GITIGNORED local config — auto-detected, no env var, and
|
|
348
356
|
// the MCP server picks it up too. Atomic write (con_902759b3dc) since it's under .hunch/.
|
|
349
357
|
mkdirSync(paths.hunch, { recursive: true }); // tolerate a repo where `hunch init` hasn't run yet
|
|
@@ -365,7 +373,6 @@ program
|
|
|
365
373
|
// 4) route post-commit synthesis to the overlay (local hook, never committed)
|
|
366
374
|
let hookNote = "";
|
|
367
375
|
if (opts.hook && isGitRepo(root)) {
|
|
368
|
-
const inv = resolveInvocation();
|
|
369
376
|
const h = installPostCommitHook(root, inv.shell, { private: true, commit: opts.autoCommit });
|
|
370
377
|
hookNote = ` ✓ post-commit hook ${h.action} — captured decisions route here${opts.autoCommit ? " (auto-commit+push on)" : ""}\n`;
|
|
371
378
|
}
|
|
@@ -409,6 +416,7 @@ program
|
|
|
409
416
|
.description("Create a git worktree already wired into Hunch — it shares this repo's memory (the private overlay), with zero per-worktree setup.")
|
|
410
417
|
.option("-b, --branch <name>", "create the worktree on a NEW branch")
|
|
411
418
|
.option("--no-share", "don't register the overlay at the git common dir (the worktree won't see private memory)")
|
|
419
|
+
.option("--no-index", "don't build the new worktree's code graph (skip if you'll index later)")
|
|
412
420
|
.action((path, opts) => {
|
|
413
421
|
const root = findRoot();
|
|
414
422
|
if (!isGitRepo(root))
|
|
@@ -439,8 +447,28 @@ program
|
|
|
439
447
|
else {
|
|
440
448
|
shareNote = ` · no private overlay configured — run \`hunch private\` to share memory across worktrees`;
|
|
441
449
|
}
|
|
450
|
+
// 3) build the new worktree's CODE GRAPH (symbols/edges → blast-radius / dependents).
|
|
451
|
+
// Indexed IN-PROCESS (uses THIS install's tree-sitter, so the worktree needs no
|
|
452
|
+
// node_modules) and ONLY when the graph isn't already committed in the checkout —
|
|
453
|
+
// re-parsing a normal repo would just dirty its tracked .hunch/*.json. Writes the
|
|
454
|
+
// derived (gitignored) index, never the working tree.
|
|
455
|
+
let indexNote = "";
|
|
456
|
+
if (opts.index !== false) {
|
|
457
|
+
const wstore = new HunchStore(hunchPaths(dest));
|
|
458
|
+
wstore.json.ensureDirs();
|
|
459
|
+
if (wstore.json.loadAll("symbols").length === 0) {
|
|
460
|
+
const res = indexRepo(wstore, dest);
|
|
461
|
+
wstore.reindex();
|
|
462
|
+
indexNote = `\n ✓ indexed ${res.files} file(s) → ${res.symbols} symbols — blast-radius ready (code graph isn't committed here)`;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
wstore.reindex(); // committed graph already in the checkout → just build the derived SQLite
|
|
466
|
+
indexNote = `\n ✓ code graph present in the checkout — blast-radius ready`;
|
|
467
|
+
}
|
|
468
|
+
wstore.close();
|
|
469
|
+
}
|
|
442
470
|
console.log(`✓ worktree created → ${dest}${opts.branch ? ` (new branch ${opts.branch})` : ""}\n` +
|
|
443
|
-
`${shareNote}\n` +
|
|
471
|
+
`${shareNote}${indexNote}\n` +
|
|
444
472
|
` hooks + MCP server are shared (worktree-aware) — open your assistant in the new worktree to start.\n` +
|
|
445
473
|
` (needs \`hunch\` installed globally; a worktree has no node_modules of its own)`);
|
|
446
474
|
});
|
|
@@ -653,6 +681,8 @@ program
|
|
|
653
681
|
}
|
|
654
682
|
}
|
|
655
683
|
store.reindex();
|
|
684
|
+
if (opts.private && (dec || con))
|
|
685
|
+
flushPrivate(store, `hunch: capture ${dec + con} inline intent(s)`);
|
|
656
686
|
if (!intents.length)
|
|
657
687
|
console.log("No `hunch-why:` / `hunch-rule:` comments found.");
|
|
658
688
|
else
|
package/dist/extractors/git.js
CHANGED
|
@@ -44,7 +44,51 @@ export function commitAndPushHunch(hunchDir, message) {
|
|
|
44
44
|
};
|
|
45
45
|
run(["add", "--", "."]);
|
|
46
46
|
run(["commit", "-m", message]);
|
|
47
|
-
|
|
47
|
+
// Two-way sync: MERGE the remote BEFORE pushing, so a push can never be rejected
|
|
48
|
+
// non-fast-forward (the cause of memory piling up unpushed across machines). The .hunch
|
|
49
|
+
// merge driver resolves same-record conflicts by id; non-overlapping records merge cleanly
|
|
50
|
+
// without it. On an unresolved conflict / offline, mergeRemote aborts to a clean tree and we
|
|
51
|
+
// skip the push — the local commit stays and syncs on the next write (never lost).
|
|
52
|
+
if (mergeRemote(hunchDir, env))
|
|
53
|
+
run(["push"]);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
try {
|
|
57
|
+
rmSync(lock, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
catch { /* released best-effort */ }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Merge the overlay's remote into the local branch (pull, no rebase), leaving a CLEAN tree
|
|
63
|
+
* whether it succeeds or not. Returns true when the branch is safe to push (merged, or there's
|
|
64
|
+
* no upstream to merge), false when a conflict was aborted (caller skips the push and retries
|
|
65
|
+
* on the next write). Never throws. */
|
|
66
|
+
function mergeRemote(hunchDir, env) {
|
|
67
|
+
const tryGit = (args) => {
|
|
68
|
+
try {
|
|
69
|
+
execFileSync("git", ["-C", hunchDir, ...args], { stdio: "ignore", env });
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
if (!tryGit(["rev-parse", "--abbrev-ref", "@{upstream}"]))
|
|
77
|
+
return true; // no remote/offline → push no-ops
|
|
78
|
+
if (tryGit(["pull", "--no-edit", "--no-rebase", "--autostash"]))
|
|
79
|
+
return true;
|
|
80
|
+
tryGit(["merge", "--abort"]); // conflict the driver couldn't resolve → restore a clean tree
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/** Best-effort READ-side sync: merge the overlay's remote into the local branch (e.g. on MCP
|
|
84
|
+
* server start) so this machine/session sees other machines' memory. Never throws; leaves a
|
|
85
|
+
* clean tree. Serialized with the commit lock so it can't race a concurrent flush. */
|
|
86
|
+
export function pullHunch(hunchDir) {
|
|
87
|
+
const lock = join(hunchDir, ".hunch-commit.lock");
|
|
88
|
+
if (!acquireCommitLock(lock))
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
mergeRemote(hunchDir, { ...process.env, HUNCH_SYNC: "1" });
|
|
48
92
|
}
|
|
49
93
|
finally {
|
|
50
94
|
try {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { commitAndPushHunch } from "../extractors/git.js";
|
|
2
|
+
/** Auto-commit + push the overlay after a private write, when auto-commit is on. No-op
|
|
3
|
+
* otherwise (manual `hunch private --sync` still works). Never throws. */
|
|
4
|
+
export function flushPrivate(store, message) {
|
|
5
|
+
if (store.privateAutoCommit && store.privateDir)
|
|
6
|
+
commitAndPushHunch(store.privateDir, message);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=sync.js.map
|
package/dist/mcp/server.js
CHANGED
|
@@ -14,7 +14,7 @@ import { HunchStore } from "../store/hunchStore.js";
|
|
|
14
14
|
import { selectEmbedder } from "../store/embedder.js";
|
|
15
15
|
import { decisionId } from "../core/ids.js";
|
|
16
16
|
import { buildCorrectionConstraint } from "../core/correction.js";
|
|
17
|
-
import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff, commitAndPushHunch } from "../extractors/git.js";
|
|
17
|
+
import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff, commitAndPushHunch, pullHunch } from "../extractors/git.js";
|
|
18
18
|
import { formatContext } from "../core/format.js";
|
|
19
19
|
import { compareCandidates } from "../core/compare.js";
|
|
20
20
|
import { checkConformance } from "../core/conformance.js";
|
|
@@ -52,6 +52,15 @@ function resolveFiles(store, target) {
|
|
|
52
52
|
}
|
|
53
53
|
export function buildServer(root) {
|
|
54
54
|
const store = new HunchStore(hunchPaths(root));
|
|
55
|
+
// Two-way sync (read side): pull the private overlay's remote on startup, so THIS machine's
|
|
56
|
+
// session sees memory captured on other machines/worktrees before we index — making the
|
|
57
|
+
// overlay genuinely one source of truth. Best-effort, leaves a clean tree, never blocks start.
|
|
58
|
+
if (store.privateDir) {
|
|
59
|
+
try {
|
|
60
|
+
pullHunch(store.privateDir);
|
|
61
|
+
}
|
|
62
|
+
catch { /* offline / no remote — proceed with local */ }
|
|
63
|
+
}
|
|
55
64
|
// Ensure the SQLite index reflects the JSON source of truth on startup.
|
|
56
65
|
try {
|
|
57
66
|
store.reindex();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
6
6
|
"description": "Hunch — an Engineering Memory OS: a persistent, git-native reasoning graph over a codebase, exposed to Claude Code via MCP.",
|