@davesheffer/hunch 1.23.2 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -3
- package/dist/cli/index.js +17 -0
- package/dist/cli/integrations.js +40 -0
- package/dist/cli/update.js +72 -0
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/health.js +269 -0
- package/dist/integrations/probe.js +52 -0
- package/package.json +3 -2
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -46,7 +46,55 @@ 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
|
+
To update Hunch and all configured harness pins for the current repository:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
hunch update
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Agents receive an instruction to run this when you ask **“update Hunch”** in the generated
|
|
59
|
+
Hunch guidance. The command resolves npm's latest release, updates an existing dependency
|
|
60
|
+
to an exact version in its current dependency section, then runs the new version's pin
|
|
61
|
+
repair and integration check. Without a repository dependency it updates the global CLI.
|
|
62
|
+
Use `--global` to also update the global CLI when a local dependency exists, or `--dry-run`
|
|
63
|
+
to preview the commands. Restart active harnesses afterward. This applies to the current
|
|
64
|
+
repository, not every project on your machine. Automatic dependency installation currently
|
|
65
|
+
supports standalone npm projects; other package managers should update their dependency
|
|
66
|
+
explicitly and then use `hunch integrations repair-pins`. Existing hook settings are preserved.
|
|
67
|
+
An installation or check failure stops the command with a nonzero exit; completed npm
|
|
68
|
+
changes are not rolled back. Fix the reported issue and rerun the command.
|
|
69
|
+
|
|
70
|
+
Check the repository's integrations after upgrading Hunch or switching assistants:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
hunch integrations check
|
|
74
|
+
hunch integrations repair-pins
|
|
75
|
+
hunch integrations check --harness claude --probe --require mcp
|
|
76
|
+
hunch integrations check --harness codex --require context,edit-blocking
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`check` exits nonzero on stale pins, malformed configuration, or missing expected hooks.
|
|
80
|
+
`repair-pins` aligns existing exact npm pins with the exact Hunch dependency in `package.json`
|
|
81
|
+
(or the running Hunch version when no dependency is declared). It preserves other settings,
|
|
82
|
+
refuses ambiguous/custom TOML and malformed files, and does not enable enforcement.
|
|
83
|
+
Reconnect active MCP sessions after repairing pins.
|
|
84
|
+
|
|
85
|
+
Capabilities are reported as **verified**, **advisory-only**, **unsupported**, or **untested**.
|
|
86
|
+
`--require` fails unless every named capability is verified. The Codex example currently fails:
|
|
87
|
+
Hunch's Codex integration supplies MCP and instructions, with no native lifecycle adapter.
|
|
88
|
+
The opt-in `--probe` starts the selected generated npm launcher, checks the server version,
|
|
89
|
+
and reads memory; it may download the pinned package. It verifies a fresh MCP process only,
|
|
90
|
+
not the existing host session, hook delivery, or whether a model follows the memory.
|
|
91
|
+
Custom launchers and environment overrides require host-side verification.
|
|
92
|
+
|
|
93
|
+
`doctor` includes this report, and session hooks with a context channel surface configuration
|
|
94
|
+
problems. Checks cover repository-local configurations; global/managed overrides remain
|
|
95
|
+
outside this inspection. Use `hunch integrations check` in CI to prevent pin drift; add
|
|
96
|
+
`--require` for capabilities your workflow cannot operate without. Hook failure remains
|
|
97
|
+
non-blocking; this explicit CI/preflight gate fails closed on unmet requirements.
|
|
50
98
|
|
|
51
99
|
## One evidence loop, not another model
|
|
52
100
|
|
|
@@ -181,7 +229,7 @@ does not host that repository. Give teammates and CI normal Git access, keep cre
|
|
|
181
229
|
the Git credential helper, and have one maintainer connect it:
|
|
182
230
|
|
|
183
231
|
```bash
|
|
184
|
-
npm i -g @davesheffer/hunch@1.23.
|
|
232
|
+
npm i -g @davesheffer/hunch@1.23.3
|
|
185
233
|
hunch shared --repo git@github.com:acme/project-hunch-memory.git
|
|
186
234
|
git add .gitignore .hunch/team.json
|
|
187
235
|
git commit -m "chore: connect shared Hunch memory"
|
|
@@ -191,7 +239,7 @@ git push
|
|
|
191
239
|
Teammates then install the same version and run:
|
|
192
240
|
|
|
193
241
|
```bash
|
|
194
|
-
npm i -g @davesheffer/hunch@1.23.
|
|
242
|
+
npm i -g @davesheffer/hunch@1.23.3
|
|
195
243
|
git pull
|
|
196
244
|
hunch init
|
|
197
245
|
hunch doctor
|
package/dist/cli/index.js
CHANGED
|
@@ -24,6 +24,9 @@ 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 { registerUpdateCommand } from "./update.js";
|
|
29
|
+
import { inspectIntegrations, formatIntegrationHealth, integrationHealthFails, integrationSessionWarning } from "../integrations/health.js";
|
|
27
30
|
import { HunchStore } from "../store/hunchStore.js";
|
|
28
31
|
import { JsonStore } from "../store/jsonStore.js";
|
|
29
32
|
import { selectEmbedder } from "../store/embedder.js";
|
|
@@ -109,6 +112,8 @@ import { repairDecisionReference } from "../core/refrepair.js";
|
|
|
109
112
|
import { resolveInvocation, dim, synthesisStatusLines, maybeWarnOllamaContext } from "./invocation.js";
|
|
110
113
|
const program = new Command();
|
|
111
114
|
program.name("hunch").description("Hunch — engineering memory and a deterministic Change Gate for AI-assisted codebases.").version(HUNCH_VERSION);
|
|
115
|
+
registerIntegrationCommands(program);
|
|
116
|
+
registerUpdateCommand(program);
|
|
112
117
|
let openStore = null;
|
|
113
118
|
function openTeamStore(root, opts = {}) {
|
|
114
119
|
// A committed team.json is an explicit declaration that this checkout belongs
|
|
@@ -338,6 +343,7 @@ program
|
|
|
338
343
|
console.log(` ✓ linked worktree — sharing the repo's hooks + memory (no separate setup needed)`);
|
|
339
344
|
}
|
|
340
345
|
store.close();
|
|
346
|
+
console.log("\n" + formatIntegrationHealth(inspectIntegrations(root)));
|
|
341
347
|
console.log("\nNext: make a commit (the hook captures a decision), then ask your coding assistant \"why is X built this way?\"");
|
|
342
348
|
console.log("Cold start? Seed from history: hunch backfill --since 90d");
|
|
343
349
|
console.log("\n⭐ If Hunch earns its keep, a star helps others find it → https://github.com/davesheffer/hunch");
|
|
@@ -5901,6 +5907,12 @@ program
|
|
|
5901
5907
|
.command("doctor")
|
|
5902
5908
|
.description("Diagnose the environment (git, synthesis provider, index freshness).")
|
|
5903
5909
|
.action(async () => {
|
|
5910
|
+
const integrations = inspectIntegrations(findRoot());
|
|
5911
|
+
console.log(formatIntegrationHealth(integrations));
|
|
5912
|
+
// A shared-memory or CLI-only checkout may intentionally have no local
|
|
5913
|
+
// assistant config. The explicit integrations check still fails that case.
|
|
5914
|
+
if (integrations.harnesses.length > 0 && integrationHealthFails(integrations))
|
|
5915
|
+
process.exitCode = 1;
|
|
5904
5916
|
const { store, root } = storeFor();
|
|
5905
5917
|
console.log(`Hunch root: ${root}`);
|
|
5906
5918
|
console.log(`git repo: ${isGitRepo(root) ? "yes" : "no"} ${isGitRepo(root) ? `(HEAD ${headSha(root).slice(0, 8)})` : ""}`);
|
|
@@ -6069,6 +6081,11 @@ function toRepoRel(root, abs) {
|
|
|
6069
6081
|
return relative(realpathNorm(root), realpathNorm(abs)).split("\\").join("/");
|
|
6070
6082
|
}
|
|
6071
6083
|
function emitContext(provider, event, text) {
|
|
6084
|
+
if (event === "SessionStart") {
|
|
6085
|
+
const warning = integrationSessionWarning(findRoot(), provider);
|
|
6086
|
+
if (warning)
|
|
6087
|
+
text = `${warning}\n\n${text}`;
|
|
6088
|
+
}
|
|
6072
6089
|
const output = contextHookOutput(provider, event, text);
|
|
6073
6090
|
if (output)
|
|
6074
6091
|
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,72 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { findRoot } from "../core/paths.js";
|
|
5
|
+
const PACKAGE = "@davesheffer/hunch";
|
|
6
|
+
/** Arguments come only from fixed commands and a validated registry version.
|
|
7
|
+
* Windows needs the shell to resolve npm.cmd; cwd is never interpolated. */
|
|
8
|
+
export function runNpm(root, args, capture = false) {
|
|
9
|
+
if (args.some(arg => !/^[a-zA-Z0-9@/_.=+:-]+$/.test(arg)))
|
|
10
|
+
throw new Error("unsafe npm argument");
|
|
11
|
+
const windows = process.platform === "win32";
|
|
12
|
+
const result = spawnSync(windows ? `npm ${args.join(" ")}` : "npm", windows ? [] : args, {
|
|
13
|
+
cwd: root, shell: windows, windowsHide: true,
|
|
14
|
+
encoding: "utf8", stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
15
|
+
});
|
|
16
|
+
if (result.error)
|
|
17
|
+
throw result.error;
|
|
18
|
+
if (result.status !== 0)
|
|
19
|
+
throw new Error(`npm ${args.join(" ")} failed (${result.status ?? result.signal})${result.stderr ? `: ${result.stderr.trim()}` : ""}`);
|
|
20
|
+
return result.stdout ?? "";
|
|
21
|
+
}
|
|
22
|
+
/** Fresh child execution is essential: the currently running CLI still has the
|
|
23
|
+
* old modules loaded after npm replaces its installation. */
|
|
24
|
+
export function updateHunch(root, opts = {}, run = (args, capture) => runNpm(root, args, capture), log = console.log) {
|
|
25
|
+
const file = join(root, "package.json");
|
|
26
|
+
const manifest = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
|
|
27
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
|
|
28
|
+
throw new Error("package.json must contain an object");
|
|
29
|
+
if (manifest.name === PACKAGE)
|
|
30
|
+
throw new Error("Run hunch update in a consumer repository, not Hunch's own source checkout.");
|
|
31
|
+
const sections = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
32
|
+
const declared = sections.filter(section => {
|
|
33
|
+
const deps = manifest[section];
|
|
34
|
+
if (deps !== undefined && (!deps || typeof deps !== "object" || Array.isArray(deps)))
|
|
35
|
+
throw new Error(`invalid ${section} in package.json`);
|
|
36
|
+
return deps && Object.hasOwn(deps, PACKAGE);
|
|
37
|
+
});
|
|
38
|
+
if (declared.length > 1)
|
|
39
|
+
throw new Error("Hunch is declared in multiple dependency sections; resolve the duplicate before updating.");
|
|
40
|
+
if (declared.length && (manifest.workspaces || (manifest.packageManager && !/^npm@/.test(manifest.packageManager)) || ["pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"].some(name => existsSync(join(root, name))))) {
|
|
41
|
+
throw new Error("Automatic dependency updates currently support standalone npm projects. Update Hunch to an exact version with your package manager, then run hunch integrations repair-pins.");
|
|
42
|
+
}
|
|
43
|
+
const version = JSON.parse(run(["view", `${PACKAGE}@latest`, "version", "--json"], true));
|
|
44
|
+
if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version))
|
|
45
|
+
throw new Error("npm returned an invalid Hunch version");
|
|
46
|
+
const spec = `${PACKAGE}@${version}`;
|
|
47
|
+
const commands = [];
|
|
48
|
+
if (declared.length) {
|
|
49
|
+
const flag = { dependencies: "--save-prod", devDependencies: "--save-dev", optionalDependencies: "--save-optional" }[declared[0]];
|
|
50
|
+
commands.push(["install", flag, "--save-exact", spec]);
|
|
51
|
+
}
|
|
52
|
+
if (!declared.length || opts.global)
|
|
53
|
+
commands.push(["install", "--global", spec]);
|
|
54
|
+
// The alias prevents npm exec from substituting a stale local hunch binary.
|
|
55
|
+
commands.push(["exec", "--yes", `--package=hunch-exact@npm:${spec}`, "--", "hunch", "integrations", "repair-pins"]);
|
|
56
|
+
log(`${opts.dryRun ? "Preview" : "Updating"}: Hunch ${version} for ${root}`);
|
|
57
|
+
for (const args of commands) {
|
|
58
|
+
log(`npm ${args.join(" ")}`);
|
|
59
|
+
if (!opts.dryRun)
|
|
60
|
+
run(args);
|
|
61
|
+
}
|
|
62
|
+
if (!opts.dryRun)
|
|
63
|
+
log("Hunch updated; repository integration check passed. Restart or reconnect active harnesses to load the new MCP version.");
|
|
64
|
+
}
|
|
65
|
+
export function registerUpdateCommand(program) {
|
|
66
|
+
program.command("update")
|
|
67
|
+
.description("Update Hunch to latest and repair all configured harness pins in this repository")
|
|
68
|
+
.option("--global", "also update the global CLI when a repository dependency exists")
|
|
69
|
+
.option("--dry-run", "resolve latest and print commands without changing files")
|
|
70
|
+
.action((opts) => updateHunch(findRoot(), opts));
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=update.js.map
|
|
@@ -46,6 +46,7 @@ export function renderHunchSection(store, root) {
|
|
|
46
46
|
lines.push("**Consult Hunch via the `hunch_*` MCP tools — pick by MOMENT, not from memory:**");
|
|
47
47
|
lines.push("");
|
|
48
48
|
lines.push("**Orient (session/task start):**");
|
|
49
|
+
lines.push("- When the user asks to **update Hunch**, run `hunch update` from this repository root. It updates to the latest release and repairs all configured harness pins. Use `hunch update --global` to also update a global CLI alongside a repository dependency; reconnect active MCP sessions afterward.");
|
|
49
50
|
lines.push("- `hunch_context(target)` — the minimal relevant slice for what you're about to do; a task phrase falls back to the closest graph matches. **Call FIRST.**");
|
|
50
51
|
lines.push("- `hunch_structure(target?)` — the indexed shape of the repo/dir/file/symbol — orient from the graph, not grep rounds.");
|
|
51
52
|
lines.push("- `hunch_runbook(task)` — the proven steps for a recurring task, before re-deriving them.");
|
|
@@ -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.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"mcpName": "io.github.davesheffer/hunch",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"node": ">=22.13.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
|
-
"version": "node tooling/sync-version-pins.mjs && git add plugin/.mcp.json server.json",
|
|
72
|
+
"version": "node tooling/sync-version-pins.mjs && git add plugin/.mcp.json server.json .windsurf/hooks.json",
|
|
73
73
|
"sync-version-pins": "node tooling/sync-version-pins.mjs",
|
|
74
74
|
"clean": "node --input-type=commonjs -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
75
75
|
"build": "npm run clean && tsc -p tsconfig.json",
|
|
@@ -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.
|
|
10
|
+
"version": "1.24.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.
|
|
16
|
+
"version": "1.24.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|