@davesheffer/hunch 1.11.0 → 1.12.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/dist/cli/index.js CHANGED
@@ -57,7 +57,8 @@ import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpol
57
57
  import { isHumanConfirmed } from "../core/strictgate.js";
58
58
  import { appendEvent, readEvents } from "../core/events.js";
59
59
  import { computeStats, formatStats } from "../core/stats.js";
60
- import { injectionMode } from "../core/hookcache.js";
60
+ import { injectionMode, resetSessionInjections } from "../core/hookcache.js";
61
+ import { recordServed, servedSummary } from "../core/served.js";
61
62
  import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
62
63
  import { PIPELINE_LOOP, UNVERIFIED_NAG, loadPipelineState, onCommand, onEdit, onPrompt, onSkill, pipelineEnabled, savePipelineState, stopVerdict, } from "../core/pipeline.js";
63
64
  import { draftDuplicateOf, isAcceptedDuplicateAnchor } from "../core/dupdetect.js";
@@ -3718,6 +3719,47 @@ program
3718
3719
  store.close();
3719
3720
  }
3720
3721
  });
3722
+ program
3723
+ .command("served")
3724
+ .description("Delivery receipts: which memory records actually reached an agent, how often, and which never have")
3725
+ .option("--json", "emit the raw ledger summary")
3726
+ .action((opts) => {
3727
+ const { store, root } = storeFor();
3728
+ try {
3729
+ const summary = servedSummary(root);
3730
+ if (opts.json) {
3731
+ console.log(JSON.stringify(summary, null, 2));
3732
+ return;
3733
+ }
3734
+ if (!summary.total) {
3735
+ console.log("No delivery receipts yet — they accrue as the pre-edit hook and subagent grounding fire on this machine.");
3736
+ return;
3737
+ }
3738
+ console.log(`\nHunch — delivery receipts (this machine)\n`);
3739
+ console.log(` ${summary.total} deliveries · ${summary.distinct_records} distinct record(s) · ${summary.distinct_sessions} session(s) · ${summary.first_at?.slice(0, 10)} → ${summary.last_at?.slice(0, 10)}\n`);
3740
+ const titleOf = (row) => {
3741
+ if (row.kind === "decisions")
3742
+ return store.recs("decisions").find((r) => r.id === row.record_id)?.title ?? row.record_id;
3743
+ if (row.kind === "constraints")
3744
+ return store.recs("constraints").find((r) => r.id === row.record_id)?.statement.slice(0, 70) ?? row.record_id;
3745
+ return row.record_id;
3746
+ };
3747
+ console.log(" Most delivered:");
3748
+ for (const row of summary.rows.slice(0, 10)) {
3749
+ console.log(` ${String(row.serves).padStart(4)}× (+${row.refreshes} still-current) ${row.record_id} — ${titleOf(row)}`);
3750
+ }
3751
+ const servedIds = new Set(summary.rows.map((r) => r.record_id));
3752
+ const neverServed = [
3753
+ ...store.recs("constraints").filter((c) => c.status === "active" && !servedIds.has(c.id)).map((c) => c.id),
3754
+ ...store.recs("decisions").filter((d) => d.status === "accepted" && !d.superseded_by && !servedIds.has(d.id)).map((d) => d.id),
3755
+ ];
3756
+ console.log(`\n Never delivered on this machine: ${neverServed.length} in-force record(s)${neverServed.length ? ` — compact candidates start here (${neverServed.slice(0, 5).join(", ")}${neverServed.length > 5 ? ", …" : ""})` : ""}`);
3757
+ console.log(` Prevented violations live in \`hunch stats\` (events ledger); receipts here are the delivery half.\n`);
3758
+ }
3759
+ finally {
3760
+ store.close();
3761
+ }
3762
+ });
3721
3763
  program
3722
3764
  .command("hook")
3723
3765
  .description("Agent-agnostic hook handler: normalizes Claude, VS Code, Cursor, Windsurf, and Antigravity events into Hunch context and strict policy checks. Reads hook JSON on stdin.")
@@ -3797,7 +3839,92 @@ program
3797
3839
  emitContext(provider, "UserPromptSubmit", text);
3798
3840
  return;
3799
3841
  }
3842
+ if (evt.hook_event_name === "PreCompact") {
3843
+ // Compaction is about to summarize injected grounding out of the agent's
3844
+ // context while the dedup map still says "delivered". Reset it so every
3845
+ // post-compact injection is full again. Emit nothing — this event has no
3846
+ // context channel worth spending.
3847
+ resetSessionInjections(evt.session_id);
3848
+ return;
3849
+ }
3850
+ if (evt.hook_event_name === "SubagentStart") {
3851
+ // A delegated agent starts with NONE of the parent session's grounding:
3852
+ // session orientation never fired inside it and only per-edit PreToolUse
3853
+ // follows it in — so read-only agents (Explore/Plan) could work fully
3854
+ // blind. Slice by what the agent TYPE is about to do (dec_a788cc039b):
3855
+ // explorers get the indexed shape, planners get live decisions + what
3856
+ // was already rejected, everyone else gets the invariant digest. Public
3857
+ // store only; cheap reads.
3858
+ const s = new HunchStore(paths);
3859
+ try {
3860
+ const clip1 = (text, max) => {
3861
+ const flat = text.replace(/\s+/g, " ").trim();
3862
+ return flat.length > max ? `${flat.slice(0, max - 1).trimEnd()}…` : flat;
3863
+ };
3864
+ const type = (evt.agent_type ?? "").toLowerCase();
3865
+ const L = [];
3866
+ const served = [];
3867
+ if (/explore|search|investigat/.test(type)) {
3868
+ // Orient from the graph, not grep rounds: the component map IS the shape.
3869
+ const components = s.advisoryRecs("components").filter((c) => c.status === "active");
3870
+ if (!components.length)
3871
+ return;
3872
+ L.push(`🧠 Hunch — repo shape for a delegated explorer: ${components.length} component(s).`);
3873
+ for (const c of components.slice(0, 12)) {
3874
+ L.push(`- ${c.name}${c.paths.length ? ` (${c.paths.slice(0, 2).join(", ")})` : ""}${c.responsibility ? ` — ${clip1(c.responsibility, 90)}` : ""}`);
3875
+ served.push({ kind: "components", record_id: c.id });
3876
+ }
3877
+ if (components.length > 12)
3878
+ L.push(`…and ${components.length - 12} more — hunch_structure() for the full map.`);
3879
+ L.push("Orient: hunch_structure(target) · hunch_why(target) · hunch_context(task).");
3880
+ }
3881
+ else if (/plan|architect|design/.test(type)) {
3882
+ // A plan drafted blind re-proposes what the graph already rejected.
3883
+ const decisions = s.advisoryRecs("decisions")
3884
+ .filter((d) => d.status === "accepted")
3885
+ .sort((a, b) => (a.date < b.date ? 1 : -1));
3886
+ if (!decisions.length)
3887
+ return;
3888
+ L.push(`🧠 Hunch — live decisions for a delegated planner (${decisions.length} in force; plans must not re-propose the rejected).`);
3889
+ for (const d of decisions.slice(0, 6)) {
3890
+ L.push(`- ${d.title} (${d.id})${d.alternatives_rejected.length ? ` — rejected: ${clip1(d.alternatives_rejected[0], 80)}` : ""}`);
3891
+ served.push({ kind: "decisions", record_id: d.id });
3892
+ }
3893
+ L.push("Before finalizing a plan: hunch_why(target) · hunch_current_decision(topic) · hunch_check_constraints(scope).");
3894
+ }
3895
+ else {
3896
+ const sevRank = { blocking: 0, warning: 1, advisory: 2 };
3897
+ const constraints = s.advisoryRecs("constraints")
3898
+ .filter((c) => c.status === "active")
3899
+ .sort((a, b) => sevRank[a.severity] - sevRank[b.severity]);
3900
+ if (!constraints.length)
3901
+ return;
3902
+ L.push(`🧠 Hunch — delegated agent grounding: ${constraints.length} invariant(s) in force in this repo.`);
3903
+ for (const c of constraints.slice(0, 8)) {
3904
+ L.push(`- [${c.severity}] ${clip1(c.statement, 140)}${c.scope.length ? ` (scope: ${c.scope.slice(0, 3).join(", ")})` : ""}`);
3905
+ served.push({ kind: "constraints", record_id: c.id });
3906
+ }
3907
+ if (constraints.length > 8)
3908
+ L.push(`…and ${constraints.length - 8} more — hunch_check_constraints(scope) for your files.`);
3909
+ L.push("Before editing: hunch_check_constraints(scope) · hunch_why(target). Orient: hunch_context(task).");
3910
+ }
3911
+ // No dedup here: the hook event carries the PARENT session id, but each
3912
+ // spawned agent is a fresh empty context — deduping would ground the
3913
+ // first Explore and silently starve every later one.
3914
+ recordServed(root, served.map((r) => ({ ...r, event: "served", target: `(subagent:${evt.agent_type ?? "any"})`, session_id: evt.session_id })));
3915
+ emitContext(provider, "SubagentStart", L.join("\n"));
3916
+ }
3917
+ finally {
3918
+ s.close();
3919
+ }
3920
+ return;
3921
+ }
3800
3922
  if (evt.hook_event_name === "SessionStart") {
3923
+ // A compact-resume means everything injected so far was just summarized
3924
+ // away — the dedup map must forget it delivered anything, or the rest of
3925
+ // the session gets delta one-liners against grounding that is gone.
3926
+ if (evt.source === "compact")
3927
+ resetSessionInjections(evt.session_id);
3801
3928
  // Orientation at the moment it matters: what just happened + what's next,
3802
3929
  // straight from the graph — the agent sits down already knowing where it
3803
3930
  // is instead of pulling (or worse, grepping) for it. Cheap reads only
@@ -3953,10 +4080,21 @@ program
3953
4080
  // Identical grounding already shown this session → one-line delta instead of
3954
4081
  // the full 10-16KB block. Any record change re-sends the full text; the
3955
4082
  // strict-gate deny path above never routes through this (dec_244397d920).
4083
+ // Delivery receipts (dec_925f4bcaad): the ledger of what actually reached
4084
+ // an agent. A full injection is a serve; a delta one-liner attests the
4085
+ // earlier serve is still standing. Never throws, never blocks.
4086
+ const receipts = (event) => recordServed(root, [
4087
+ ...ctx.constraints.map((c) => ({ event, kind: "constraints", record_id: c.id, target, session_id: evt.session_id })),
4088
+ ...ctx.decisions.map((d) => ({ event, kind: "decisions", record_id: d.id, target, session_id: evt.session_id })),
4089
+ ...ctx.bugs.map((b) => ({ event, kind: "bugs", record_id: b.id, target, session_id: evt.session_id })),
4090
+ ...ctx.findings.map((f) => ({ event, kind: "findings", record_id: f.id, target, session_id: evt.session_id })),
4091
+ ]);
3956
4092
  if (injectionMode(evt.session_id, `pre:${target}`, text) === "delta") {
4093
+ receipts("refreshed");
3957
4094
  emitContext(provider, "PreToolUse", `Hunch grounding for ${target}: unchanged this session (${ctx.decisions.length} decision(s), ${ctx.constraints.length} invariant(s) shown earlier — still current; hunch_why("${target}") to re-expand).`);
3958
4095
  return;
3959
4096
  }
4097
+ receipts("served");
3960
4098
  emitContext(provider, "PreToolUse", text);
3961
4099
  }
3962
4100
  catch {
@@ -77,6 +77,8 @@ function eventName(value, provider) {
77
77
  posttooluse: "PostToolUse",
78
78
  userpromptsubmit: "UserPromptSubmit",
79
79
  sessionstart: "SessionStart",
80
+ subagentstart: "SubagentStart",
81
+ precompact: "PreCompact",
80
82
  stop: "Stop",
81
83
  };
82
84
  if (map[name])
@@ -155,6 +157,8 @@ export function normalizeHookEvent(raw, provider) {
155
157
  tool_name: hunchToolName(stringAt(input, "tool_name", "toolName"), toolInput ?? {}),
156
158
  tool_input: toolInput,
157
159
  prompt: stringAt(input, "prompt", "user_prompt", "userPrompt"),
160
+ source: stringAt(input, "source"),
161
+ agent_type: stringAt(input, "agent_type", "agentType", "subagent_type", "subagentType"),
158
162
  };
159
163
  }
160
164
  /** Provider-aware hook output. Context output is intentionally omitted for
@@ -28,16 +28,40 @@ function fencedRanges(text) {
28
28
  ranges.push([open.start, text.length]);
29
29
  return ranges;
30
30
  }
31
+ /** Character ranges covered by inline code spans (`…`), same rationale as
32
+ * fencedRanges: prose quoting a marker in backticks is showing an example.
33
+ * CommonMark-lite: an opener run pairs with the next run of the SAME length
34
+ * on the same line; unpaired runs never open a span. */
35
+ function inlineSpanRanges(text) {
36
+ const ranges = [];
37
+ let offset = 0;
38
+ for (const line of text.split("\n")) {
39
+ let pending = null;
40
+ const runs = /`+/g;
41
+ let m;
42
+ while ((m = runs.exec(line))) {
43
+ if (!pending)
44
+ pending = { len: m[0].length, start: m.index };
45
+ else if (m[0].length === pending.len) {
46
+ ranges.push([offset + pending.start, offset + m.index + m[0].length - 1]);
47
+ pending = null;
48
+ }
49
+ }
50
+ offset += line.length + 1;
51
+ }
52
+ return ranges;
53
+ }
31
54
  /** Parse every hunch:topic marker out of a markdown document. Markers inside
32
- * fenced code blocks are examples, not declarations, and are skipped. */
55
+ * fenced code blocks or inline code spans are examples, not declarations,
56
+ * and are skipped. */
33
57
  export function parseDocAnchors(text) {
34
58
  const out = [];
35
- const fences = fencedRanges(text);
59
+ const skip = [...fencedRanges(text), ...inlineSpanRanges(text)];
36
60
  MARKER.lastIndex = 0;
37
61
  let m;
38
62
  while ((m = MARKER.exec(text))) {
39
63
  const at = m.index;
40
- if (fences.some(([s, e]) => at >= s && at <= e))
64
+ if (skip.some(([s, e]) => at >= s && at <= e))
41
65
  continue;
42
66
  out.push({ topic: m[1], pin: m[2] ?? null, line: text.slice(0, at).split("\n").length });
43
67
  }
@@ -20,7 +20,11 @@ import { join, extname } from "node:path";
20
20
  import { parseDocAnchors } from "./docanchors.js";
21
21
  import { currentForTopic } from "./topics.js";
22
22
  import { compareCodeUnits } from "./canonicalOrder.js";
23
- export const STALE_MARKER = /\b(proposed|not yet implemented|no code yet)\b/i;
23
+ // Self-referential status declarations only. A bare "proposed" is too loose:
24
+ // the auto-generated grounding block in AGENTS.md legitimately DESCRIBES the
25
+ // proposed decision status ("candidate/proposed rules") and was graded stale
26
+ // for it — a false alarm in the machinery that polices false alarms.
27
+ export const STALE_MARKER = /\b(?:status|state)\s*[:\-—]\s*(?:proposed|draft)\b|\bnot yet implemented\b|\bno code yet\b/i;
24
28
  export const SRC_REF = /\bsrc\/[A-Za-z0-9_\-/]+\.ts\b/g;
25
29
  const SKIP_DIRS = new Set(["node_modules", ".git", ".hunch", ".hunch-private", "dist", "vscode-extension", "site"]);
26
30
  /** Bounded walk for repo markdown (root + docs/, depth-limited; heavy/irrelevant trees skipped). */
@@ -55,6 +55,22 @@ export function injectionMode(sessionId, key, content) {
55
55
  return "full"; // grounded beats deduped, always
56
56
  }
57
57
  }
58
+ /** Forget everything injected into a session. Compaction summarizes injected
59
+ * grounding out of the agent's context while the dedup map still says
60
+ * "delivered" — so on PreCompact / SessionStart[source=compact] the map must
61
+ * reset, or post-compact edits get delta one-liners against grounding the
62
+ * agent no longer has. Never throws (same posture as injectionMode). */
63
+ export function resetSessionInjections(sessionId) {
64
+ try {
65
+ if (!sessionId)
66
+ return;
67
+ const file = join(tmpdir(), "hunch-hookcache", `${sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80)}.json`);
68
+ rmSync(file, { force: true });
69
+ }
70
+ catch {
71
+ /* unwritable tmpdir — next injectionMode call falls back to "full" anyway */
72
+ }
73
+ }
58
74
  /** Drop session caches from long-gone sessions (best effort, bounded dir). */
59
75
  function sweep(dir) {
60
76
  try {
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Delivery receipts (roadmap dec_925f4bcaad): a machine-local ledger of which
3
+ * memory records were actually DELIVERED to an agent, when, and into what.
4
+ *
5
+ * This is observed telemetry, not derived state: it cannot be reconstructed
6
+ * from the JSON store, so it must NOT live in the reindex-rebuilt SQLite index
7
+ * (con_a87360128b's derived layer is dropped and rebuilt at will). It gets its
8
+ * own database under .hunch-cache/ — gitignored, per-machine, append-only —
9
+ * the same family as hookcache's session state, not the store's.
10
+ *
11
+ * Failure posture inherits the hook's: recording a receipt must never cost a
12
+ * delivery. Every entry point swallows every error; a lost receipt is noise,
13
+ * a blocked edit is a broken product.
14
+ */
15
+ import { createRequire } from "node:module";
16
+ import { mkdirSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ /** Load node:sqlite while swallowing ONLY its ExperimentalWarning — the same
19
+ * discipline as src/store/db.ts: this module rides the hook into every CLI
20
+ * invocation, and Hunch's stderr reaches humans, hooks, and MCP clients. A
21
+ * bare top-level import printed the warning on every command and failed the
22
+ * release gate's clean-stderr contract. */
23
+ function loadSqlite() {
24
+ const require = createRequire(import.meta.url);
25
+ const realEmit = process.emitWarning.bind(process);
26
+ process.emitWarning = ((warning, ...rest) => {
27
+ if (String(warning).includes("SQLite is an experimental feature"))
28
+ return;
29
+ realEmit(warning, ...rest);
30
+ });
31
+ try {
32
+ return require("node:sqlite");
33
+ }
34
+ finally {
35
+ process.emitWarning = realEmit;
36
+ }
37
+ }
38
+ let sqlite = null;
39
+ function openServedDb(root) {
40
+ sqlite ??= loadSqlite();
41
+ const dir = join(root, ".hunch-cache");
42
+ mkdirSync(dir, { recursive: true });
43
+ const db = new sqlite.DatabaseSync(join(dir, "served.db"));
44
+ db.exec(`CREATE TABLE IF NOT EXISTS served (
45
+ at TEXT NOT NULL,
46
+ session TEXT,
47
+ event TEXT NOT NULL,
48
+ kind TEXT NOT NULL,
49
+ record_id TEXT NOT NULL,
50
+ target TEXT NOT NULL
51
+ );
52
+ CREATE INDEX IF NOT EXISTS served_record ON served (record_id);`);
53
+ return db;
54
+ }
55
+ /** Append delivery receipts. Never throws — a receipt must never cost a delivery. */
56
+ export function recordServed(root, entries) {
57
+ if (!entries.length)
58
+ return;
59
+ try {
60
+ const db = openServedDb(root);
61
+ try {
62
+ const at = new Date().toISOString();
63
+ const insert = db.prepare("INSERT INTO served (at, session, event, kind, record_id, target) VALUES (?, ?, ?, ?, ?, ?)");
64
+ for (const entry of entries) {
65
+ insert.run(at, entry.session_id ?? null, entry.event, entry.kind, entry.record_id, entry.target);
66
+ }
67
+ }
68
+ finally {
69
+ db.close();
70
+ }
71
+ }
72
+ catch {
73
+ /* unwritable cache dir / locked db — the delivery already happened; drop the receipt */
74
+ }
75
+ }
76
+ /** The ledger, aggregated per record. Never throws; an unreadable ledger reads as empty. */
77
+ export function servedSummary(root) {
78
+ const empty = { total: 0, distinct_records: 0, distinct_sessions: 0, first_at: null, last_at: null, rows: [] };
79
+ try {
80
+ const db = openServedDb(root);
81
+ try {
82
+ const totals = db.prepare("SELECT COUNT(*) AS total, COUNT(DISTINCT record_id) AS records, COUNT(DISTINCT session) AS sessions, MIN(at) AS first_at, MAX(at) AS last_at FROM served").get();
83
+ const rows = db.prepare(`SELECT record_id, kind,
84
+ SUM(CASE WHEN event = 'served' THEN 1 ELSE 0 END) AS serves,
85
+ SUM(CASE WHEN event = 'refreshed' THEN 1 ELSE 0 END) AS refreshes,
86
+ MAX(at) AS last_at
87
+ FROM served GROUP BY record_id, kind ORDER BY serves DESC, refreshes DESC`).all();
88
+ return {
89
+ total: totals?.total ?? 0,
90
+ distinct_records: totals?.records ?? 0,
91
+ distinct_sessions: totals?.sessions ?? 0,
92
+ first_at: totals?.first_at ?? null,
93
+ last_at: totals?.last_at ?? null,
94
+ rows,
95
+ };
96
+ }
97
+ finally {
98
+ db.close();
99
+ }
100
+ }
101
+ catch {
102
+ return empty;
103
+ }
104
+ }
105
+ //# sourceMappingURL=served.js.map
@@ -156,6 +156,18 @@ export function installClaudeHooks(root, hookCmd) {
156
156
  ...keep(json.hooks.SessionStart),
157
157
  { hooks: [{ type: "command", command: hookCmd }] },
158
158
  ];
159
+ // Delegated agents start with no session grounding (orientation never fired
160
+ // inside them); compaction summarizes injected grounding away while the dedup
161
+ // map still says "delivered". These two events keep delivery alive across the
162
+ // whole session lifecycle, not just its first context window.
163
+ json.hooks.SubagentStart = [
164
+ ...keep(json.hooks.SubagentStart),
165
+ { hooks: [{ type: "command", command: hookCmd }] },
166
+ ];
167
+ json.hooks.PreCompact = [
168
+ ...keep(json.hooks.PreCompact),
169
+ { hooks: [{ type: "command", command: hookCmd }] },
170
+ ];
159
171
  // Verification pipeline (core/pipeline.ts): PostToolUse records observable
160
172
  // facts (edits, verify commands); Stop refuses to end a turn with unverified
161
173
  // product edits at firm/strict firmness. Delivery is enforced, not hoped for.
@@ -49,6 +49,13 @@ const flushNote = (flush, home, mode) => flush === "pushed" ? ` (committed + pus
49
49
  ? " (committed to the overlay repo — push deferred: offline, no upstream, or merge conflict; the next capture or `hunch private --sync` retries)"
50
50
  : " (auto-committed to .hunch/ — rides your next push)"
51
51
  : "";
52
+ /** When an overlay exists, a PUBLIC write deserves one visible line: a record that
53
+ * lands in the committed store publishes on the next push, and an agent writing
54
+ * strategy/competitive content there is a leak nobody notices until it ships
55
+ * (2026-08-09: 15 roadmap records caught pre-push only by a release sweep). */
56
+ const publicHomeNote = (home, hasPrivate) => home === "public" && hasPrivate
57
+ ? "\nℹ Landed in the COMMITTED PUBLIC store (publishes with the repo). For sensitive/strategy content, re-record with private:true — the overlay store."
58
+ : "";
52
59
  // Read-side token budgets: every tool result is injected into a Claude Code
53
60
  // session, so an uncapped list pollutes the context window. Cap each list to its
54
61
  // highest-signal head (records are pre-sorted by severity/confidence) and tell the
@@ -903,7 +910,7 @@ export function buildServerWithRootControl(initialRoot) {
903
910
  // record commits+pushes its overlay repo; a public one commits .hunch/ in THIS repo
904
911
  // (commit only — it rides the user's next push, never auto-pushing their code branch).
905
912
  const flush = flushCapture(store, hunchPaths(root).hunch, !!decision.private, `hunch: capture ${id}`, startupTeamRoute ?? undefined);
906
- const flushed = flushNote(flush, home, store.mode);
913
+ const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate);
907
914
  // Capture-session gate (staged deprecation, §9.3): the token was consumed
908
915
  // above (it also decides the provenance tier). No token still writes
909
916
  // (non-breaking) but lands as agent_recorded with a nudge toward /capture.
@@ -980,7 +987,7 @@ export function buildServerWithRootControl(initialRoot) {
980
987
  if (home === "public" && !store.autoCommit)
981
988
  refreshExistingGrounding(root, store); // overlay rules never render into committed grounding
982
989
  const flush = flushCapture(store, hunchPaths(root).hunch, !!input.private, `hunch: capture ${rec.id}`, startupTeamRoute ?? undefined);
983
- const flushed = flushNote(flush, home, store.mode);
990
+ const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate);
984
991
  const enforce = rec.severity === "blocking"
985
992
  ? "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"
986
993
  : "flags violating edits and PRs (advisory)";
@@ -1056,7 +1063,7 @@ export function buildServerWithRootControl(initialRoot) {
1056
1063
  store.putCapture("findings", rec, !!finding.private);
1057
1064
  store.reindex();
1058
1065
  const flush = flushCapture(store, hunchPaths(root).hunch, !!finding.private, `hunch: capture ${id}`, startupTeamRoute ?? undefined);
1059
- const flushed = flushNote(flush, home, store.mode);
1066
+ const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate);
1060
1067
  const where = finding.private
1061
1068
  ? ` [PRIVATE overlay — not committed to this repo]${flushed}`
1062
1069
  : home === "private" ? ` [SHARED store — one source of truth for the whole team]${flushed}` : flushed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.11.0",
3
+ "version": "1.12.1",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
@@ -52,6 +52,8 @@
52
52
  "node": ">=22.13.0"
53
53
  },
54
54
  "scripts": {
55
+ "version": "node tooling/sync-version-pins.mjs && git add plugin/.mcp.json server.json",
56
+ "sync-version-pins": "node tooling/sync-version-pins.mjs",
55
57
  "clean": "node --input-type=commonjs -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
56
58
  "build": "npm run clean && tsc -p tsconfig.json",
57
59
  "dev": "tsx src/cli/index.ts",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://hunch-pi.vercel.app",
10
- "version": "1.11.0",
10
+ "version": "1.12.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.11.0",
16
+ "version": "1.12.1",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {