@davesheffer/hunch 1.45.1 → 1.46.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.
Files changed (64) hide show
  1. package/dist/cli/index.js +165 -33
  2. package/dist/cli/invocation.d.ts +14 -0
  3. package/dist/cli/invocation.js +23 -0
  4. package/dist/cli/serve.d.ts +2 -0
  5. package/dist/cli/serve.js +2 -1
  6. package/dist/cli/verify.d.ts +28 -0
  7. package/dist/cli/verify.js +265 -0
  8. package/dist/constitution/experiment.d.ts +1 -1
  9. package/dist/core/agenthook.d.ts +1 -0
  10. package/dist/core/agenthook.js +82 -26
  11. package/dist/core/boundedSpawn.d.ts +57 -0
  12. package/dist/core/boundedSpawn.js +321 -0
  13. package/dist/core/config.d.ts +6 -0
  14. package/dist/core/config.js +1 -0
  15. package/dist/core/delivery.d.ts +10 -0
  16. package/dist/core/delivery.js +117 -9
  17. package/dist/core/migrate.d.ts +10 -1
  18. package/dist/core/migrate.js +17 -6
  19. package/dist/core/pipeline.d.ts +9 -2
  20. package/dist/core/pipeline.js +13 -3
  21. package/dist/core/procstart.d.ts +17 -3
  22. package/dist/core/procstart.js +26 -5
  23. package/dist/core/projectDna.js +11 -1
  24. package/dist/core/relativeImports.js +6 -2
  25. package/dist/core/taskSelection.d.ts +1 -0
  26. package/dist/core/taskSelection.js +132 -11
  27. package/dist/core/testSelection.d.ts +122 -0
  28. package/dist/core/testSelection.js +588 -0
  29. package/dist/core/whyRecord.d.ts +47 -0
  30. package/dist/core/whyRecord.js +105 -0
  31. package/dist/extractors/git.d.ts +32 -0
  32. package/dist/extractors/git.js +206 -94
  33. package/dist/extractors/indexer.js +42 -6
  34. package/dist/extractors/languages.d.ts +9 -0
  35. package/dist/extractors/languages.js +28 -0
  36. package/dist/extractors/parse.d.ts +20 -0
  37. package/dist/extractors/parse.js +81 -5
  38. package/dist/extractors/workspaces.js +3 -1
  39. package/dist/integrations/health.d.ts +6 -0
  40. package/dist/integrations/health.js +33 -12
  41. package/dist/integrations/hooks.d.ts +4 -2
  42. package/dist/integrations/hooks.js +12 -2
  43. package/dist/integrations/mergeDriver.js +5 -3
  44. package/dist/integrations/optout.d.ts +3 -0
  45. package/dist/integrations/optout.js +17 -0
  46. package/dist/integrations/providers.d.ts +6 -2
  47. package/dist/integrations/providers.js +19 -2
  48. package/dist/integrations/team.js +6 -4
  49. package/dist/integrations/uninstall.d.ts +17 -4
  50. package/dist/integrations/uninstall.js +35 -13
  51. package/dist/mcp/server.js +179 -30
  52. package/dist/serve/app.js +9 -1
  53. package/dist/serve/config.js +5 -0
  54. package/dist/serve/writelock.d.ts +6 -5
  55. package/dist/serve/writelock.js +26 -21
  56. package/dist/store/hunchStore.d.ts +12 -1
  57. package/dist/store/hunchStore.js +86 -4
  58. package/dist/store/jsonStore.d.ts +24 -4
  59. package/dist/store/jsonStore.js +75 -26
  60. package/dist/store/stateBinding.js +17 -4
  61. package/dist/synthesis/provider.js +26 -78
  62. package/dist/synthesis/synthesize.js +1 -1
  63. package/package.json +2 -2
  64. package/server.json +2 -2
@@ -23,11 +23,11 @@
23
23
  * Every provider returns the same shape so the rest of the system never knows
24
24
  * (or cares) which one ran.
25
25
  */
26
- import { execFile, spawn } from "node:child_process";
27
26
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
28
27
  import { isIP } from "node:net";
29
28
  import { tmpdir } from "node:os";
30
29
  import { dirname, join } from "node:path";
30
+ import { boundedSpawn } from "../core/boundedSpawn.js";
31
31
  import { writeFileAtomic } from "../core/io.js";
32
32
  import { summarizeDiff } from "../extractors/diff.js";
33
33
  import { languageFor } from "../extractors/languages.js";
@@ -45,84 +45,32 @@ const IS_WIN = process.platform === "win32";
45
45
  * win32 routes through cmd.exe so the shim resolves. Safe here because
46
46
  * every argv we pass is a trusted, space-free flag (the prompt is stdin).
47
47
  */
48
- export function pexecIn(cmd, args, opts = {}) {
49
- return new Promise((resolve, reject) => {
50
- // Windows: launch through cmd.exe so the `claude` .cmd/.ps1 shim resolves,
51
- // and pass the whole line as ONE shell string (args are trusted, space-free
52
- // flags) — avoids Node's DEP0190 warning for `args + shell:true`. The prompt
53
- // is never here; it goes via stdin below. POSIX: no shell, argv as-is.
54
- const child = IS_WIN
55
- ? spawn([cmd, ...args].join(" "), {
56
- shell: true,
57
- env: initiatorChildEnv(opts.env ?? process.env),
58
- cwd: opts.cwd,
59
- windowsHide: true,
60
- })
61
- : spawn(cmd, args, {
62
- env: initiatorChildEnv(opts.env ?? process.env),
63
- cwd: opts.cwd,
64
- windowsHide: true,
65
- });
66
- const max = opts.maxBuffer ?? 16 * 1024 * 1024;
67
- let out = "";
68
- let err = "";
69
- let outLen = 0;
70
- let settled = false;
71
- // Windows spawns through a cmd.exe wrapper (shell:true), and child.kill()
72
- // terminates only that wrapper — the actual agent CLI survives as an orphan,
73
- // still burning the user's subscription after every timeout, accumulating
74
- // with each post-commit hook fire (issue #44). taskkill /T fells the tree.
75
- // Best-effort by design: the wrapper kill below still runs either way.
76
- const killTree = () => {
77
- if (IS_WIN && child.pid) {
78
- try {
79
- execFile("taskkill", ["/pid", String(child.pid), "/T", "/F"], { windowsHide: true }, () => { });
80
- }
81
- catch { /* taskkill unavailable — fall through to the wrapper kill */ }
82
- }
83
- child.kill();
84
- };
85
- const done = (fn) => {
86
- if (settled)
87
- return;
88
- settled = true;
89
- if (timer)
90
- clearTimeout(timer);
91
- fn();
92
- };
93
- const timer = opts.timeout
94
- ? setTimeout(() => {
95
- killTree();
96
- done(() => reject(new Error(`"${cmd}" timed out after ${opts.timeout}ms`)));
97
- }, opts.timeout)
98
- : null;
99
- child.on("error", (e) => done(() => reject(e)));
100
- child.stdout.on("data", (d) => {
101
- outLen += d.length;
102
- if (outLen > max) {
103
- killTree();
104
- done(() => reject(new Error(`"${cmd}" exceeded maxBuffer (${max} bytes)`)));
105
- return;
106
- }
107
- out += d.toString();
108
- });
109
- child.stderr.on("data", (d) => {
110
- err += d.toString();
111
- });
112
- child.on("close", (code) => {
113
- done(() => {
114
- if (code === 0)
115
- resolve({ stdout: out });
116
- else
117
- reject(new Error(`"${cmd}" exited ${code}: ${err.slice(0, 300)}`));
118
- });
119
- });
120
- // Feed stdin (the prompt) then close it; commands with no input just get EOF.
121
- if (opts.input != null)
122
- child.stdin.write(opts.input);
123
- child.stdin.on("error", () => { }); // ignore EPIPE if the child exits early
124
- child.stdin.end();
48
+ export async function pexecIn(cmd, args, opts = {}) {
49
+ // Windows: launch through cmd.exe so the `claude` .cmd/.ps1 shim resolves,
50
+ // and pass the whole line as ONE shell string (args are trusted, space-free
51
+ // flags) — avoids Node's DEP0190 warning for `args + shell:true`. The prompt
52
+ // is never here; it goes via stdin. POSIX: no shell, argv as-is.
53
+ // boundedSpawn owns the timer and, on timeout or overflow, kills the WHOLE
54
+ // tree while the root is alive: on Windows child.kill() alone ends only the
55
+ // cmd.exe wrapper and orphans the agent CLI, still burning the user's
56
+ // subscription after every timeout (issues #44, #489).
57
+ const result = await boundedSpawn(IS_WIN ? [cmd, ...args].join(" ") : cmd, IS_WIN ? [] : args, {
58
+ shell: IS_WIN,
59
+ env: initiatorChildEnv(opts.env ?? process.env),
60
+ cwd: opts.cwd,
61
+ input: opts.input,
62
+ timeoutMs: opts.timeout,
63
+ maxBuffer: opts.maxBuffer ?? 16 * 1024 * 1024,
125
64
  });
65
+ if (result.error)
66
+ throw result.error;
67
+ if (result.timedOut)
68
+ throw new Error(`"${cmd}" timed out after ${opts.timeout}ms`);
69
+ if (result.overflowed)
70
+ throw new Error(`"${cmd}" exceeded maxBuffer (${opts.maxBuffer ?? 16 * 1024 * 1024} bytes)`);
71
+ if (result.status !== 0)
72
+ throw new Error(`"${cmd}" exited ${result.status}: ${result.stderr.slice(0, 300)}`);
73
+ return { stdout: result.stdout };
126
74
  }
127
75
  /** Every selectable synthesis mode. `auto` is a preference value rather than a
128
76
  * provider: it resolves the invocation's origin without choosing by availability.
@@ -12,7 +12,7 @@ import { isSubstantive } from "../extractors/languages.js";
12
12
  const SKIP_SUBJECT = /^(merge|revert|bump|format|lint|wip)\b|^chore\(deps\):/i;
13
13
  // Below this many changed code lines, a commit with no structural change and no
14
14
  // explanatory body isn't worth a paid LLM call. Tunable via HUNCH_SIG_MIN_LINES.
15
- const SIG_MIN_LINES = Number(process.env.HUNCH_SIG_MIN_LINES) || 12;
15
+ const SIG_MIN_LINES = ((raw) => Number.isFinite(raw) && raw > 0 ? raw : 12)(Number(process.env.HUNCH_SIG_MIN_LINES));
16
16
  const SIG_MIN_BODY = 40;
17
17
  /** Trivial-subject commits (merge/revert/bump/format/...) are noise UNLESS the body
18
18
  * carries real content — a squash/PR description often lands there, not on the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.45.1",
3
+ "version": "1.46.0",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
@@ -103,7 +103,7 @@
103
103
  "tree-sitter-php": "0.23.12",
104
104
  "tree-sitter-python": "0.23.4",
105
105
  "tree-sitter-typescript": "^0.23.2",
106
- "zod": "^4.4.3"
106
+ "zod": "^4.6.6"
107
107
  },
108
108
  "devDependencies": {
109
109
  "@types/cross-spawn": "^6.0.6",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://www.hunchmemory.com",
10
- "version": "1.45.1",
10
+ "version": "1.46.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.45.1",
16
+ "version": "1.46.0",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {