@davesheffer/hunch 0.36.2 → 0.37.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 +43 -4
- package/dist/eval/guards.js +28 -1
- package/dist/mcp/server.js +7 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -968,7 +968,7 @@ program
|
|
|
968
968
|
provenance: { source: "human_confirmed", confidence: 1, evidence: [], last_verified: new Date().toISOString() },
|
|
969
969
|
});
|
|
970
970
|
store.reindex();
|
|
971
|
-
|
|
971
|
+
refreshExistingGrounding(root, store); // keep EVERY assistant's grounding current, not just CLAUDE.md
|
|
972
972
|
console.log(`✓ recorded ${c.severity} constraint ${c.id}: "${c.statement}" (scope: ${scope.join(", ") || "repo"})`);
|
|
973
973
|
if (derived && c.forbids?.deps.length)
|
|
974
974
|
console.log(` ↳ matcher: forbids import of ${c.forbids.deps.join(", ")} (precise, immune to staleness)`);
|
|
@@ -1309,6 +1309,45 @@ program
|
|
|
1309
1309
|
const next = writeConfig(paths, { firmness: level }).firmness;
|
|
1310
1310
|
console.log(`✓ firmness set to ${next} (takes effect on the next edit — no Claude Code restart needed).`);
|
|
1311
1311
|
});
|
|
1312
|
+
// ---- status (enforcement readiness at a glance) ---------------------------
|
|
1313
|
+
program
|
|
1314
|
+
.command("status")
|
|
1315
|
+
.description("Enforcement readiness at a glance: what's enforcing, what's waiting to confirm, what went stale.")
|
|
1316
|
+
.action(() => {
|
|
1317
|
+
const { store, root } = storeFor();
|
|
1318
|
+
const firmness = readConfig(hunchPaths(root)).firmness;
|
|
1319
|
+
const vouchedSrc = (s) => !!s && (s.includes("human_confirmed") || s === "derived");
|
|
1320
|
+
const blocking = store.recs("constraints").filter((c) => c.status === "active" && c.severity === "blocking" && vouchedSrc(c.provenance?.source));
|
|
1321
|
+
const precise = blocking.filter((c) => !!effectiveForbids(c));
|
|
1322
|
+
const scopeOnly = blocking.filter((c) => !effectiveForbids(c));
|
|
1323
|
+
const drafts = store.json.loadAll("decisions").filter((d) => d.status === "proposed" || d.provenance.confidence < 0.6);
|
|
1324
|
+
const { ready, scrutiny } = partitionReview(drafts, READY_MIN_GROUNDED);
|
|
1325
|
+
const stale = store.staleness((f) => lastChangeDate(f, root)).filter((s) => s.kind === "constraint");
|
|
1326
|
+
const fnote = {
|
|
1327
|
+
off: "not enforcing — `hunch firmness advisory` to start",
|
|
1328
|
+
advisory: "surfaces context to the agent; never blocks",
|
|
1329
|
+
firm: "surfaces + warns on a violating edit",
|
|
1330
|
+
strict: "edit-time DENY + CI guard — the teeth are on",
|
|
1331
|
+
};
|
|
1332
|
+
console.log(`\nHunch — enforcement status (${root.split("/").pop()})\n`);
|
|
1333
|
+
console.log(` firmness: ${firmness} ← ${fnote[firmness] ?? ""}\n`);
|
|
1334
|
+
console.log(` ✓ ARMED ${blocking.length} confirmed blocking invariant(s) — held against every assistant`);
|
|
1335
|
+
if (blocking.length) {
|
|
1336
|
+
console.log(` ${precise.length} precise (block the actual violation, immune to staleness)`);
|
|
1337
|
+
console.log(` ${scopeOnly.length} scope-only (relax to advisory once the file changes)${scopeOnly.length ? " → harden with --forbid-dep" : ""}`);
|
|
1338
|
+
}
|
|
1339
|
+
if (ready.length || scrutiny.length) {
|
|
1340
|
+
console.log(`\n ⏳ TO CONFIRM ${ready.length} ready · ${scrutiny.length} need scrutiny → hunch review${ready.length ? " --accept-verified" : ""}`);
|
|
1341
|
+
}
|
|
1342
|
+
if (stale.length) {
|
|
1343
|
+
console.log(`\n ♻ STALE ${stale.length} rule(s) whose guarded code moved since last verified → re-confirm to keep the teeth`);
|
|
1344
|
+
}
|
|
1345
|
+
if (firmness !== "strict" && precise.length) {
|
|
1346
|
+
console.log(`\n ⚡ ${precise.length} precise rule(s) are armed but firmness is "${firmness}" — set \`hunch firmness strict\` to hard-block them.`);
|
|
1347
|
+
}
|
|
1348
|
+
console.log("");
|
|
1349
|
+
store.close();
|
|
1350
|
+
});
|
|
1312
1351
|
// ---- hook (Claude Code agent-hook handler) --------------------------------
|
|
1313
1352
|
program
|
|
1314
1353
|
.command("hook")
|
|
@@ -1446,7 +1485,7 @@ program
|
|
|
1446
1485
|
}
|
|
1447
1486
|
const { source, armed } = acceptDecision(store, d);
|
|
1448
1487
|
store.reindex();
|
|
1449
|
-
|
|
1488
|
+
refreshExistingGrounding(root, store); // confirming a rule must reach EVERY assistant's grounding
|
|
1450
1489
|
console.log(`✓ accepted ${opts.accept} (now ${source}, confidence 0.95${armed ? `, ${armed} tripwire(s) now blocking` : ""})`);
|
|
1451
1490
|
}
|
|
1452
1491
|
else if (opts.reject) {
|
|
@@ -1467,7 +1506,7 @@ program
|
|
|
1467
1506
|
for (const it of ready)
|
|
1468
1507
|
armedTotal += acceptDecision(store, it.d).armed;
|
|
1469
1508
|
store.reindex();
|
|
1470
|
-
|
|
1509
|
+
refreshExistingGrounding(root, store); // batch-confirm must reach EVERY assistant's grounding
|
|
1471
1510
|
console.log(`✓ accepted ${ready.length} verified draft(s); ${armedTotal} tripwire(s) now blocking.`);
|
|
1472
1511
|
for (const it of ready)
|
|
1473
1512
|
console.log(` ${it.d.id} grounded=${it.synth.grounded ?? "?"} ${it.d.title}`);
|
|
@@ -1557,7 +1596,7 @@ program
|
|
|
1557
1596
|
if (store.json.delete(c.kind, c.id))
|
|
1558
1597
|
removed++;
|
|
1559
1598
|
store.reindex();
|
|
1560
|
-
|
|
1599
|
+
refreshExistingGrounding(root, store); // removing records must reach EVERY assistant's grounding
|
|
1561
1600
|
console.log(`\n✓ Removed ${removed} record(s).`);
|
|
1562
1601
|
}
|
|
1563
1602
|
else {
|
package/dist/eval/guards.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { verdict } from "../core/checkreport.js";
|
|
2
2
|
import { pathMatchesGlob } from "../core/glob.js";
|
|
3
|
+
import { effectiveForbids } from "../core/constraintmatch.js";
|
|
3
4
|
const surfaced = (v) => v === "block" || v === "warn";
|
|
4
5
|
const matches = (expect, got) => (expect === "catch" ? surfaced(got) : got === expect);
|
|
5
6
|
/** Parse + validate a hand-authored golden set. */
|
|
@@ -47,7 +48,18 @@ export function generateGuardCases(store) {
|
|
|
47
48
|
.recs("constraints")
|
|
48
49
|
.filter((c) => c.status === "active" && c.severity === "blocking" && c.scope.length && isVouched(c.provenance?.source));
|
|
49
50
|
for (const c of blocking) {
|
|
50
|
-
|
|
51
|
+
const file = pathForGlob(c.scope[0]);
|
|
52
|
+
const line = violatingLine(c);
|
|
53
|
+
if (line) {
|
|
54
|
+
// CONTENT-MATCHED (dep/symbol): synthesize the REAL violation in scoped code and require a
|
|
55
|
+
// hard BLOCK — content is verified per commit, so a vouched matcher must block (not just warn).
|
|
56
|
+
cases.push({ name: `BLOCK · ${c.statement.slice(0, 56)}`, files: [file], diff: addLineDiff(file, line), expect: "block" });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// scope-only (or pattern-only, which we can't synthesize): a change in scope must at least be
|
|
60
|
+
// SURFACED (block or warn) — staleness/firmness decides which.
|
|
61
|
+
cases.push({ name: `CATCH · ${c.statement.slice(0, 56)}`, files: [file], expect: "catch" });
|
|
62
|
+
}
|
|
51
63
|
}
|
|
52
64
|
for (const p of ["docs/__eval__notes.md", "scripts/__eval__.txt", ".github/__eval__.yml"]) {
|
|
53
65
|
if (!blocking.some((c) => c.scope.some((g) => pathMatchesGlob(p, g)))) {
|
|
@@ -56,6 +68,21 @@ export function generateGuardCases(store) {
|
|
|
56
68
|
}
|
|
57
69
|
return cases;
|
|
58
70
|
}
|
|
71
|
+
/** A line of scoped code that actually trips a constraint's matcher — an import of the
|
|
72
|
+
* forbidden dep, or a call to the forbidden symbol. null when there's no synthesizable
|
|
73
|
+
* violation (scope-only, or a free-form regex we can't reverse). */
|
|
74
|
+
function violatingLine(c) {
|
|
75
|
+
const f = effectiveForbids(c);
|
|
76
|
+
if (f?.deps.length)
|
|
77
|
+
return `import _x from "${f.deps[0]}";`;
|
|
78
|
+
if (f?.symbols.length)
|
|
79
|
+
return `const _v = ${f.symbols[0]}();`;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/** A minimal unified diff that ADDS `line` to a (code) file, for a synthetic eval case. */
|
|
83
|
+
function addLineDiff(file, line) {
|
|
84
|
+
return `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1,1 +1,2 @@\n const __base = 1;\n+${line}\n`;
|
|
85
|
+
}
|
|
59
86
|
const isVouched = (source) => !!source && (source.includes("human_confirmed") || source === "derived");
|
|
60
87
|
/** A concrete path that matches a scope glob, for a synthetic case. Exact-file scopes pass
|
|
61
88
|
* through; wildcard scopes get a representative file inside them. */
|
package/dist/mcp/server.js
CHANGED
|
@@ -15,6 +15,7 @@ import { selectEmbedder } from "../store/embedder.js";
|
|
|
15
15
|
import { decisionId } from "../core/ids.js";
|
|
16
16
|
import { buildCorrectionConstraint } from "../core/correction.js";
|
|
17
17
|
import { knownRepoDeps } from "../synthesis/tripwires.js";
|
|
18
|
+
import { refreshExistingGrounding } from "../integrations/providers.js";
|
|
18
19
|
import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff, commitAndPushHunch, pullHunch } from "../extractors/git.js";
|
|
19
20
|
import { formatContext } from "../core/format.js";
|
|
20
21
|
import { compareCandidates } from "../core/compare.js";
|
|
@@ -370,6 +371,12 @@ export function buildServer(root) {
|
|
|
370
371
|
else
|
|
371
372
|
store.json.put("constraints", rec);
|
|
372
373
|
store.reindex();
|
|
374
|
+
// Propagate the new rule to EVERY assistant's ambient grounding (Cursor/Copilot/
|
|
375
|
+
// Windsurf/AGENTS.md/CLAUDE.md), so a correction captured in one assistant is held
|
|
376
|
+
// by all of them. Public only — a private rule must never render into committed
|
|
377
|
+
// grounding. Refresh-only: it never scaffolds a doc the project opted out of.
|
|
378
|
+
if (!input.private)
|
|
379
|
+
refreshExistingGrounding(root, store);
|
|
373
380
|
let flushed = "";
|
|
374
381
|
if (input.private && store.privateAutoCommit && store.privateDir) {
|
|
375
382
|
commitAndPushHunch(store.privateDir, `hunch: capture ${rec.id}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.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 graph of the decisions, bugs, and rules behind your code, served to any MCP coding assistant (Claude Code, Cursor, Copilot, Windsurf, Codex).",
|