@davesheffer/hunch 0.36.3 → 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 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
- updateClaudeMd(root, store);
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
- updateClaudeMd(root, store);
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
- updateClaudeMd(root, store);
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
- updateClaudeMd(root, store);
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 {
@@ -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.36.3",
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).",