@davesheffer/hunch 1.23.2 → 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 +33 -3
- package/dist/cli/index.js +15 -0
- package/dist/cli/integrations.js +40 -0
- package/dist/integrations/health.js +269 -0
- package/dist/integrations/probe.js +52 -0
- package/package.json +2 -1
- package/server.json +2 -2
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.
|
|
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.
|
|
224
|
+
npm i -g @davesheffer/hunch@1.23.3
|
|
195
225
|
git pull
|
|
196
226
|
hunch init
|
|
197
227
|
hunch doctor
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "1.23.
|
|
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>",
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
94
94
|
"@tree-sitter-grammars/tree-sitter-yaml": "^0.6.1",
|
|
95
95
|
"commander": "^15.0.0",
|
|
96
|
+
"smol-toml": "1.8.0",
|
|
96
97
|
"tree-sitter": "0.21.1",
|
|
97
98
|
"tree-sitter-go": "^0.23.4",
|
|
98
99
|
"tree-sitter-php": "0.23.12",
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://www.hunchmemory.com",
|
|
10
|
-
"version": "1.23.
|
|
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.
|
|
16
|
+
"version": "1.23.3",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|