@davesheffer/hunch 1.23.1 → 1.23.3

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
@@ -46,7 +46,37 @@ Reload your coding assistant, then ask a normal question:
46
46
 
47
47
  `hunch init` indexes the repository, installs local lifecycle hooks, and connects supported
48
48
  assistants without replacing their existing configuration. The next session receives the relevant
49
- story with its sources, not a giant transcript or a generic prompt wall.
49
+ story with its sources, not a giant transcript or a generic prompt wall. Lifecycle coverage
50
+ depends on the harness; MCP connectivity alone does not establish automatic grounding or enforcement.
51
+
52
+ Check the repository's integrations after upgrading Hunch or switching assistants:
53
+
54
+ ```sh
55
+ hunch integrations check
56
+ hunch integrations repair-pins
57
+ hunch integrations check --harness claude --probe --require mcp
58
+ hunch integrations check --harness codex --require context,edit-blocking
59
+ ```
60
+
61
+ `check` exits nonzero on stale pins, malformed configuration, or missing expected hooks.
62
+ `repair-pins` aligns existing exact npm pins with the exact Hunch dependency in `package.json`
63
+ (or the running Hunch version when no dependency is declared). It preserves other settings,
64
+ refuses ambiguous/custom TOML and malformed files, and does not enable enforcement.
65
+ Reconnect active MCP sessions after repairing pins.
66
+
67
+ Capabilities are reported as **verified**, **advisory-only**, **unsupported**, or **untested**.
68
+ `--require` fails unless every named capability is verified. The Codex example currently fails:
69
+ Hunch's Codex integration supplies MCP and instructions, with no native lifecycle adapter.
70
+ The opt-in `--probe` starts the selected generated npm launcher, checks the server version,
71
+ and reads memory; it may download the pinned package. It verifies a fresh MCP process only,
72
+ not the existing host session, hook delivery, or whether a model follows the memory.
73
+ Custom launchers and environment overrides require host-side verification.
74
+
75
+ `doctor` includes this report, and session hooks with a context channel surface configuration
76
+ problems. Checks cover repository-local configurations; global/managed overrides remain
77
+ outside this inspection. Use `hunch integrations check` in CI to prevent pin drift; add
78
+ `--require` for capabilities your workflow cannot operate without. Hook failure remains
79
+ non-blocking; this explicit CI/preflight gate fails closed on unmet requirements.
50
80
 
51
81
  ## One evidence loop, not another model
52
82
 
@@ -181,7 +211,7 @@ does not host that repository. Give teammates and CI normal Git access, keep cre
181
211
  the Git credential helper, and have one maintainer connect it:
182
212
 
183
213
  ```bash
184
- npm i -g @davesheffer/hunch@1.23.1
214
+ npm i -g @davesheffer/hunch@1.23.3
185
215
  hunch shared --repo git@github.com:acme/project-hunch-memory.git
186
216
  git add .gitignore .hunch/team.json
187
217
  git commit -m "chore: connect shared Hunch memory"
@@ -191,7 +221,7 @@ git push
191
221
  Teammates then install the same version and run:
192
222
 
193
223
  ```bash
194
- npm i -g @davesheffer/hunch@1.23.1
224
+ npm i -g @davesheffer/hunch@1.23.3
195
225
  git pull
196
226
  hunch init
197
227
  hunch doctor
@@ -223,8 +253,8 @@ Use `hunch firmness off` to pause hook enforcement without deleting history. Use
223
253
 
224
254
  ## Learn more
225
255
 
226
- - [Full documentation](https://hunch-pi.vercel.app/docs)
227
- - [Copy-paste cookbook](https://hunch-pi.vercel.app/cookbook)
256
+ - [Full documentation](https://www.hunchmemory.com/docs)
257
+ - [Copy-paste cookbook](https://www.hunchmemory.com/cookbook)
228
258
  - [Project DNA](docs/project-dna.md)
229
259
  - [Native change proof](docs/change-proof.md)
230
260
  - [Engineering Landscape Graph](docs/engineering-landscape.md)
package/dist/cli/index.js CHANGED
@@ -24,6 +24,8 @@ import { hunchPaths, hunchPathsForDir, findRoot, toPosixTarget } from "../core/p
24
24
  import { writeFileAtomic } from "../core/io.js";
25
25
  import { looksLikeCorrection, CORRECTION_NUDGE } from "../core/correction.js";
26
26
  import { HUNCH_VERSION } from "../core/version.js";
27
+ import { registerIntegrationCommands } from "./integrations.js";
28
+ import { inspectIntegrations, formatIntegrationHealth, integrationHealthFails, integrationSessionWarning } from "../integrations/health.js";
27
29
  import { HunchStore } from "../store/hunchStore.js";
28
30
  import { JsonStore } from "../store/jsonStore.js";
29
31
  import { selectEmbedder } from "../store/embedder.js";
@@ -109,6 +111,7 @@ import { repairDecisionReference } from "../core/refrepair.js";
109
111
  import { resolveInvocation, dim, synthesisStatusLines, maybeWarnOllamaContext } from "./invocation.js";
110
112
  const program = new Command();
111
113
  program.name("hunch").description("Hunch — engineering memory and a deterministic Change Gate for AI-assisted codebases.").version(HUNCH_VERSION);
114
+ registerIntegrationCommands(program);
112
115
  let openStore = null;
113
116
  function openTeamStore(root, opts = {}) {
114
117
  // A committed team.json is an explicit declaration that this checkout belongs
@@ -338,6 +341,7 @@ program
338
341
  console.log(` ✓ linked worktree — sharing the repo's hooks + memory (no separate setup needed)`);
339
342
  }
340
343
  store.close();
344
+ console.log("\n" + formatIntegrationHealth(inspectIntegrations(root)));
341
345
  console.log("\nNext: make a commit (the hook captures a decision), then ask your coding assistant \"why is X built this way?\"");
342
346
  console.log("Cold start? Seed from history: hunch backfill --since 90d");
343
347
  console.log("\n⭐ If Hunch earns its keep, a star helps others find it → https://github.com/davesheffer/hunch");
@@ -5901,6 +5905,12 @@ program
5901
5905
  .command("doctor")
5902
5906
  .description("Diagnose the environment (git, synthesis provider, index freshness).")
5903
5907
  .action(async () => {
5908
+ const integrations = inspectIntegrations(findRoot());
5909
+ console.log(formatIntegrationHealth(integrations));
5910
+ // A shared-memory or CLI-only checkout may intentionally have no local
5911
+ // assistant config. The explicit integrations check still fails that case.
5912
+ if (integrations.harnesses.length > 0 && integrationHealthFails(integrations))
5913
+ process.exitCode = 1;
5904
5914
  const { store, root } = storeFor();
5905
5915
  console.log(`Hunch root: ${root}`);
5906
5916
  console.log(`git repo: ${isGitRepo(root) ? "yes" : "no"} ${isGitRepo(root) ? `(HEAD ${headSha(root).slice(0, 8)})` : ""}`);
@@ -6069,6 +6079,11 @@ function toRepoRel(root, abs) {
6069
6079
  return relative(realpathNorm(root), realpathNorm(abs)).split("\\").join("/");
6070
6080
  }
6071
6081
  function emitContext(provider, event, text) {
6082
+ if (event === "SessionStart") {
6083
+ const warning = integrationSessionWarning(findRoot(), provider);
6084
+ if (warning)
6085
+ text = `${warning}\n\n${text}`;
6086
+ }
6072
6087
  const output = contextHookOutput(provider, event, text);
6073
6088
  if (output)
6074
6089
  process.stdout.write(JSON.stringify(output));
@@ -0,0 +1,40 @@
1
+ import { findRoot } from "../core/paths.js";
2
+ import { CAPABILITIES, HARNESSES, inspectIntegrations, integrationHealthFails, formatIntegrationHealth, repairIntegrationPins } from "../integrations/health.js";
3
+ import { probeIntegration } from "../integrations/probe.js";
4
+ export function registerIntegrationCommands(program) {
5
+ const integrations = program.command("integrations").description("Check harness coverage and repair stale repository Hunch pins");
6
+ integrations.command("check")
7
+ .description("Fail on configuration drift; optionally require verified capabilities for CI")
8
+ .option("--harness <name>", "claude | codex | cursor | vscode | windsurf | antigravity")
9
+ .option("--require <capabilities>", "comma-separated capabilities that must be verified")
10
+ .option("--probe", "start the selected harness's published Hunch MCP launcher and perform a memory read (may download its pinned package)")
11
+ .option("--json", "machine-readable report")
12
+ .action(async (opts) => {
13
+ if (opts.harness && !Object.hasOwn(HARNESSES, opts.harness))
14
+ throw new Error(`unknown harness: ${opts.harness}`);
15
+ if (opts.probe && !opts.harness)
16
+ throw new Error("--probe requires --harness");
17
+ const required = opts.require === undefined ? [] : opts.require.split(",").map(c => c.trim());
18
+ if (required.some(c => !CAPABILITIES.includes(c)))
19
+ throw new Error(`--require accepts ${CAPABILITIES.join(",")}`);
20
+ const root = findRoot();
21
+ const report = inspectIntegrations(root, opts.harness);
22
+ if (opts.probe)
23
+ await probeIntegration(root, opts.harness, report);
24
+ console.log(opts.json ? JSON.stringify(report, null, 2) : formatIntegrationHealth(report));
25
+ if (integrationHealthFails(report, required))
26
+ process.exitCode = 1;
27
+ });
28
+ integrations.command("repair-pins")
29
+ .description("Align existing exact-version integration pins with package.json; preserves other settings and does not enable hooks")
30
+ .action(() => {
31
+ const root = findRoot();
32
+ const files = repairIntegrationPins(root);
33
+ console.log(files.length ? `Updated ${files.length} integration file(s): ${files.join(", ")}. Reconnect active MCP sessions.` : "Integration pins already aligned.");
34
+ const report = inspectIntegrations(root);
35
+ console.log(formatIntegrationHealth(report));
36
+ if (integrationHealthFails(report))
37
+ process.exitCode = 1;
38
+ });
39
+ }
40
+ //# sourceMappingURL=integrations.js.map
@@ -0,0 +1,269 @@
1
+ /** Repository integration checks. Configuration is evidence of wiring, never
2
+ * evidence that a host delivered context or enforced a decision. */
3
+ import { existsSync, readFileSync, lstatSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { parse as parseToml } from "smol-toml";
6
+ import { parseJsonc } from "../core/jsonc.js";
7
+ import { writeFileAtomic } from "../core/io.js";
8
+ import { HUNCH_VERSION } from "../core/version.js";
9
+ import { readConfig } from "../core/config.js";
10
+ import { hunchPaths } from "../core/paths.js";
11
+ export const CAPABILITIES = ["mcp", "context", "edit-blocking", "failure-capture", "compaction"];
12
+ export const HARNESSES = {
13
+ claude: { mcp: ".mcp.json", hooks: ".claude/settings.json", key: "mcpServers", events: ["SessionStart", "PreToolUse", "PostToolUseFailure", "PreCompact"] },
14
+ codex: { mcp: ".codex/config.toml", hooks: "", key: "", events: [] },
15
+ cursor: { mcp: ".cursor/mcp.json", hooks: ".cursor/hooks.json", key: "mcpServers", events: ["sessionStart", "preToolUse", "postToolUse", ""] },
16
+ vscode: { mcp: ".vscode/mcp.json", hooks: ".github/hooks/hunch.json", key: "servers", events: ["SessionStart", "PreToolUse", "PostToolUse", ""] },
17
+ windsurf: { mcp: ".windsurf/mcp_config.json", hooks: ".windsurf/hooks.json", key: "mcpServers", events: ["", "pre_write_code", "post_run_command", ""] },
18
+ antigravity: { mcp: ".agents/mcp_config.json", hooks: ".agents/hooks.json", key: "mcpServers", events: ["PreInvocation", "PreToolUse", "", ""] },
19
+ };
20
+ const exactVersion = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
21
+ const pinPattern = /@davesheffer\/hunch@([^\s"'\],;]+)/g;
22
+ const object = (v) => {
23
+ if (!v || typeof v !== "object" || Array.isArray(v))
24
+ throw new Error("expected a configuration object");
25
+ return v;
26
+ };
27
+ function strings(value) {
28
+ if (typeof value === "string")
29
+ return [value];
30
+ if (Array.isArray(value))
31
+ return value.flatMap(strings);
32
+ if (value && typeof value === "object")
33
+ return Object.values(value).flatMap(strings);
34
+ return [];
35
+ }
36
+ function hookCommands(value) {
37
+ if (Array.isArray(value))
38
+ return value.flatMap(hookCommands);
39
+ if (!value || typeof value !== "object")
40
+ return [];
41
+ const obj = value;
42
+ if (obj.enabled === false || (obj.type !== undefined && obj.type !== "command"))
43
+ return [];
44
+ const command = typeof obj.command === "string" ? obj.command : "";
45
+ const own = /(?:@davesheffer\/hunch|[\\/]index\.(?:js|ts))/.test(command)
46
+ && /\s"?hook"?(?:\s+"?--provider"?\s+"?[a-z]+"?)?\s*$/.test(command);
47
+ return [...(own ? [command] : []), ...(obj.hooks ? hookCommands(obj.hooks) : [])];
48
+ }
49
+ /** Pin repair is restricted to Hunch's marker-owned TOML block. */
50
+ function codexBlock(raw) {
51
+ const start = "# >>> hunch mcp (managed) >>>";
52
+ const end = "# <<< hunch mcp <<<";
53
+ if (raw.split(start).length !== 2 || raw.split(end).length !== 2)
54
+ throw new Error("managed Hunch TOML block missing or duplicated; run hunch init after reviewing custom configuration");
55
+ const begin = raw.indexOf(start), finish = raw.indexOf(end);
56
+ if (finish < begin)
57
+ throw new Error("malformed managed Hunch TOML block");
58
+ const block = raw.slice(begin + start.length, finish);
59
+ if ((raw.match(/^\s*\[mcp_servers\.hunch\]/gm) ?? []).length !== 1)
60
+ throw new Error("missing or duplicate Hunch MCP table");
61
+ if (!/^\s*\[mcp_servers\.hunch\]\s*$/m.test(block))
62
+ throw new Error("Hunch table is outside its managed block");
63
+ return block;
64
+ }
65
+ export function readLauncher(root, harness) {
66
+ const spec = HARNESSES[harness];
67
+ const raw = readFileSync(join(root, spec.mcp), "utf8");
68
+ let config;
69
+ if (harness === "codex") {
70
+ config = object(object(parseToml(raw).mcp_servers).hunch);
71
+ }
72
+ else {
73
+ config = object(object(object(parseJsonc(raw))[spec.key]).hunch);
74
+ }
75
+ if (config.enabled === false || config.disabled === true)
76
+ throw new Error("Hunch MCP server is disabled");
77
+ if (typeof config.command !== "string" || !config.command.trim() || !Array.isArray(config.args) || !config.args.every(a => typeof a === "string"))
78
+ throw new Error("expected a local stdio command and string arguments");
79
+ return { command: config.command, args: config.args, customEnvironment: ["env", "env_vars", "cwd"].some(key => config[key] !== undefined) };
80
+ }
81
+ function expectedVersion(root) {
82
+ const file = join(root, "package.json");
83
+ if (!existsSync(file))
84
+ return HUNCH_VERSION;
85
+ const manifest = object(JSON.parse(readFileSync(file, "utf8")));
86
+ const declarations = [manifest.dependencies, manifest.devDependencies, manifest.optionalDependencies]
87
+ .filter(Boolean).map(object).map(deps => deps["@davesheffer/hunch"]).filter(v => v !== undefined);
88
+ if (new Set(declarations).size > 1)
89
+ throw new Error("conflicting Hunch dependency versions");
90
+ const version = declarations[0] ?? (manifest.name === "@davesheffer/hunch" ? manifest.version : HUNCH_VERSION);
91
+ if (typeof version !== "string" || !exactVersion.test(version))
92
+ throw new Error("pin @davesheffer/hunch to an exact version in package.json before checking integrations");
93
+ return version;
94
+ }
95
+ export function inspectIntegrations(root, selected) {
96
+ const report = { schema: "hunch.integration-health/1", expectedVersion: HUNCH_VERSION, scope: "repository-config", issues: [], harnesses: [] };
97
+ try {
98
+ report.expectedVersion = expectedVersion(root);
99
+ }
100
+ catch (e) {
101
+ report.issues.push({ file: "package.json", code: "dependency-version", detail: e.message });
102
+ }
103
+ const firmness = readConfig(hunchPaths(root)).firmness;
104
+ const recordPins = (file, values) => {
105
+ for (const value of values) {
106
+ const pins = [...value.matchAll(pinPattern)];
107
+ if (value.includes("@davesheffer/hunch") && !pins.length)
108
+ report.issues.push({ file, code: "unpinned-package", detail: "Hunch npm launcher has no exact version; run hunch init with the intended version" });
109
+ for (const [, version] of pins) {
110
+ if (version !== report.expectedVersion)
111
+ report.issues.push({ file, code: "version-drift", detail: `Hunch ${version} differs from expected ${report.expectedVersion}; run hunch integrations repair-pins` });
112
+ }
113
+ }
114
+ };
115
+ for (const harness of selected ? [selected] : Object.keys(HARNESSES)) {
116
+ const spec = HARNESSES[harness];
117
+ if (!selected && !existsSync(join(root, spec.mcp)) && (!spec.hooks || !existsSync(join(root, spec.hooks))))
118
+ continue;
119
+ const capabilities = Object.fromEntries(CAPABILITIES.map(c => [c, { status: "untested", detail: "No runtime evidence" }]));
120
+ report.harnesses.push({ harness, capabilities });
121
+ try {
122
+ const launcher = readLauncher(root, harness);
123
+ recordPins(spec.mcp, [launcher.command, ...launcher.args]);
124
+ capabilities.mcp.detail = "Configured locally; use --probe to verify a fresh server, then reconnect the host";
125
+ }
126
+ catch (e) {
127
+ report.issues.push({ file: spec.mcp, code: "mcp-config", detail: e.message });
128
+ capabilities.mcp.detail = "MCP configuration missing, disabled, invalid, or outside supported inspection format";
129
+ }
130
+ let events = {};
131
+ let disabled = false;
132
+ if (spec.hooks) {
133
+ try {
134
+ const config = object(parseJsonc(readFileSync(join(root, spec.hooks), "utf8")));
135
+ disabled = config.disableAllHooks === true;
136
+ events = object(harness === "antigravity" ? config.hunch : config.hooks);
137
+ recordPins(spec.hooks, Object.values(events).flatMap(hookCommands));
138
+ }
139
+ catch (e) {
140
+ report.issues.push({ file: spec.hooks, code: "hook-config", detail: e.message });
141
+ }
142
+ }
143
+ for (const [i, capability] of ["context", "edit-blocking", "failure-capture", "compaction"].entries()) {
144
+ const event = spec.events[i];
145
+ const status = capabilities[capability];
146
+ if (!event) {
147
+ status.status = capability === "context" ? "advisory-only" : "unsupported";
148
+ status.detail = capability === "context" ? "Hunch relies on instructions and voluntary MCP calls on this adapter" : "No Hunch lifecycle adapter for this capability";
149
+ }
150
+ else if (disabled || firmness === "off" || ((capability === "failure-capture") && process.env.HUNCH_PIPELINE === "0")) {
151
+ status.status = "unsupported";
152
+ status.detail = "Disabled by local hook settings, firmness, or HUNCH_PIPELINE";
153
+ }
154
+ else if (!hookCommands(events[event]).some(command => {
155
+ const dialect = command.match(/"?--provider"?\s+"?([a-z]+)"?/i)?.[1]?.toLowerCase() ?? "claude";
156
+ return dialect === harness;
157
+ })) {
158
+ status.detail = `Missing Hunch ${event} handler`;
159
+ report.issues.push({ file: spec.hooks, code: "missing-hook", detail: status.detail });
160
+ }
161
+ else if (capability === "edit-blocking" && firmness !== "strict") {
162
+ status.status = "advisory-only";
163
+ status.detail = `firmness=${firmness}; edits are not blocked`;
164
+ }
165
+ else {
166
+ status.detail = `${event} configured; host delivery, matchers, and tool coverage are not verified`;
167
+ }
168
+ }
169
+ }
170
+ if (!report.harnesses.length)
171
+ report.issues.push({ file: ".", code: "no-integrations", detail: "No repository integrations found; global and managed host settings are not inspected" });
172
+ return report;
173
+ }
174
+ /** Repair only exact published pins. Preserve formatting and all other values.
175
+ * Preflight every affected file before writing any; reject malformed JSON/TOML. */
176
+ export function repairIntegrationPins(root) {
177
+ const version = expectedVersion(root);
178
+ const pending = [];
179
+ for (const [name, spec] of Object.entries(HARNESSES)) {
180
+ for (const file of [spec.mcp, spec.hooks].filter(Boolean)) {
181
+ const path = join(root, file);
182
+ if (!existsSync(path))
183
+ continue;
184
+ // Never follow a config symlink or symlinked parent into another project.
185
+ let current = resolve(root);
186
+ for (const part of file.split("/")) {
187
+ current = join(current, part);
188
+ if (lstatSync(current).isSymbolicLink())
189
+ throw new Error(`refusing to rewrite symlink: ${file}`);
190
+ }
191
+ const before = readFileSync(path, "utf8");
192
+ const replace = (text) => text.replace(pinPattern, (match, old) => {
193
+ if (!exactVersion.test(old))
194
+ throw new Error(`refusing non-exact Hunch pin in ${file}`);
195
+ return `@davesheffer/hunch@${version}`;
196
+ });
197
+ let after;
198
+ if (name === "codex") {
199
+ readLauncher(root, "codex");
200
+ const block = codexBlock(before);
201
+ const table = object(object(parseToml(block).mcp_servers).hunch);
202
+ if (Object.keys(table).some(key => !["command", "args"].includes(key)))
203
+ throw new Error(`custom managed settings require manual pin repair: ${file}`);
204
+ // Replace only the canonical args line, never comments or another table.
205
+ const lines = block.split("\n");
206
+ if (lines.some(line => line.trim() && !line.trim().startsWith("#") && !/^\s*(?:\[mcp_servers\.hunch\]|command\s*=|args\s*=)/.test(line)))
207
+ throw new Error(`custom managed TOML requires manual pin repair: ${file}`);
208
+ const next = lines.map(line => /^\s*args\s*=/.test(line) ? replace(line.split("#")[0]) + (line.includes("#") ? `#${line.split("#").slice(1).join("#")}` : "") : line).join("\n");
209
+ after = before.replace(block, next);
210
+ parseToml(after);
211
+ }
212
+ else {
213
+ const config = object(parseJsonc(before));
214
+ const values = file === spec.mcp
215
+ ? strings(object(object(config[spec.key]).hunch).args)
216
+ : Object.values(object(name === "antigravity" ? config.hunch : config.hooks)).flatMap(hookCommands);
217
+ const replacements = new Map(values.map(v => [v, replace(v)]).filter(([a, b]) => a !== b));
218
+ const counts = new Map();
219
+ // Tokenize comments too, so a quoted command in a comment is untouched.
220
+ after = before.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:[^"\\]|\\.)*"/g, token => {
221
+ if (!token.startsWith('"'))
222
+ return token;
223
+ const value = JSON.parse(token);
224
+ const replacement = replacements.get(value);
225
+ if (replacement !== undefined)
226
+ counts.set(value, (counts.get(value) ?? 0) + 1);
227
+ return replacement !== undefined && replacement !== value ? JSON.stringify(replacement) : token;
228
+ });
229
+ for (const [value, count] of counts) {
230
+ if (count !== values.filter(v => v === value).length)
231
+ throw new Error(`ambiguous Hunch string also appears outside managed settings in ${file}; refusing repair`);
232
+ }
233
+ }
234
+ if (before !== after)
235
+ pending.push({ file, before, after });
236
+ }
237
+ }
238
+ for (const { file, before } of pending)
239
+ if (readFileSync(join(root, file), "utf8") !== before)
240
+ throw new Error(`${file} changed during repair; retry`);
241
+ for (const { file, after } of pending)
242
+ writeFileAtomic(join(root, file), after);
243
+ return pending.map(p => p.file);
244
+ }
245
+ export function integrationHealthFails(report, required = []) {
246
+ return report.issues.length > 0 || report.harnesses.some(h => required.some(c => h.capabilities[c].status !== "verified"));
247
+ }
248
+ export function formatIntegrationHealth(report) {
249
+ return [
250
+ `Hunch integrations — expected ${report.expectedVersion} (repository configuration only)`,
251
+ ...report.harnesses.map(h => `${h.harness}:\n${CAPABILITIES.map(c => ` ${c}: ${h.capabilities[c].status} — ${h.capabilities[c].detail}`).join("\n")}`),
252
+ ...report.issues.map(i => `ERROR ${i.file}: ${i.detail}`),
253
+ "Configured hooks are untested until exercised inside the host. Global settings, active sessions, and model compliance are not verified.",
254
+ ].join("\n");
255
+ }
256
+ /** Bounded session warning; diagnostics must never break hook execution. */
257
+ export function integrationSessionWarning(root, harness) {
258
+ try {
259
+ const report = inspectIntegrations(root, harness);
260
+ if (!report.issues.length)
261
+ return "";
262
+ const issues = [...new Set(report.issues.map(i => `${i.file}: ${i.detail}`))];
263
+ return `Hunch integration needs attention: ${issues.slice(0, 3).join("; ").slice(0, 1200)}. Run hunch integrations check; do not assume full harness coverage.`;
264
+ }
265
+ catch {
266
+ return "";
267
+ }
268
+ }
269
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1,52 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { HARNESSES, readLauncher } from "./health.js";
4
+ /** Opt-in: starts a fresh configured MCP process, never claims the current
5
+ * harness connection or hook delivery has been tested. No model is invoked. */
6
+ export async function probeIntegration(root, harness, report, timeoutMs = 15_000) {
7
+ const health = report.harnesses.find(h => h.harness === harness);
8
+ if (!health)
9
+ throw new Error(`missing ${harness} inspection`);
10
+ const client = new Client({ name: "hunch-integration-probe", version: "1" });
11
+ let transport;
12
+ let timer;
13
+ try {
14
+ const launcher = readLauncher(root, harness);
15
+ if (launcher.customEnvironment)
16
+ throw new Error("custom MCP environment or working directory requires a host-side probe; no settings were ignored");
17
+ // Only execute the exact published launcher Hunch generates. Custom
18
+ // commands may need credentials or have unrelated effects; inspect only.
19
+ if (!/^npx(?:\.cmd)?$/.test(launcher.command) || JSON.stringify(launcher.args) !== JSON.stringify([
20
+ "-y", `--package=hunch-exact@npm:@davesheffer/hunch@${report.expectedVersion}`, "hunch", "mcp",
21
+ ]))
22
+ throw new Error("probe requires the generated exact-version npx launcher; repair stale pins first; custom launchers remain untested");
23
+ transport = new StdioClientTransport({ ...launcher, cwd: root, stderr: "pipe" });
24
+ transport.stderr?.on("data", () => { });
25
+ await Promise.race([
26
+ (async () => {
27
+ await client.connect(transport);
28
+ const server = client.getServerVersion();
29
+ if (server?.name !== "hunch" || server.version !== report.expectedVersion)
30
+ throw new Error("MCP server identity/version does not match the repository dependency");
31
+ const { tools } = await client.listTools();
32
+ if (!tools.some(t => t.name === "hunch_context") || !tools.some(t => t.name === "hunch_structure"))
33
+ throw new Error("required Hunch memory tools are missing");
34
+ const result = await client.callTool({ name: "hunch_structure", arguments: {} });
35
+ if (result.isError || !Array.isArray(result.content) || !result.content.some(c => c.type === "text" && c.text))
36
+ throw new Error("Hunch memory read failed");
37
+ })(),
38
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("MCP probe timed out")), timeoutMs); }),
39
+ ]);
40
+ health.capabilities.mcp = { status: "verified", detail: `Fresh MCP process reports ${report.expectedVersion}; memory tool read succeeded. Existing host sessions were not probed` };
41
+ }
42
+ catch (e) {
43
+ report.issues.push({ file: HARNESSES[harness].mcp, code: "mcp-probe", detail: e.message });
44
+ }
45
+ finally {
46
+ if (timer)
47
+ clearTimeout(timer);
48
+ await client.close().catch(() => { });
49
+ await transport?.close().catch(() => { });
50
+ }
51
+ }
52
+ //# sourceMappingURL=probe.js.map
@@ -596,7 +596,7 @@ export async function probeOllamaNumCtx(baseUrl, model) {
596
596
  return null;
597
597
  if (/^num_ctx\s+\d+/m.test(body.parameters))
598
598
  return null; // already configured — nothing to warn about
599
- return "⚠ This Ollama model does not pin num_ctx; its effective context depends on server/VRAM defaults. For stable large-diff synthesis, see https://hunch-pi.vercel.app/cookbook and pin num_ctx via a custom Modelfile.";
599
+ return "⚠ This Ollama model does not pin num_ctx; its effective context depends on server/VRAM defaults. For stable large-diff synthesis, see https://www.hunchmemory.com/cookbook and pin num_ctx via a custom Modelfile.";
600
600
  }
601
601
  catch {
602
602
  return null; // not Ollama, unreachable, or an unexpected response shape — advisory only, never throw
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.23.1",
3
+ "version": "1.23.3",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
7
7
  "description": "Engineering memory and a deterministic Change Gate for AI-assisted codebases: decisions, rejected approaches, constraints, and bug lineage become portable context and opt-in enforcement for every MCP assistant.",
8
- "homepage": "https://hunch-pi.vercel.app",
8
+ "homepage": "https://www.hunchmemory.com",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/davesheffer/hunch.git"
@@ -83,6 +83,7 @@
83
83
  "rehearse:constitution": "npm run build && node tooling/constitution-clean-rehearsal.mjs",
84
84
  "gate:release": "node tooling/release-gate.mjs",
85
85
  "site:proof": "npm run build && node tooling/generate-public-proof.mjs",
86
+ "site:sitemap": "node tooling/generate-sitemap.mjs",
86
87
  "bench:md1": "node tooling/md1-benchmark.mjs",
87
88
  "research:competitors": "node tooling/competitive-watch.mjs",
88
89
  "outreach": "node tooling/outreach-pipeline.mjs",
@@ -92,6 +93,7 @@
92
93
  "@modelcontextprotocol/sdk": "^1.30.0",
93
94
  "@tree-sitter-grammars/tree-sitter-yaml": "^0.6.1",
94
95
  "commander": "^15.0.0",
96
+ "smol-toml": "1.8.0",
95
97
  "tree-sitter": "0.21.1",
96
98
  "tree-sitter-go": "^0.23.4",
97
99
  "tree-sitter-php": "0.23.12",
package/server.json CHANGED
@@ -6,14 +6,14 @@
6
6
  "url": "https://github.com/davesheffer/hunch",
7
7
  "source": "github"
8
8
  },
9
- "websiteUrl": "https://hunch-pi.vercel.app",
10
- "version": "1.23.1",
9
+ "websiteUrl": "https://www.hunchmemory.com",
10
+ "version": "1.23.3",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.23.1",
16
+ "version": "1.23.3",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {