@davesheffer/hunch 0.33.1 → 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 +11 -2
- 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
|
}
|
|
@@ -674,6 +681,8 @@ program
|
|
|
674
681
|
}
|
|
675
682
|
}
|
|
676
683
|
store.reindex();
|
|
684
|
+
if (opts.private && (dec || con))
|
|
685
|
+
flushPrivate(store, `hunch: capture ${dec + con} inline intent(s)`);
|
|
677
686
|
if (!intents.length)
|
|
678
687
|
console.log("No `hunch-why:` / `hunch-rule:` comments found.");
|
|
679
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.",
|