@davesheffer/hunch 0.17.0 → 0.17.2
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 +9 -8
- package/dist/cli/index.js +65 -10
- package/dist/integrations/gitignore.js +3 -0
- package/dist/integrations/hooks.js +8 -4
- package/dist/store/hunchStore.js +27 -4
- package/dist/synthesis/synthesize.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,14 +160,15 @@ per-machine fixups. → [docs](https://hunch-pi.vercel.app/docs#team)
|
|
|
160
160
|
|
|
161
161
|
## Private memory (public repo, private context)
|
|
162
162
|
|
|
163
|
-
Open-source your code without open-sourcing your *reasoning*.
|
|
164
|
-
separate
|
|
165
|
-
MCP and the pre-edit hook see your sensitive decisions/bugs/constraints
|
|
166
|
-
`.hunch/` stays clean. It
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
Open-source your code without open-sourcing your *reasoning*. **`hunch private`** sets up a
|
|
164
|
+
separate private store in one command — Hunch unions it into every query and guard **locally**
|
|
165
|
+
(MCP and the pre-edit hook see your sensitive decisions/bugs/constraints) while your public
|
|
166
|
+
`.hunch/` stays clean. It writes a gitignored `.hunch/local.json` so it's auto-detected — **no
|
|
167
|
+
env var, no shell-profile edit** (and `HUNCH_PRIVATE_DIR` still overrides per-shell). **Opt-in,
|
|
168
|
+
default-off** (no config → fully inert), and **leak-safe by construction**: committed files and
|
|
169
|
+
the CI PR comment render *public-only*, so a private record can't reach a public surface. Record
|
|
170
|
+
sensitive items with `private: true` (`hunch_record_decision` / `hunch_record_correction`);
|
|
171
|
+
post-commit synthesis can route there too. → [docs](https://hunch-pi.vercel.app/docs#private)
|
|
171
172
|
|
|
172
173
|
## Continuous learning (CI)
|
|
173
174
|
|
package/dist/cli/index.js
CHANGED
|
@@ -13,14 +13,16 @@
|
|
|
13
13
|
* mcp start the MCP server (Claude Code connects here)
|
|
14
14
|
* doctor environment diagnostics
|
|
15
15
|
*/
|
|
16
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
17
17
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
18
|
-
import { relative, resolve } from "node:path";
|
|
18
|
+
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
19
19
|
import { Command } from "commander";
|
|
20
|
-
import { hunchPaths, findRoot, toPosixTarget } from "../core/paths.js";
|
|
20
|
+
import { hunchPaths, hunchPathsForDir, findRoot, toPosixTarget } from "../core/paths.js";
|
|
21
|
+
import { writeFileAtomic } from "../core/io.js";
|
|
21
22
|
import { looksLikeCorrection, CORRECTION_NUDGE } from "../core/correction.js";
|
|
22
23
|
import { HUNCH_VERSION } from "../core/version.js";
|
|
23
24
|
import { HunchStore } from "../store/hunchStore.js";
|
|
25
|
+
import { JsonStore } from "../store/jsonStore.js";
|
|
24
26
|
import { selectEmbedder } from "../store/embedder.js";
|
|
25
27
|
import { indexRepo } from "../extractors/indexer.js";
|
|
26
28
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
@@ -64,6 +66,7 @@ program
|
|
|
64
66
|
.option("--no-providers", "skip scaffolding non-Claude assistant configs (Cursor / VS Code / Codex / AGENTS.md)")
|
|
65
67
|
.option("--no-agent-hooks", "skip installing the Claude Code agent hooks (.claude/settings.json)")
|
|
66
68
|
.option("--firmness <level>", "agent-hook firmness: off | advisory | firm | strict")
|
|
69
|
+
.option("--private-sync", "post-commit synthesis writes captured decisions into the private overlay (HUNCH_PRIVATE_DIR), never the public repo")
|
|
67
70
|
.action((opts) => {
|
|
68
71
|
// Validate --firmness up front, before any side effects (indexing, git hooks,
|
|
69
72
|
// .mcp.json) or opening the store — a bad value must not leave a half-init.
|
|
@@ -92,8 +95,8 @@ program
|
|
|
92
95
|
console.log(` ⚠ ${res.skipped} file(s) could not be parsed (skipped)`);
|
|
93
96
|
}
|
|
94
97
|
if (isGitRepo(root)) {
|
|
95
|
-
const h = installPostCommitHook(root, inv.shell);
|
|
96
|
-
console.log(` ✓ post-commit hook ${h.action} (learning loop)`);
|
|
98
|
+
const h = installPostCommitHook(root, inv.shell, { private: opts.privateSync });
|
|
99
|
+
console.log(` ✓ post-commit hook ${h.action} (learning loop)${opts.privateSync ? " — syncs to the private overlay" : ""}`);
|
|
97
100
|
const m = installMergeDriver(root, inv.shell);
|
|
98
101
|
console.log(` ✓ team merge driver ${m.action}`);
|
|
99
102
|
// Auto-install the pre-commit guard by default (advisory: flags invariants
|
|
@@ -223,12 +226,17 @@ program
|
|
|
223
226
|
.option("--from-hook", "invoked by the git hook")
|
|
224
227
|
.option("--quiet", "minimal output")
|
|
225
228
|
.option("--force", "re-synthesize even if a decision already exists for the commit")
|
|
229
|
+
.option("--private", "write the synthesized decision into the private overlay (HUNCH_PRIVATE_DIR), not the public repo — for a repo whose memory is kept private")
|
|
226
230
|
.action(async (sha, opts) => {
|
|
227
231
|
const { store, root } = storeFor();
|
|
228
232
|
if (!isGitRepo(root))
|
|
229
233
|
return opts.quiet ? undefined : fail("sync needs a git repo");
|
|
234
|
+
if (opts.private && !store.hasPrivate) {
|
|
235
|
+
store.close();
|
|
236
|
+
return opts.quiet ? undefined : fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
237
|
+
}
|
|
230
238
|
store.json.ensureDirs();
|
|
231
|
-
const r = await syncCommit(store, root, sha ?? headSha(root), { force: opts.force });
|
|
239
|
+
const r = await syncCommit(store, root, sha ?? headSha(root), { force: opts.force, private: opts.private });
|
|
232
240
|
if (r.status === "written") {
|
|
233
241
|
store.reindex();
|
|
234
242
|
// Don't rewrite CLAUDE.md from the hook — it would dirty the working tree
|
|
@@ -243,6 +251,54 @@ program
|
|
|
243
251
|
}
|
|
244
252
|
store.close();
|
|
245
253
|
});
|
|
254
|
+
// ---- private (one-command setup for the private memory overlay) ------------
|
|
255
|
+
program
|
|
256
|
+
.command("private [dir]")
|
|
257
|
+
.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).")
|
|
258
|
+
.option("--repo <url>", "clone a private git repo to use as the store (into ./.hunch-private)")
|
|
259
|
+
.option("--no-hook", "don't switch the post-commit hook to private sync")
|
|
260
|
+
.action((dir, opts) => {
|
|
261
|
+
const root = findRoot();
|
|
262
|
+
const paths = hunchPaths(root);
|
|
263
|
+
// 1) resolve the private store's hunch dir (holds decisions/, bugs/, …)
|
|
264
|
+
let hunchDir;
|
|
265
|
+
if (opts.repo) {
|
|
266
|
+
const dest = join(root, ".hunch-private");
|
|
267
|
+
if (!existsSync(dest)) {
|
|
268
|
+
const r = spawnSync("git", ["clone", opts.repo, dest], { stdio: "inherit" });
|
|
269
|
+
if (r.status !== 0)
|
|
270
|
+
return fail(`git clone failed for ${opts.repo}`);
|
|
271
|
+
}
|
|
272
|
+
hunchDir = join(dest, ".hunch");
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
hunchDir = dir ? resolve(root, dir) : join(root, ".hunch-private", ".hunch");
|
|
276
|
+
}
|
|
277
|
+
// 2) create the layout (decisions/, manifest, …) so it's queryable immediately
|
|
278
|
+
new JsonStore(hunchPathsForDir(hunchDir)).ensureDirs();
|
|
279
|
+
// 3) record the path in a GITIGNORED local config — auto-detected, no env var, and
|
|
280
|
+
// the MCP server picks it up too. Atomic write (con_902759b3dc) since it's under .hunch/.
|
|
281
|
+
mkdirSync(paths.hunch, { recursive: true }); // tolerate a repo where `hunch init` hasn't run yet
|
|
282
|
+
// Store a repo-relative POSIX path when the store lives INSIDE the repo (portable +
|
|
283
|
+
// OS-clean — survives a repo move, resolves the same on any OS); an absolute path for
|
|
284
|
+
// a store elsewhere on disk. Resolution (env || local.json) re-resolves against root.
|
|
285
|
+
const rel = relative(root, hunchDir);
|
|
286
|
+
const stored = rel && !rel.startsWith("..") && !isAbsolute(rel) ? toPosixTarget(rel) : hunchDir;
|
|
287
|
+
writeFileAtomic(join(paths.hunch, "local.json"), JSON.stringify({ privateDir: stored }, null, 2) + "\n");
|
|
288
|
+
ensureGitignore(root); // keeps .hunch/local.json + .hunch-private/ out of git
|
|
289
|
+
// 4) route post-commit synthesis to the overlay (local hook, never committed)
|
|
290
|
+
let hookNote = "";
|
|
291
|
+
if (opts.hook && isGitRepo(root)) {
|
|
292
|
+
const inv = resolveInvocation();
|
|
293
|
+
const h = installPostCommitHook(root, inv.shell, { private: true });
|
|
294
|
+
hookNote = ` ✓ post-commit hook ${h.action} — captured decisions route here\n`;
|
|
295
|
+
}
|
|
296
|
+
console.log(`✓ private overlay enabled → ${hunchDir}\n` +
|
|
297
|
+
` ✓ recorded in .hunch/local.json (gitignored) — auto-detected, no env var or shell-profile edit\n` +
|
|
298
|
+
hookNote +
|
|
299
|
+
` record sensitive items with private:true (hunch_record_decision / hunch_record_correction)\n` +
|
|
300
|
+
` override per-shell with HUNCH_PRIVATE_DIR; CI / public PR comments stay public-only.`);
|
|
301
|
+
});
|
|
246
302
|
// ---- query ----------------------------------------------------------------
|
|
247
303
|
program
|
|
248
304
|
.command("query")
|
|
@@ -1043,10 +1099,9 @@ program
|
|
|
1043
1099
|
}
|
|
1044
1100
|
const c = store.reindex().counts;
|
|
1045
1101
|
console.log(`hunch: ${c.symbols} symbols, ${c.edges} edges, ${c.components} components, ${c.decisions} decisions, ${c.bugs} bugs, ${c.constraints} constraints`);
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
: dim(`private: off — set HUNCH_PRIVATE_DIR to overlay a separate private memory repo for sensitive records`));
|
|
1102
|
+
console.log(store.privateDir
|
|
1103
|
+
? `private: on → ${store.privateDir} (local overlay — unioned into queries; never committed or posted publicly)`
|
|
1104
|
+
: dim(`private: off — run \`hunch private\` to keep sensitive memory in a separate repo (or set HUNCH_PRIVATE_DIR)`));
|
|
1050
1105
|
// Semantic search is opt-in and local. Report availability + coverage without
|
|
1051
1106
|
// loading the model (selectEmbedder only probes; embeddingStats just counts rows).
|
|
1052
1107
|
const emb = await selectEmbedder();
|
|
@@ -19,6 +19,9 @@ const ENTRIES = [
|
|
|
19
19
|
".hunch/*.sqlite-wal",
|
|
20
20
|
".hunch/*.sqlite-journal",
|
|
21
21
|
".hunch/**/*.tmp*",
|
|
22
|
+
// Per-machine private-overlay pointer written by `hunch private` (holds the local
|
|
23
|
+
// path to the private store) — never committed.
|
|
24
|
+
".hunch/local.json",
|
|
22
25
|
// A local PRIVATE overlay store (HUNCH_PRIVATE_DIR) for sensitive memory — never
|
|
23
26
|
// committed. This is the conventional in-repo path; point the env elsewhere for a
|
|
24
27
|
// fully separate private repo.
|
|
@@ -9,17 +9,21 @@ import { join, isAbsolute } from "node:path";
|
|
|
9
9
|
import { hooksDir } from "../extractors/git.js";
|
|
10
10
|
const MARK = "# >>> hunch post-commit >>>";
|
|
11
11
|
const ENDMARK = "# <<< hunch post-commit <<<";
|
|
12
|
-
function block(invocation) {
|
|
12
|
+
function block(invocation, opts = {}) {
|
|
13
|
+
// --private routes the auto-synthesized decision into the HUNCH_PRIVATE_DIR overlay
|
|
14
|
+
// instead of the public repo. The hook script is local (.git/hooks/), never committed,
|
|
15
|
+
// so a repo whose memory is kept private leaves no trace of this in the public tree.
|
|
16
|
+
const priv = opts.private ? " --private" : "";
|
|
13
17
|
return [
|
|
14
18
|
MARK,
|
|
15
19
|
'if [ -z "$HUNCH_SYNC" ]; then',
|
|
16
20
|
" export HUNCH_SYNC=1",
|
|
17
|
-
` ( ${invocation} sync --from-hook --quiet >/dev/null 2>&1 || true ) &`,
|
|
21
|
+
` ( ${invocation} sync --from-hook --quiet${priv} >/dev/null 2>&1 || true ) &`,
|
|
18
22
|
"fi",
|
|
19
23
|
ENDMARK,
|
|
20
24
|
].join("\n");
|
|
21
25
|
}
|
|
22
|
-
export function installPostCommitHook(root, invocation) {
|
|
26
|
+
export function installPostCommitHook(root, invocation, opts = {}) {
|
|
23
27
|
const dir = hooksDir(root);
|
|
24
28
|
// `git rev-parse --git-path hooks` returns a path relative to the repo in a
|
|
25
29
|
// normal checkout, but an ABSOLUTE one inside a linked worktree (the shared
|
|
@@ -28,7 +32,7 @@ export function installPostCommitHook(root, invocation) {
|
|
|
28
32
|
const abs = isAbsolute(dir) ? dir : join(root, dir);
|
|
29
33
|
mkdirSync(abs, { recursive: true });
|
|
30
34
|
const hookPath = join(abs, "post-commit");
|
|
31
|
-
const blk = block(invocation);
|
|
35
|
+
const blk = block(invocation, opts);
|
|
32
36
|
if (!existsSync(hookPath)) {
|
|
33
37
|
writeFileSync(hookPath, `#!/bin/sh\n${blk}\n`);
|
|
34
38
|
chmodSync(hookPath, 0o755);
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - bugLineage(): bugs matching a symptom/symbol + their lineage
|
|
11
11
|
* - fragility(): ranked fragility report with evidence
|
|
12
12
|
*/
|
|
13
|
-
import { resolve } from "node:path";
|
|
13
|
+
import { resolve, join } from "node:path";
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
15
|
import { toPosixTarget, hunchPathsForDir } from "../core/paths.js";
|
|
15
16
|
import { ENTITY_KINDS } from "../core/types.js";
|
|
16
17
|
import { openDb } from "./db.js";
|
|
@@ -27,6 +28,9 @@ export class HunchStore {
|
|
|
27
28
|
/** Optional PRIVATE overlay (HUNCH_PRIVATE_DIR) — a second store in a repo the
|
|
28
29
|
* user controls. Unioned into reads via recs(); never written by public paths. */
|
|
29
30
|
privateJson;
|
|
31
|
+
/** The resolved private-overlay hunch dir (from env or .hunch/local.json), or undefined
|
|
32
|
+
* when no overlay is configured. Surfaced so `hunch doctor` reflects the true state. */
|
|
33
|
+
privateDir;
|
|
30
34
|
/** When true, recs() ignores the private overlay (public-only). Set transiently by
|
|
31
35
|
* buildCheckReport({publicOnly}) so any PUBLICLY-POSTED report (the CI PR comment)
|
|
32
36
|
* can never render a private record — a publicly-posted output is a leak surface
|
|
@@ -36,9 +40,28 @@ export class HunchStore {
|
|
|
36
40
|
constructor(paths) {
|
|
37
41
|
this.paths = paths;
|
|
38
42
|
this.json = new JsonStore(paths);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
// Private overlay location: env (override, for CI / portability) → else a gitignored
|
|
44
|
+
// local config (.hunch/local.json) so `hunch private` enables it with NO env var, and
|
|
45
|
+
// the MCP server / hook pick it up automatically. Relative paths resolve from root.
|
|
46
|
+
const priv = process.env.HUNCH_PRIVATE_DIR?.trim() || this.localPrivateDir();
|
|
47
|
+
if (priv) {
|
|
48
|
+
this.privateDir = resolve(this.paths.root, priv);
|
|
49
|
+
this.privateJson = new JsonStore(hunchPathsForDir(this.privateDir));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** The private-overlay path from the gitignored `.hunch/local.json` (per-machine,
|
|
53
|
+
* never committed). Tolerant: undefined on missing/invalid so reads never crash. */
|
|
54
|
+
localPrivateDir() {
|
|
55
|
+
try {
|
|
56
|
+
const f = join(this.paths.hunch, "local.json");
|
|
57
|
+
if (!existsSync(f))
|
|
58
|
+
return undefined;
|
|
59
|
+
const v = JSON.parse(readFileSync(f, "utf8"));
|
|
60
|
+
return typeof v.privateDir === "string" && v.privateDir.trim() ? v.privateDir.trim() : undefined;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
42
65
|
}
|
|
43
66
|
/** Merged read: public ∪ private overlay (private wins on id collision). Every
|
|
44
67
|
* QUERY / REINDEX path uses this so MCP + the guards see private memory. Public-
|
|
@@ -119,7 +119,12 @@ export async function syncCommit(store, root, sha, opts = {}) {
|
|
|
119
119
|
},
|
|
120
120
|
date: meta.date, // the commit date
|
|
121
121
|
};
|
|
122
|
-
|
|
122
|
+
// Route to the PRIVATE overlay when asked (post-commit sync in a repo whose memory
|
|
123
|
+
// is kept private) — keeps auto-captured decisions out of the public repo entirely.
|
|
124
|
+
if (opts.private)
|
|
125
|
+
store.putPrivate("decisions", decision);
|
|
126
|
+
else
|
|
127
|
+
store.json.put("decisions", decision);
|
|
123
128
|
return { status: "written", decision, provider: provider.name };
|
|
124
129
|
}
|
|
125
130
|
/** Capture a Bug from a test failure. Suspects are ranked churn×recency×fan-in. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
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.",
|