@davesheffer/hunch 1.43.0 → 1.44.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 (47) hide show
  1. package/README.md +10 -0
  2. package/dist/cli/index.js +114 -19
  3. package/dist/core/changeProof.js +2 -1
  4. package/dist/core/commitrepair.d.ts +9 -3
  5. package/dist/core/commitrepair.js +23 -6
  6. package/dist/core/correction.d.ts +1 -1
  7. package/dist/core/correction.js +4 -3
  8. package/dist/core/docanchors.d.ts +11 -2
  9. package/dist/core/docanchors.js +232 -65
  10. package/dist/core/drift.d.ts +1 -1
  11. package/dist/core/drift.js +10 -3
  12. package/dist/core/eol.d.ts +2 -0
  13. package/dist/core/eol.js +6 -0
  14. package/dist/core/procstart.d.ts +15 -4
  15. package/dist/core/procstart.js +56 -18
  16. package/dist/core/runbookDraft.d.ts +7 -0
  17. package/dist/core/runbookDraft.js +43 -0
  18. package/dist/core/taskTouched.d.ts +2 -0
  19. package/dist/core/taskTouched.js +1 -1
  20. package/dist/extractors/git.d.ts +36 -6
  21. package/dist/extractors/git.js +86 -9
  22. package/dist/extractors/indexer.js +53 -14
  23. package/dist/extractors/k8sManifest.d.ts +31 -1
  24. package/dist/extractors/k8sManifest.js +133 -8
  25. package/dist/integrations/claudemd.d.ts +2 -0
  26. package/dist/integrations/claudemd.js +4 -2
  27. package/dist/integrations/gitignore.d.ts +7 -0
  28. package/dist/integrations/gitignore.js +18 -0
  29. package/dist/integrations/hooks.d.ts +15 -0
  30. package/dist/integrations/hooks.js +49 -1
  31. package/dist/integrations/mergeDriver.d.ts +2 -0
  32. package/dist/integrations/mergeDriver.js +2 -2
  33. package/dist/integrations/providers.d.ts +7 -0
  34. package/dist/integrations/providers.js +4 -4
  35. package/dist/integrations/scaffold.d.ts +5 -0
  36. package/dist/integrations/scaffold.js +12 -7
  37. package/dist/integrations/uninstall.d.ts +40 -0
  38. package/dist/integrations/uninstall.js +435 -0
  39. package/dist/mcp/server.d.ts +2 -1
  40. package/dist/mcp/server.js +58 -4
  41. package/dist/serve/writelock.d.ts +7 -0
  42. package/dist/serve/writelock.js +30 -3
  43. package/dist/store/changeLedger.d.ts +5 -2
  44. package/dist/store/changeLedger.js +88 -37
  45. package/dist/store/hunchStore.js +4 -1
  46. package/package.json +3 -3
  47. package/server.json +2 -2
@@ -144,6 +144,21 @@ export declare function hookStatus(root: string): {
144
144
  postMerge: boolean;
145
145
  postCheckout: boolean;
146
146
  };
147
+ /** One git hook file `hunch uninstall` would change. `next === null` deletes the
148
+ * file (only a shebang was left); a `reason` means nothing is edited and the
149
+ * user removes the block by hand. */
150
+ export interface HookRemoval {
151
+ path: string;
152
+ hookName: string;
153
+ next?: string | null;
154
+ reason?: string;
155
+ }
156
+ /** Plan the removal of every Hunch-managed block from the hooks git runs (#449).
157
+ * Read-only. Only a hook file Hunch itself writes (manager "none", or an
158
+ * exec/exit file whose block it appended) is edited, and only a block with both
159
+ * markers; a hook manager's file, a tracked hooks dir, or an unclosed block is
160
+ * reported instead — the same lines Hunch refuses to write on install. */
161
+ export declare function planHookRemoval(root: string): HookRemoval[];
147
162
  /** Doctor lines for where each installed/stale hook points (issue #315): hooks
148
163
  * sharing one invocation share one line, and a line whose invocation is not the
149
164
  * Hunch currently running says so. Informational — a hook may legitimately
@@ -20,7 +20,7 @@ import { homedir } from "node:os";
20
20
  import { hooksDir, gitCommonDir, isLinkedWorktree, mainWorktreeRoot } from "../extractors/git.js";
21
21
  import { initiatorChildEnv } from "../synthesis/initiator.js";
22
22
  import { HUNCH_NPX_PACKAGE_SPEC, HUNCH_PACKAGE_NAME } from "../core/version.js";
23
- import { normalizeEol } from "../core/eol.js";
23
+ import { eolLengthAt, normalizeEol } from "../core/eol.js";
24
24
  const MARK = "# >>> hunch post-commit >>>";
25
25
  const ENDMARK = "# <<< hunch post-commit <<<";
26
26
  const GIT_CONTEXT = { checkoutType: "$3", portable: false };
@@ -942,6 +942,54 @@ export function hookStatus(root) {
942
942
  postCheckout: r.postCheckout.state === "installed",
943
943
  };
944
944
  }
945
+ /** Plan the removal of every Hunch-managed block from the hooks git runs (#449).
946
+ * Read-only. Only a hook file Hunch itself writes (manager "none", or an
947
+ * exec/exit file whose block it appended) is edited, and only a block with both
948
+ * markers; a hook manager's file, a tracked hooks dir, or an unclosed block is
949
+ * reported instead — the same lines Hunch refuses to write on install. */
950
+ export function planHookRemoval(root) {
951
+ const ends = {
952
+ [MARK]: ENDMARK, [PRE_MARK]: PRE_END, [GROUNDING_MERGE_MARK]: GROUNDING_MERGE_END,
953
+ [REPAIR_MERGE_MARK]: REPAIR_MERGE_END, [CHECKOUT_MARK]: CHECKOUT_END,
954
+ };
955
+ const byHook = new Map();
956
+ for (const [hookName, mark] of managedBlocks())
957
+ byHook.set(hookName, [...(byHook.get(hookName) ?? []), mark]);
958
+ const out = [];
959
+ for (const [hookName, marks] of byHook) {
960
+ const t = resolveHookTarget(root, hookName, marks[0]);
961
+ if (t.manager !== "none" && t.manager !== "exec-exit") {
962
+ const owner = readText(t.ownerFile);
963
+ if (owner !== null && marks.some((m) => owner.includes(m))) {
964
+ out.push({ path: t.ownerFile, hookName, reason: `${t.manager} owns this hook; remove the Hunch block from it by hand` });
965
+ }
966
+ continue;
967
+ }
968
+ const cur = readText(t.hookPath);
969
+ if (cur === null || !marks.some((m) => cur.includes(m)))
970
+ continue;
971
+ let next = cur;
972
+ let damaged = false;
973
+ for (const mark of marks) {
974
+ if (!next.includes(mark))
975
+ continue;
976
+ const m = new RegExp(`${escapeRe(mark)}[\\s\\S]*?${escapeRe(ends[mark])}`).exec(next);
977
+ if (!m) {
978
+ damaged = true;
979
+ continue;
980
+ }
981
+ const end = m.index + m[0].length;
982
+ next = next.slice(0, m.index) + next.slice(end + eolLengthAt(next, end));
983
+ }
984
+ if (damaged) {
985
+ out.push({ path: t.hookPath, hookName, reason: "a Hunch block here has no closing marker; remove it by hand" });
986
+ continue;
987
+ }
988
+ const body = normalizeEol(next).split("\n").filter((l, i) => !(i === 0 && l.startsWith("#!")));
989
+ out.push({ path: t.hookPath, hookName, next: body.every((l) => l.trim() === "") ? null : next });
990
+ }
991
+ return out;
992
+ }
945
993
  /** Doctor lines for where each installed/stale hook points (issue #315): hooks
946
994
  * sharing one invocation share one line, and a line whose invocation is not the
947
995
  * Hunch currently running says so. Informational — a hook may legitimately
@@ -1,3 +1,5 @@
1
+ export declare const ATTR_LINES: readonly string[];
2
+ export declare function targetRepositoryEnv(): NodeJS.ProcessEnv;
1
3
  export declare function installMergeDriver(root: string, invShell: string): {
2
4
  action: string;
3
5
  };
@@ -20,12 +20,12 @@ import { GROUNDING_DOC_PATHS } from "./providers.js";
20
20
  //
21
21
  // The five generated grounding docs get the OTHER driver — narrower in scope
22
22
  // (it only ever touches a hard conflict confined to the counts sentence).
23
- const ATTR_LINES = [
23
+ export const ATTR_LINES = [
24
24
  ".hunch/**/*.json merge=hunch",
25
25
  ".hunch/manifest.json merge=text",
26
26
  ...GROUNDING_DOC_PATHS.map((f) => `${f} merge=hunch-grounding`),
27
27
  ];
28
- function targetRepositoryEnv() {
28
+ export function targetRepositoryEnv() {
29
29
  const env = { ...process.env };
30
30
  for (const key of [
31
31
  "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_CONFIG", "GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT",
@@ -1,5 +1,10 @@
1
1
  import type { HunchStore } from "../store/hunchStore.js";
2
2
  import type { Invocation } from "./scaffold.js";
3
+ /** Read a JSON/JSONC object. Returns {} only for an ABSENT or empty file. A
4
+ * non-empty file we cannot parse THROWS — overwriting it would silently wipe the
5
+ * user's other MCP servers. */
6
+ export declare function readJsonObj(file: string): Record<string, unknown>;
7
+ export declare function isHunchProviderHook(entry: unknown): boolean;
3
8
  /** Cursor: .cursor/mcp.json — same `mcpServers` shape as Claude Desktop/Code. */
4
9
  export declare function writeCursorMcp(root: string, inv: Invocation): string;
5
10
  /** VS Code (Copilot agent mode): .vscode/mcp.json — root key is `servers`, and
@@ -20,6 +25,8 @@ export declare function writeAntigravityMcp(inv: Invocation, home?: string): str
20
25
  * committed with the repository, so every clone gets the same private/local
21
26
  * Hunch server without touching a user's home directory. */
22
27
  export declare function writeAntigravityWorkspaceMcp(root: string, inv: Invocation): string;
28
+ export declare const TOML_START = "# >>> hunch mcp (managed) >>>";
29
+ export declare const TOML_END = "# <<< hunch mcp <<<";
23
30
  /** Codex CLI: .codex/config.toml — `[mcp_servers.hunch]` stdio entry. We own only
24
31
  * a marker-delimited block; any other TOML the user has is preserved. Paths use
25
32
  * TOML single-quote LITERAL strings so Windows backslashes need no escaping. */
@@ -50,7 +50,7 @@ function hasJsoncComments(raw) {
50
50
  /** Read a JSON/JSONC object. Returns {} only for an ABSENT or empty file. A
51
51
  * non-empty file we cannot parse THROWS — overwriting it would silently wipe the
52
52
  * user's other MCP servers. */
53
- function readJsonObj(file) {
53
+ export function readJsonObj(file) {
54
54
  if (!existsSync(file))
55
55
  return {};
56
56
  const raw = readFileSync(file, "utf8");
@@ -128,7 +128,7 @@ function shellToken(part) {
128
128
  function hookCommand(inv, provider) {
129
129
  return [inv.command, ...inv.args, "hook", "--provider", provider].map(shellToken).join(" ");
130
130
  }
131
- function isHunchProviderHook(entry) {
131
+ export function isHunchProviderHook(entry) {
132
132
  const e = entry && typeof entry === "object" ? entry : null;
133
133
  // Codex (like Claude Code) nests commands under a matcher entry: an entry
134
134
  // whose every nested hook is ours is ours; a mixed entry stays foreign.
@@ -210,8 +210,8 @@ export function writeAntigravityWorkspaceMcp(root, inv) {
210
210
  json.mcpServers.hunch = { command: inv.command, args: [...inv.args, "mcp"] };
211
211
  return writeJson(file, json);
212
212
  }
213
- const TOML_START = "# >>> hunch mcp (managed) >>>";
214
- const TOML_END = "# <<< hunch mcp <<<";
213
+ export const TOML_START = "# >>> hunch mcp (managed) >>>";
214
+ export const TOML_END = "# <<< hunch mcp <<<";
215
215
  /** Codex CLI: .codex/config.toml — `[mcp_servers.hunch]` stdio entry. We own only
216
216
  * a marker-delimited block; any other TOML the user has is preserved. Paths use
217
217
  * TOML single-quote LITERAL strings so Windows backslashes need no escaping. */
@@ -23,6 +23,11 @@ export interface ClaudeHookInstall {
23
23
  * non-empty file we cannot parse THROWS rather than clobbering the user's config.
24
24
  */
25
25
  export declare function installClaudeHooks(root: string, hookCmd: string): ClaudeHookInstall;
26
+ /** Ownership marker for generated slash commands: its presence means Hunch may
27
+ * refresh the file; deleting the line hands the file to the user for good. */
28
+ export declare const CMD_MARKER = "<!-- hunch:generated \u2014 refreshed by hunch init; delete this line to take ownership -->";
29
+ /** File names `writeSlashCommands` writes under .claude/commands/. */
30
+ export declare const SLASH_COMMAND_FILES: readonly string[];
26
31
  export declare function writeSlashCommands(root: string): {
27
32
  written: string[];
28
33
  skipped: string[];
@@ -233,13 +233,9 @@ export function installClaudeHooks(root, hookCmd) {
233
233
  }
234
234
  /** Ownership marker for generated slash commands: its presence means Hunch may
235
235
  * refresh the file; deleting the line hands the file to the user for good. */
236
- const CMD_MARKER = "<!-- hunch:generated — refreshed by hunch init; delete this line to take ownership -->";
237
- export function writeSlashCommands(root) {
238
- const dir = join(root, ".claude", "commands");
239
- mkdirSync(dir, { recursive: true });
240
- const written = [];
241
- const skipped = [];
242
- const files = [
236
+ export const CMD_MARKER = "<!-- hunch:generated — refreshed by hunch init; delete this line to take ownership -->";
237
+ function slashCommandFiles() {
238
+ return [
243
239
  ["hunch-why.md", WHY_CMD],
244
240
  ["hunch-fix.md", FIX_CMD],
245
241
  ["hunch-fragile.md", FRAGILE_CMD],
@@ -248,6 +244,15 @@ export function writeSlashCommands(root) {
248
244
  ["audit.md", AUDIT_CMD],
249
245
  ["worktrees.md", WORKTREES_CMD],
250
246
  ];
247
+ }
248
+ /** File names `writeSlashCommands` writes under .claude/commands/. */
249
+ export const SLASH_COMMAND_FILES = Object.freeze(slashCommandFiles().map(([name]) => name));
250
+ export function writeSlashCommands(root) {
251
+ const dir = join(root, ".claude", "commands");
252
+ mkdirSync(dir, { recursive: true });
253
+ const written = [];
254
+ const skipped = [];
255
+ const files = slashCommandFiles();
251
256
  for (const [name, body] of files) {
252
257
  const p = join(dir, name);
253
258
  // Generic names (capture/heal/audit) are plausibly the USER'S OWN commands;
@@ -0,0 +1,40 @@
1
+ /** The opt-out marker, at the repository root. Commit it so every clone stays opted out. */
2
+ export declare const OPT_OUT_FILE = ".hunch-removed";
3
+ export declare function isOptedOut(root: string): boolean;
4
+ export declare function optOutMessage(): string;
5
+ export interface UninstallStep {
6
+ /** Repo-relative (POSIX) where possible, else absolute. */
7
+ path: string;
8
+ kind: "edit" | "delete" | "create" | "keep";
9
+ note: string;
10
+ run?: () => void;
11
+ }
12
+ export interface UninstallPlan {
13
+ root: string;
14
+ purge: boolean;
15
+ steps: UninstallStep[];
16
+ refused: Array<{
17
+ path: string;
18
+ reason: string;
19
+ }>;
20
+ }
21
+ /** Read-only: everything `hunch uninstall` would do in `root`. */
22
+ export declare function planUninstall(root: string, opts?: {
23
+ purge?: boolean;
24
+ }): UninstallPlan;
25
+ /** Apply a plan with no refusals. The opt-out marker goes first, so an interrupted
26
+ * run is already opted out. A step that fails at write time (a file held open by a
27
+ * running `hunch mcp`/`hunch serve` on Windows, a permission) does not stop the
28
+ * others; the failures are returned so the caller can say what is left and that a
29
+ * re-run — idempotent — finishes the job. */
30
+ /** What applyUninstall actually did: the steps that ran (so output never
31
+ * claims a step that was skipped), and the ones that failed. */
32
+ export interface UninstallResult {
33
+ applied: UninstallStep[];
34
+ failed: Array<{
35
+ path: string;
36
+ error: string;
37
+ }>;
38
+ }
39
+ export declare function applyUninstall(plan: UninstallPlan): UninstallResult;
40
+ export declare function formatUninstallPlan(plan: UninstallPlan, dryRun: boolean): string[];