@davesheffer/hunch 0.18.0 → 0.18.1
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 +2 -2
- package/dist/cli/index.js +15 -7
- package/dist/extractors/git.js +17 -0
- package/dist/mcp/server.js +15 -3
- package/dist/store/hunchStore.js +13 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -221,7 +221,7 @@ src/
|
|
|
221
221
|
|
|
222
222
|
Everything lives under `.hunch/` as git-tracked JSON (the source of truth); SQLite is a
|
|
223
223
|
throwaway derived index. → [storage layout](https://hunch-pi.vercel.app/docs#storage) ·
|
|
224
|
-
[
|
|
224
|
+
[the docs](https://hunch-pi.vercel.app/docs) for the full conceptual model.
|
|
225
225
|
|
|
226
226
|
## Notable engineering decisions
|
|
227
227
|
|
|
@@ -247,5 +247,5 @@ npm run build # compile to dist/ (the published artifact)
|
|
|
247
247
|
```
|
|
248
248
|
|
|
249
249
|
Hunch is pure TypeScript ESM, Node ≥ 20, licensed **Apache-2.0**. See
|
|
250
|
-
[CONTRIBUTING.md](CONTRIBUTING.md)
|
|
250
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) and the full
|
|
251
251
|
[developer docs](https://hunch-pi.vercel.app/docs#develop).
|
package/dist/cli/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import { indexRepo } from "../extractors/indexer.js";
|
|
|
28
28
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
29
29
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
30
30
|
import { selectProvider } from "../synthesis/provider.js";
|
|
31
|
-
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, revExists } from "../extractors/git.js";
|
|
31
|
+
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, revExists, commitAndPushHunch } from "../extractors/git.js";
|
|
32
32
|
import { renderText, renderMarkdown, reportFailsStrict } from "../core/checkreport.js";
|
|
33
33
|
import { installPostCommitHook, installPreCommitHook } from "../integrations/hooks.js";
|
|
34
34
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
@@ -254,10 +254,7 @@ program
|
|
|
254
254
|
// hook (no recursion, including on a manual `hunch sync --commit`).
|
|
255
255
|
const commitTarget = opts.commit ? (opts.private ? store.privateDir : hunchPaths(root).hunch) : undefined;
|
|
256
256
|
if (commitTarget) {
|
|
257
|
-
|
|
258
|
-
g(["add", "--", "."]);
|
|
259
|
-
g(["commit", "-m", `hunch: capture ${r.decision?.id ?? "decision"}`]);
|
|
260
|
-
g(["push"]);
|
|
257
|
+
commitAndPushHunch(commitTarget, `hunch: capture ${r.decision?.id ?? "decision"}`);
|
|
261
258
|
if (!opts.quiet)
|
|
262
259
|
console.log(` ↳ committed + pushed ${r.decision?.id} (${commitTarget})`);
|
|
263
260
|
}
|
|
@@ -275,9 +272,20 @@ program
|
|
|
275
272
|
.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).")
|
|
276
273
|
.option("--repo <url>", "clone a private git repo to use as the store (into ./.hunch-private)")
|
|
277
274
|
.option("--no-hook", "don't switch the post-commit hook to private sync")
|
|
278
|
-
.option("--auto-commit", "opt-in:
|
|
275
|
+
.option("--auto-commit", "opt-in: also git add+commit+push the private repo after each capture (post-commit hook AND MCP private writes)")
|
|
276
|
+
.option("--sync", "flush the configured private store now (git add+commit+push) — catches records made via MCP between commits")
|
|
279
277
|
.action((dir, opts) => {
|
|
280
278
|
const root = findRoot();
|
|
279
|
+
if (opts.sync) {
|
|
280
|
+
const s = new HunchStore(hunchPaths(root));
|
|
281
|
+
const target = s.privateDir;
|
|
282
|
+
s.close();
|
|
283
|
+
if (!target)
|
|
284
|
+
return fail("no private overlay configured — run `hunch private` first");
|
|
285
|
+
commitAndPushHunch(target, "hunch: sync private memory");
|
|
286
|
+
console.log(`✓ flushed private store → ${target}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
281
289
|
const paths = hunchPaths(root);
|
|
282
290
|
// 1) resolve the private store's hunch dir (holds decisions/, bugs/, …)
|
|
283
291
|
let hunchDir;
|
|
@@ -303,7 +311,7 @@ program
|
|
|
303
311
|
// a store elsewhere on disk. Resolution (env || local.json) re-resolves against root.
|
|
304
312
|
const rel = relative(root, hunchDir);
|
|
305
313
|
const stored = rel && !rel.startsWith("..") && !isAbsolute(rel) ? toPosixTarget(rel) : hunchDir;
|
|
306
|
-
writeFileAtomic(join(paths.hunch, "local.json"), JSON.stringify({ privateDir: stored }, null, 2) + "\n");
|
|
314
|
+
writeFileAtomic(join(paths.hunch, "local.json"), JSON.stringify({ privateDir: stored, autoCommit: !!opts.autoCommit }, null, 2) + "\n");
|
|
307
315
|
ensureGitignore(root); // keeps .hunch/local.json + .hunch-private/ out of git
|
|
308
316
|
// 4) route post-commit synthesis to the overlay (local hook, never committed)
|
|
309
317
|
let hookNote = "";
|
package/dist/extractors/git.js
CHANGED
|
@@ -19,6 +19,23 @@ function gitSafe(args, cwd, maxBuffer) {
|
|
|
19
19
|
export function isGitRepo(cwd) {
|
|
20
20
|
return gitSafe(["rev-parse", "--is-inside-work-tree"], cwd) === "true";
|
|
21
21
|
}
|
|
22
|
+
/** Best-effort: stage ONLY the hunch dir, commit, and push the repo it lives in. Shared by
|
|
23
|
+
* the post-commit auto-commit (CLI sync --commit), MCP private writes, and `hunch private
|
|
24
|
+
* --sync`. HUNCH_SYNC=1 stops the created commit from re-triggering the post-commit hook
|
|
25
|
+
* (no recursion). Stages with a pathspec scoped to `hunchDir`, so it never sweeps unrelated
|
|
26
|
+
* working-tree changes. Never throws — a non-repo dir / offline push just no-ops. */
|
|
27
|
+
export function commitAndPushHunch(hunchDir, message) {
|
|
28
|
+
const env = { ...process.env, HUNCH_SYNC: "1" };
|
|
29
|
+
const run = (args) => {
|
|
30
|
+
try {
|
|
31
|
+
execFileSync("git", ["-C", hunchDir, ...args], { stdio: "ignore", env });
|
|
32
|
+
}
|
|
33
|
+
catch { /* best-effort: nothing staged / not a repo / offline */ }
|
|
34
|
+
};
|
|
35
|
+
run(["add", "--", "."]);
|
|
36
|
+
run(["commit", "-m", message]);
|
|
37
|
+
run(["push"]);
|
|
38
|
+
}
|
|
22
39
|
export function headSha(cwd) {
|
|
23
40
|
return gitSafe(["rev-parse", "HEAD"], cwd);
|
|
24
41
|
}
|
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 } from "../extractors/git.js";
|
|
17
|
+
import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff, commitAndPushHunch } from "../extractors/git.js";
|
|
18
18
|
import { formatContext } from "../core/format.js";
|
|
19
19
|
import { renderMarkdown, verdict } from "../core/checkreport.js";
|
|
20
20
|
import { HUNCH_VERSION } from "../core/version.js";
|
|
@@ -292,9 +292,16 @@ export function buildServer(root) {
|
|
|
292
292
|
// the public store, so skip it for a private record (a v1 limitation, not a leak).
|
|
293
293
|
const superseded = decision.supersedes && !decision.private ? store.supersede(decision.supersedes, rec) : null;
|
|
294
294
|
store.reindex();
|
|
295
|
+
// Auto-flush the private repo when configured (hunch private --auto-commit), so a
|
|
296
|
+
// record made via MCP between public commits is committed+pushed immediately.
|
|
297
|
+
let flushed = "";
|
|
298
|
+
if (decision.private && store.privateAutoCommit && store.privateDir) {
|
|
299
|
+
commitAndPushHunch(store.privateDir, `hunch: capture ${id}`);
|
|
300
|
+
flushed = " (committed + pushed to the private repo)";
|
|
301
|
+
}
|
|
295
302
|
const supNote = superseded ? ` Superseded ${superseded.id} (window closed at ${rec.valid_from}).` : "";
|
|
296
303
|
const note = decision.commit && !fullSha ? ` (note: commit "${decision.commit}" could not be resolved — recorded as a standalone decision, not linked to a commit)` : "";
|
|
297
|
-
const where = decision.private ?
|
|
304
|
+
const where = decision.private ? ` [PRIVATE overlay — not committed to this repo]${flushed}` : "";
|
|
298
305
|
return ok(`Recorded decision ${id}: "${rec.title}" (status ${rec.status}, ${source}).${where}${supNote}${note}`);
|
|
299
306
|
}
|
|
300
307
|
catch (e) {
|
|
@@ -328,10 +335,15 @@ export function buildServer(root) {
|
|
|
328
335
|
else
|
|
329
336
|
store.json.put("constraints", rec);
|
|
330
337
|
store.reindex();
|
|
338
|
+
let flushed = "";
|
|
339
|
+
if (input.private && store.privateAutoCommit && store.privateDir) {
|
|
340
|
+
commitAndPushHunch(store.privateDir, `hunch: capture ${rec.id}`);
|
|
341
|
+
flushed = " (committed + pushed to the private repo)";
|
|
342
|
+
}
|
|
331
343
|
const enforce = rec.severity === "blocking"
|
|
332
344
|
? "blocks a DIRECT edit to its scope at strict firmness, and fails a PR whose diff touches that scope (CI guard); blast-radius hits and lower firmness stay advisory"
|
|
333
345
|
: "flags violating edits and PRs (advisory)";
|
|
334
|
-
const where = input.private ?
|
|
346
|
+
const where = input.private ? ` [PRIVATE overlay — not committed to this repo]${flushed}` : "";
|
|
335
347
|
return ok(`${existing ? "Updated" : "Recorded"} ${rec.severity} constraint ${rec.id}: "${rec.statement}" (scope: ${rec.scope.join(", ")}).${where} It now ${enforce}.`);
|
|
336
348
|
}
|
|
337
349
|
catch (e) {
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -31,6 +31,9 @@ export class HunchStore {
|
|
|
31
31
|
/** The resolved private-overlay hunch dir (from env or .hunch/local.json), or undefined
|
|
32
32
|
* when no overlay is configured. Surfaced so `hunch doctor` reflects the true state. */
|
|
33
33
|
privateDir;
|
|
34
|
+
/** Whether private writes should auto commit+push the private repo (from local.json
|
|
35
|
+
* `autoCommit`, set by `hunch private --auto-commit`). Read by the MCP write tools. */
|
|
36
|
+
privateAutoCommit;
|
|
34
37
|
/** When true, recs() ignores the private overlay (public-only). Set transiently by
|
|
35
38
|
* buildCheckReport({publicOnly}) so any PUBLICLY-POSTED report (the CI PR comment)
|
|
36
39
|
* can never render a private record — a publicly-posted output is a leak surface
|
|
@@ -43,24 +46,27 @@ export class HunchStore {
|
|
|
43
46
|
// Private overlay location: env (override, for CI / portability) → else a gitignored
|
|
44
47
|
// local config (.hunch/local.json) so `hunch private` enables it with NO env var, and
|
|
45
48
|
// the MCP server / hook pick it up automatically. Relative paths resolve from root.
|
|
46
|
-
const
|
|
49
|
+
const local = this.localConfig();
|
|
50
|
+
const priv = process.env.HUNCH_PRIVATE_DIR?.trim() || local.privateDir;
|
|
47
51
|
if (priv) {
|
|
48
52
|
this.privateDir = resolve(this.paths.root, priv);
|
|
49
53
|
this.privateJson = new JsonStore(hunchPathsForDir(this.privateDir));
|
|
50
54
|
}
|
|
55
|
+
this.privateAutoCommit = !!(priv && local.autoCommit);
|
|
51
56
|
}
|
|
52
|
-
/** The private-overlay
|
|
53
|
-
* never committed). Tolerant:
|
|
54
|
-
|
|
57
|
+
/** The private-overlay config from the gitignored `.hunch/local.json` (per-machine,
|
|
58
|
+
* never committed). Tolerant: returns {} on missing/invalid so reads never crash. */
|
|
59
|
+
localConfig() {
|
|
55
60
|
try {
|
|
56
61
|
const f = join(this.paths.hunch, "local.json");
|
|
57
62
|
if (!existsSync(f))
|
|
58
|
-
return
|
|
63
|
+
return {};
|
|
59
64
|
const v = JSON.parse(readFileSync(f, "utf8"));
|
|
60
|
-
|
|
65
|
+
const privateDir = typeof v.privateDir === "string" && v.privateDir.trim() ? v.privateDir.trim() : undefined;
|
|
66
|
+
return { privateDir, autoCommit: v.autoCommit === true };
|
|
61
67
|
}
|
|
62
68
|
catch {
|
|
63
|
-
return
|
|
69
|
+
return {};
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
/** Merged read: public ∪ private overlay (private wins on id collision). Every
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
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.",
|