@davesheffer/hunch 1.13.0 → 1.13.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 CHANGED
@@ -16,8 +16,8 @@ strict enforcement.
16
16
  **Memory is the input. The product boundary is the receipt:** relevant evidence before an edit,
17
17
  then a deterministic check of the change against the rules your team has explicitly trusted.
18
18
 
19
- > **New in v1.13.0:** CLI, MCP, and edit hooks now share one provenance-checked, hard-budgeted
20
- > delivery envelope, and local receipts record exactly what reached the agent and why.
19
+ > **New in v1.13.1:** `hunch_context` exposes that provenance-checked, hard-budgeted delivery
20
+ > envelope as MCP structured output and records exactly which returned items reached the client.
21
21
 
22
22
  See the public [roadmap](ROADMAP.md) for what is next and what is deliberately out of scope.
23
23
 
@@ -82,7 +82,7 @@ Git repo that every teammate can access, install the Matrix release on team mach
82
82
  have one maintainer run:
83
83
 
84
84
  ```bash
85
- npm i -g @davesheffer/hunch@1.13.0
85
+ npm i -g @davesheffer/hunch@1.13.1
86
86
  hunch shared --repo git@github.com:acme/project-hunch-memory.git
87
87
  git add .gitignore .hunch/team.json
88
88
  git commit -m "chore: connect shared Hunch memory"
@@ -97,7 +97,7 @@ printed by Hunch. Omit `--migrate` for a new setup.
97
97
  After the pointer commit lands, teammates need Hunch installed and Git access to the memory repo:
98
98
 
99
99
  ```bash
100
- npm i -g @davesheffer/hunch@1.13.0
100
+ npm i -g @davesheffer/hunch@1.13.1
101
101
  git pull
102
102
  hunch init
103
103
  hunch doctor
@@ -21,7 +21,9 @@ import { refreshExistingGrounding } from "../integrations/providers.js";
21
21
  import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff, workingFiles, workingDiff, pullHunchStatus, sameRemoteUrl } from "../extractors/git.js";
22
22
  import { flushCapture, flushMemoryHome, pinSharedRemote } from "../integrations/sync.js";
23
23
  import { advertisedTeamRemoteContract, ensureTeamOverlay, overlayMatchesTeamRemote, readTeamConfig, teamRemoteContract, teamSharedRef } from "../integrations/team.js";
24
- import { formatContext, formatStructure } from "../core/format.js";
24
+ import { formatStructure } from "../core/format.js";
25
+ import { buildDeliveryEnvelope } from "../core/delivery.js";
26
+ import { recordServed } from "../core/served.js";
25
27
  import { compareCandidates } from "../core/compare.js";
26
28
  import { checkConformance } from "../core/conformance.js";
27
29
  import { ConstitutionService, policyEvaluationEnvelope } from "../constitution/service.js";
@@ -82,6 +84,61 @@ const FINDINGS_CAP = 12; // hunch_findings listing
82
84
  const SEV_CONSTRAINT = { blocking: 3, warning: 2, advisory: 1 };
83
85
  const SEV_BUG = { critical: 4, high: 3, medium: 2, low: 1 };
84
86
  const more = (total, cap, hint = "") => total > cap ? `\n …(+${total - cap} more${hint ? ` — ${hint}` : ""})` : "";
87
+ /** Public MCP shape for the canonical delivery envelope. Keeping the schema on
88
+ * the tool means orchestrators can consume receipt facts without scraping the
89
+ * backward-compatible text block. */
90
+ const DELIVERY_OUTPUT_SCHEMA = z.object({
91
+ text: z.string(),
92
+ delivered: z.array(z.object({
93
+ kind: z.enum(["constraints", "decisions", "bugs", "findings"]),
94
+ record_id: z.string(),
95
+ rank: z.number().int().positive(),
96
+ delivery_reason: z.enum(["ranked", "blocking-reserved"]),
97
+ provenance_status: z.enum(["current", "unverified", "stale"]),
98
+ token_cost: z.number().int().nonnegative(),
99
+ })),
100
+ supplements: z.array(z.object({
101
+ id: z.string(),
102
+ kind: z.string(),
103
+ delivered: z.boolean(),
104
+ reason: z.enum(["supplemental", "budget", "empty"]),
105
+ rank: z.number().int().positive(),
106
+ token_cost: z.number().int().nonnegative(),
107
+ })),
108
+ omitted: z.array(z.object({
109
+ kind: z.enum(["constraints", "decisions", "bugs", "findings"]),
110
+ record_id: z.string(),
111
+ reason: z.enum(["budget", "stale-provenance", "retired"]),
112
+ detail: z.string(),
113
+ })),
114
+ budget_tokens: z.number().int().nonnegative(),
115
+ used_chars: z.number().int().nonnegative(),
116
+ blocking_overflow: z.boolean(),
117
+ });
118
+ /** Return the same human-readable brief older clients consume plus the exact
119
+ * machine-readable envelope. Receipt recording is deliberately best-effort:
120
+ * recordServed never throws, so telemetry can never cost a delivery. */
121
+ function deliveredContext(root, target, envelope, sessionId) {
122
+ // Validate before recording: if a future envelope change drifts from the
123
+ // advertised MCP contract, the SDK will reject the call and the local ledger
124
+ // must not claim that response was served.
125
+ const structuredContent = DELIVERY_OUTPUT_SCHEMA.parse(envelope);
126
+ recordServed(root, structuredContent.delivered.map((item) => ({
127
+ event: "served",
128
+ kind: item.kind,
129
+ record_id: item.record_id,
130
+ target,
131
+ session_id: sessionId,
132
+ rank: item.rank,
133
+ delivery_reason: item.delivery_reason,
134
+ provenance_status: item.provenance_status,
135
+ token_cost: item.token_cost,
136
+ })));
137
+ return {
138
+ content: [{ type: "text", text: structuredContent.text }],
139
+ structuredContent,
140
+ };
141
+ }
85
142
  // Capture-session tokens live in src/core/capturetoken.ts (pure + testable). These
86
143
  // thin wrappers bind the process clock and id source at the call site (§5 Stage 1).
87
144
  const issueCaptureToken = () => issueToken(randomUUID, Date.now());
@@ -589,11 +646,19 @@ export function buildServerWithRootControl(initialRoot) {
589
646
  budget_tokens: z.number().optional().describe("Rough token budget for the brief (default 1500)."),
590
647
  as_of: z.string().optional().describe("Time-travel ref (commit/tag/branch): assemble the slice as it stood then."),
591
648
  },
592
- }, async ({ target, budget_tokens, as_of }) => {
649
+ outputSchema: DELIVERY_OUTPUT_SCHEMA,
650
+ }, async ({ target, budget_tokens, as_of }, extra) => {
593
651
  const asOf = as_of ? asOfDate(as_of, root) : undefined;
594
652
  if (as_of && !asOf)
595
653
  return err(`Could not resolve as_of "${as_of}" to a commit.`);
596
654
  const ctx = store.assembleContext(target, budget_tokens ?? 1500, { asOf });
655
+ const options = {
656
+ root,
657
+ symbols: store.recs("symbols"),
658
+ components: store.recs("components"),
659
+ decisionCorpus: store.recs("decisions"),
660
+ historical: !!asOf,
661
+ };
597
662
  // Task-phrase input ("improve retrieval ranking") resolves no file/symbol and
598
663
  // used to return an empty brief while the graph held the answer — fall back to
599
664
  // FTS so the assistant always leaves with the closest matches, not a shrug.
@@ -601,17 +666,30 @@ export function buildServerWithRootControl(initialRoot) {
601
666
  if (empty && !asOf) {
602
667
  const hits = store.search(target, 8);
603
668
  if (hits.length) {
604
- const lines = hits.map((h) => `• ${h.ref} ${h.title}\n ${h.snippet}`);
605
- return ok(`No file/symbol resolves for "${target}" — closest graph matches instead:\n\n${lines.join("\n")}\n\n(For a file/symbol brief pass a concrete target; free-text goes through the same search as hunch_query.)`);
669
+ const resolved = hits.map((hit) => ({ hit, record: store.resolve(hit.ref)?.record }));
670
+ const fallback = {
671
+ ...ctx,
672
+ constraints: resolved.filter(({ hit, record }) => hit.kind === "constraints" && !!record).map(({ record }) => record),
673
+ decisions: resolved.filter(({ hit, record }) => hit.kind === "decisions" && !!record).map(({ record }) => record),
674
+ bugs: resolved.filter(({ hit, record }) => hit.kind === "bugs" && !!record).map(({ record }) => record),
675
+ findings: resolved.filter(({ hit, record }) => hit.kind === "findings" && !!record).map(({ record }) => record),
676
+ };
677
+ const envelope = buildDeliveryEnvelope(fallback, {
678
+ ...options,
679
+ supplements: hits
680
+ .filter((hit) => !["constraints", "decisions", "bugs", "findings"].includes(hit.kind))
681
+ .map((hit, index) => ({
682
+ id: hit.ref,
683
+ kind: `search-${hit.kind}`,
684
+ text: `${hit.ref} — ${hit.title}: ${hit.snippet}`,
685
+ priority: 100 - index,
686
+ })),
687
+ });
688
+ return deliveredContext(root, target, envelope, extra.sessionId);
606
689
  }
607
690
  }
608
- return ok(formatContext(ctx, {
609
- root,
610
- symbols: store.recs("symbols"),
611
- components: store.recs("components"),
612
- decisionCorpus: store.recs("decisions"),
613
- historical: !!asOf,
614
- }));
691
+ const envelope = buildDeliveryEnvelope(ctx, options);
692
+ return deliveredContext(root, as_of ? `${target} (as_of:${as_of})` : target, envelope, extra.sessionId);
615
693
  });
616
694
  // -- hunch_now (the hot view: recent activity + roadmap) --------------------
617
695
  // PUBLIC store only, per dec_29eff08c69's jurisdiction rule: an assistant may
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.13.0",
3
+ "version": "1.13.1",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
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.13.0",
10
+ "version": "1.13.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.13.0",
16
+ "version": "1.13.1",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {