@davesheffer/hunch 1.45.2 → 1.47.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/dist/cli/index.js +290 -353
- package/dist/cli/invocation.d.ts +22 -13
- package/dist/cli/invocation.js +49 -29
- package/dist/cli/verify.d.ts +28 -0
- package/dist/cli/verify.js +265 -0
- package/dist/constitution/delta.js +2 -2
- package/dist/constitution/experiment.d.ts +1 -1
- package/dist/constitution/sourceMutation.js +28 -18
- package/dist/core/agenthook.js +26 -6
- package/dist/core/boundedSpawn.d.ts +57 -0
- package/dist/core/boundedSpawn.js +321 -0
- package/dist/core/commitrepair.d.ts +43 -138
- package/dist/core/commitrepair.js +76 -167
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +1 -0
- package/dist/core/correction.js +9 -9
- package/dist/core/delivery.d.ts +6 -1
- package/dist/core/delivery.js +71 -5
- package/dist/core/docanchors.js +9 -9
- package/dist/core/drift.js +22 -27
- package/dist/core/escalations.d.ts +16 -58
- package/dist/core/escalations.js +39 -121
- package/dist/core/glob.d.ts +7 -7
- package/dist/core/glob.js +12 -11
- package/dist/core/groundingLag.d.ts +2 -9
- package/dist/core/groundingLag.js +5 -11
- package/dist/core/groundingMerge.d.ts +2 -7
- package/dist/core/groundingMerge.js +35 -54
- package/dist/core/migrate.d.ts +10 -1
- package/dist/core/migrate.js +17 -6
- package/dist/core/paths.d.ts +17 -24
- package/dist/core/paths.js +37 -43
- package/dist/core/projectDna.js +11 -1
- package/dist/core/refrepair.d.ts +2 -1
- package/dist/core/refrepair.js +4 -4
- package/dist/core/relativeImports.js +6 -2
- package/dist/core/repairqueue.d.ts +17 -34
- package/dist/core/repairqueue.js +48 -98
- package/dist/core/taskSelection.js +48 -6
- package/dist/core/testSelection.d.ts +122 -0
- package/dist/core/testSelection.js +588 -0
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/extractors/diff.d.ts +4 -4
- package/dist/extractors/diff.js +25 -29
- package/dist/extractors/git.d.ts +8 -23
- package/dist/extractors/git.js +77 -95
- package/dist/extractors/helm.d.ts +10 -0
- package/dist/extractors/helm.js +43 -59
- package/dist/extractors/indexer.d.ts +6 -0
- package/dist/extractors/indexer.js +168 -184
- package/dist/extractors/k8sManifest.d.ts +23 -80
- package/dist/extractors/k8sManifest.js +319 -690
- package/dist/extractors/languages.d.ts +37 -37
- package/dist/extractors/languages.js +83 -54
- package/dist/extractors/nativeTreeSitter.d.ts +4 -4
- package/dist/extractors/nativeTreeSitter.js +17 -14
- package/dist/extractors/parse.d.ts +27 -13
- package/dist/extractors/parse.js +120 -56
- package/dist/extractors/workspaces.js +3 -1
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.js +3 -5
- package/dist/integrations/health.d.ts +6 -0
- package/dist/integrations/health.js +33 -12
- package/dist/integrations/hooks.d.ts +13 -16
- package/dist/integrations/hooks.js +31 -30
- package/dist/integrations/mergeDriver.js +18 -17
- package/dist/integrations/optout.d.ts +3 -0
- package/dist/integrations/optout.js +17 -0
- package/dist/integrations/providers.d.ts +6 -2
- package/dist/integrations/providers.js +19 -2
- package/dist/integrations/team.js +4 -2
- package/dist/integrations/uninstall.d.ts +10 -4
- package/dist/integrations/uninstall.js +22 -18
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +107 -81
- package/dist/serve/app.js +9 -1
- package/dist/serve/config.js +5 -0
- package/dist/serve/writelock.d.ts +3 -1
- package/dist/serve/writelock.js +17 -3
- package/dist/store/hunchStore.d.ts +2 -1
- package/dist/store/hunchStore.js +20 -23
- package/dist/store/jsonStore.d.ts +13 -3
- package/dist/store/jsonStore.js +37 -13
- package/dist/store/stateBinding.js +17 -4
- package/dist/synthesis/provider.d.ts +19 -21
- package/dist/synthesis/provider.js +93 -154
- package/dist/synthesis/synthesize.d.ts +8 -8
- package/dist/synthesis/synthesize.js +33 -50
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wires up two independent git merge drivers:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Both routes live in
|
|
9
|
-
*
|
|
10
|
-
*
|
|
3
|
+
* - `merge=hunch`: the structured `.hunch/` JSON driver (store/merge.ts), which
|
|
4
|
+
* resolves concurrent edits by record id;
|
|
5
|
+
* - `merge=hunch-grounding`: the generated grounding docs driver
|
|
6
|
+
* (core/groundingMerge.ts), which auto-resolves only a hard conflict confined
|
|
7
|
+
* to the record-counts sentence.
|
|
8
|
+
* Both routes live in the committed `.gitattributes`. Both drivers are
|
|
9
|
+
* registered only in local, per-clone git config, because each references this
|
|
10
|
+
* machine's node and CLI path; teammates re-run `hunch init`.
|
|
11
11
|
*/
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { readFileSync, existsSync } from "node:fs";
|
|
14
14
|
import { writeFileAtomic } from "../core/io.js";
|
|
15
15
|
import { assertSafeTopLevelConfigFile } from "./gitignore.js";
|
|
16
16
|
import { GROUNDING_DOC_PATHS } from "./providers.js";
|
|
17
|
+
import { mergeDriverShell } from "../cli/invocation.js";
|
|
17
18
|
// Route the .hunch JSON records through the structured driver — but NOT the
|
|
18
19
|
// manifest (an id-less `{schema_version}` object the driver can't merge by id; a
|
|
19
20
|
// normal text merge with conflict markers is the right behavior for it).
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// (it only ever touches a hard conflict confined to the counts sentence).
|
|
21
|
+
// The five generated grounding docs go to the other driver, which is narrower:
|
|
22
|
+
// it only ever touches a hard conflict confined to the counts sentence.
|
|
23
23
|
export const ATTR_LINES = [
|
|
24
24
|
".hunch/**/*.json merge=hunch",
|
|
25
25
|
".hunch/manifest.json merge=text",
|
|
26
|
-
...GROUNDING_DOC_PATHS.map((
|
|
26
|
+
...GROUNDING_DOC_PATHS.map((p) => `${p} merge=hunch-grounding`),
|
|
27
27
|
];
|
|
28
28
|
export function targetRepositoryEnv() {
|
|
29
29
|
const env = { ...process.env };
|
|
@@ -56,11 +56,12 @@ export function installMergeDriver(root, invShell) {
|
|
|
56
56
|
assertSafeTopLevelConfigFile(root, ".gitattributes");
|
|
57
57
|
writeFileAtomic(attrPath, text);
|
|
58
58
|
}
|
|
59
|
-
// 2.
|
|
60
|
-
// this machine's node
|
|
61
|
-
// re-run init.
|
|
62
|
-
const
|
|
63
|
-
const
|
|
59
|
+
// 2. The driver definitions go into local git config. They are per-clone (they
|
|
60
|
+
// reference this machine's node and CLI path), so they are never committed; teammates
|
|
61
|
+
// re-run init. Dev mode swaps `npx tsx` for Hunch's own loader (#492).
|
|
62
|
+
const shell = mergeDriverShell(invShell);
|
|
63
|
+
const driver = `${shell} merge-driver "%O" "%A" "%B" "%P"`;
|
|
64
|
+
const groundingDriver = `${shell} merge-driver-grounding "%O" "%A" "%B" "%P"`;
|
|
64
65
|
const env = targetRepositoryEnv();
|
|
65
66
|
try {
|
|
66
67
|
execFileSync("git", ["config", "merge.hunch.name", "hunch structured JSON merge"], { cwd: root, env });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opt-out marker `hunch uninstall` leaves (#449), apart from uninstall.ts so the
|
|
3
|
+
* hook installers can honor it without an import cycle (uninstall.ts imports hooks.ts).
|
|
4
|
+
*/
|
|
5
|
+
import { lstatSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
/** The opt-out marker, at the repository root. Commit it so every clone stays opted out. */
|
|
8
|
+
export const OPT_OUT_FILE = ".hunch-removed";
|
|
9
|
+
export function isOptedOut(root) {
|
|
10
|
+
try {
|
|
11
|
+
return lstatSync(join(root, OPT_OUT_FILE)).isFile();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=optout.js.map
|
|
@@ -73,8 +73,12 @@ export declare function writeCodexHooks(root: string, inv: Invocation): string;
|
|
|
73
73
|
export declare function writeVscodeHooks(root: string, inv: Invocation): string;
|
|
74
74
|
/** Windsurf's documented workspace hooks. It only supports deterministic
|
|
75
75
|
* pre-hook blocking via exit code 2, so Hunch uses rules + MCP for context and
|
|
76
|
-
* reserves the hook for strict edit protection and pipeline observation.
|
|
77
|
-
|
|
76
|
+
* reserves the hook for strict edit protection and pipeline observation.
|
|
77
|
+
* Devin Desktop (formerly Windsurf) reads .devin/hooks.json and ignores the
|
|
78
|
+
* legacy .windsurf/hooks.json whenever the former defines any hook, so Hunch
|
|
79
|
+
* merges into both (docs.devin.ai/desktop/cascade/hooks, #494). Both files are
|
|
80
|
+
* parsed before either is written: an unparseable one refuses the whole write. */
|
|
81
|
+
export declare function writeWindsurfHooks(root: string, inv: Invocation): string[];
|
|
78
82
|
/** Antigravity keeps hook groups at the top level (not under `hooks`). Hunch
|
|
79
83
|
* owns only the `hunch` group and replaces its own old entries idempotently. */
|
|
80
84
|
export declare function writeAntigravityHooks(root: string, inv: Invocation): string;
|
|
@@ -419,9 +419,26 @@ function publishedPinEvents(file) {
|
|
|
419
419
|
}
|
|
420
420
|
/** Windsurf's documented workspace hooks. It only supports deterministic
|
|
421
421
|
* pre-hook blocking via exit code 2, so Hunch uses rules + MCP for context and
|
|
422
|
-
* reserves the hook for strict edit protection and pipeline observation.
|
|
422
|
+
* reserves the hook for strict edit protection and pipeline observation.
|
|
423
|
+
* Devin Desktop (formerly Windsurf) reads .devin/hooks.json and ignores the
|
|
424
|
+
* legacy .windsurf/hooks.json whenever the former defines any hook, so Hunch
|
|
425
|
+
* merges into both (docs.devin.ai/desktop/cascade/hooks, #494). Both files are
|
|
426
|
+
* parsed before either is written: an unparseable one refuses the whole write. */
|
|
423
427
|
export function writeWindsurfHooks(root, inv) {
|
|
424
|
-
const
|
|
428
|
+
const files = [join(root, ".windsurf", "hooks.json"), join(root, ".devin", "hooks.json")];
|
|
429
|
+
// Same shape checks writeHookConfig applies at write time, so neither file is
|
|
430
|
+
// written when the other would refuse.
|
|
431
|
+
for (const file of files) {
|
|
432
|
+
const hooks = objectField(readJsonObj(file), "hooks", file);
|
|
433
|
+
for (const event of WINDSURF_EVENTS) {
|
|
434
|
+
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
435
|
+
throw new Error(`refusing to edit ${file}: hooks.${event} must be an array when present; fix it, then re-run.`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return files.map((file) => writeWindsurfHookFile(file, inv));
|
|
440
|
+
}
|
|
441
|
+
function writeWindsurfHookFile(file, inv) {
|
|
425
442
|
const published = hookCommand(publishedMcpInvocation(), "windsurf");
|
|
426
443
|
let command = hookCommand(inv, "windsurf");
|
|
427
444
|
// A source-checkout run (tsx / local dist) must never replace a tracked
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync } from "node:fs";
|
|
9
9
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { boundedSpawnSync } from "../core/boundedSpawn.js";
|
|
11
12
|
import { writeFileAtomic } from "../core/io.js";
|
|
12
13
|
import { machineFile } from "../core/machine.js";
|
|
13
14
|
import { hunchTreeAttributesAreSafe, safeOverlayGitTreeListing, safeOverlayTree } from "../core/overlaySafety.js";
|
|
@@ -654,13 +655,14 @@ export function cloneValidatedTeamOverlay(sharedRepo, sharedRepoCwd, destination
|
|
|
654
655
|
}
|
|
655
656
|
const cloneEnv = boundedTeamGitEnv();
|
|
656
657
|
stage = "clone";
|
|
657
|
-
|
|
658
|
+
// Bounded so a timeout kills git's whole tree, not just the root (#498).
|
|
659
|
+
const cloned = boundedSpawnSync("git", [
|
|
658
660
|
"-c", "protocol.ext.allow=never",
|
|
659
661
|
"-c", `core.hooksPath=${emptyHooks}`,
|
|
660
662
|
"clone", "--no-checkout", `--template=${emptyTemplate}`,
|
|
661
663
|
"--", repo, stagedDest,
|
|
662
664
|
], {
|
|
663
|
-
|
|
665
|
+
stdout: "ignore",
|
|
664
666
|
env: cloneEnv,
|
|
665
667
|
timeout: timeoutMs,
|
|
666
668
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export declare function isOptedOut(root: string): boolean;
|
|
1
|
+
import { isOptedOut, OPT_OUT_FILE } from "./optout.js";
|
|
2
|
+
export { isOptedOut, OPT_OUT_FILE };
|
|
4
3
|
/** Whether `start`, or the repository containing it, opted out: walks up through
|
|
5
4
|
* `start`'s ancestors (which need not exist yet), stopping after the first one holding
|
|
6
5
|
* `.git` or at the filesystem root, so a path inside an opted-out repository (`serve
|
|
@@ -8,7 +7,14 @@ export declare function isOptedOut(root: string): boolean;
|
|
|
8
7
|
* store is deliberately not a boundary: a partition store nested in an opted-out
|
|
9
8
|
* repository stays gated (fail closed). */
|
|
10
9
|
export declare function optedOutAt(start: string): boolean;
|
|
11
|
-
|
|
10
|
+
/** The opt-out marker that gates `start` (walked as in `optedOutAt`), or null. With
|
|
11
|
+
* `withinRepo`, a marker counts only when the walk reaches a repository: outside one,
|
|
12
|
+
* an ancestor's marker gates nothing. */
|
|
13
|
+
export declare function optOutMarkerAt(start: string, opts?: {
|
|
14
|
+
withinRepo?: boolean;
|
|
15
|
+
}): string | null;
|
|
16
|
+
/** The refusal, naming the marker of the repository actually checked (#488). */
|
|
17
|
+
export declare function optOutMessage(marker?: string): string;
|
|
12
18
|
export interface UninstallStep {
|
|
13
19
|
/** Repo-relative (POSIX) where possible, else absolute. */
|
|
14
20
|
path: string;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* `.hunch/` holds the engineering memory, so it stays unless `--purge`.
|
|
14
14
|
*/
|
|
15
|
-
import { existsSync,
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, rmdirSync, rmSync, statSync, writeFileSync, chmodSync } from "node:fs";
|
|
16
16
|
import { spawnSync } from "node:child_process";
|
|
17
17
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
18
18
|
import { parse as parseToml } from "smol-toml";
|
|
@@ -26,16 +26,8 @@ import { gitRun, isInside, planHookRemoval, worktreeTops } from "./hooks.js";
|
|
|
26
26
|
import { ATTR_LINES, targetRepositoryEnv } from "./mergeDriver.js";
|
|
27
27
|
import { antigravityMcpFile, isHunchProviderHook, readJsonObj, TOML_END, TOML_START, windsurfMcpFile } from "./providers.js";
|
|
28
28
|
import { SLASH_COMMAND_FILES } from "./scaffold.js";
|
|
29
|
-
|
|
30
|
-
export
|
|
31
|
-
export function isOptedOut(root) {
|
|
32
|
-
try {
|
|
33
|
-
return lstatSync(join(root, OPT_OUT_FILE)).isFile();
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
29
|
+
import { isOptedOut, OPT_OUT_FILE } from "./optout.js";
|
|
30
|
+
export { isOptedOut, OPT_OUT_FILE };
|
|
39
31
|
/** Whether `start`, or the repository containing it, opted out: walks up through
|
|
40
32
|
* `start`'s ancestors (which need not exist yet), stopping after the first one holding
|
|
41
33
|
* `.git` or at the filesystem root, so a path inside an opted-out repository (`serve
|
|
@@ -43,19 +35,30 @@ export function isOptedOut(root) {
|
|
|
43
35
|
* store is deliberately not a boundary: a partition store nested in an opted-out
|
|
44
36
|
* repository stays gated (fail closed). */
|
|
45
37
|
export function optedOutAt(start) {
|
|
38
|
+
return optOutMarkerAt(start) !== null;
|
|
39
|
+
}
|
|
40
|
+
/** The opt-out marker that gates `start` (walked as in `optedOutAt`), or null. With
|
|
41
|
+
* `withinRepo`, a marker counts only when the walk reaches a repository: outside one,
|
|
42
|
+
* an ancestor's marker gates nothing. */
|
|
43
|
+
export function optOutMarkerAt(start, opts = {}) {
|
|
44
|
+
let found = null;
|
|
46
45
|
for (let dir = resolve(start);;) {
|
|
47
|
-
if (isOptedOut(dir))
|
|
48
|
-
|
|
46
|
+
if (!found && isOptedOut(dir)) {
|
|
47
|
+
found = join(dir, OPT_OUT_FILE);
|
|
48
|
+
if (!opts.withinRepo)
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
49
51
|
if (existsSync(join(dir, ".git")))
|
|
50
|
-
return
|
|
52
|
+
return found;
|
|
51
53
|
const parent = dirname(dir);
|
|
52
54
|
if (parent === dir)
|
|
53
|
-
return
|
|
55
|
+
return opts.withinRepo ? null : found;
|
|
54
56
|
dir = parent;
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
/** The refusal, naming the marker of the repository actually checked (#488). */
|
|
60
|
+
export function optOutMessage(marker = OPT_OUT_FILE) {
|
|
61
|
+
return `Hunch was removed from this repository on purpose (${marker}), so it will not set itself up here again. To reinstall, delete ${marker} and run \`hunch init\`.`;
|
|
59
62
|
}
|
|
60
63
|
function markerText() {
|
|
61
64
|
return [
|
|
@@ -310,6 +313,7 @@ export function planUninstall(root, opts = {}) {
|
|
|
310
313
|
p.hookConfig(".codex/hooks.json", providerEntry);
|
|
311
314
|
p.hookConfig(".github/hooks/hunch.json", providerEntry);
|
|
312
315
|
p.hookConfig(".windsurf/hooks.json", providerEntry);
|
|
316
|
+
p.hookConfig(".devin/hooks.json", providerEntry);
|
|
313
317
|
p.json(".agents/hooks.json", "removed the hunch hook group", (json) => {
|
|
314
318
|
if (!("hunch" in json))
|
|
315
319
|
return false;
|
|
@@ -459,7 +463,7 @@ export function planUninstall(root, opts = {}) {
|
|
|
459
463
|
}
|
|
460
464
|
/** Directories Hunch's writers create; removed only when left empty. Deepest first. */
|
|
461
465
|
const PRUNE_DIRS = [
|
|
462
|
-
".claude/commands", ".claude", ".cursor/rules", ".cursor", ".windsurf/rules", ".windsurf",
|
|
466
|
+
".claude/commands", ".claude", ".cursor/rules", ".cursor", ".windsurf/rules", ".windsurf", ".devin",
|
|
463
467
|
".vscode", ".github/hooks", ".github/workflows", ".github", ".codex", ".agents",
|
|
464
468
|
];
|
|
465
469
|
export function applyUninstall(plan) {
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -359,12 +359,12 @@ declare const DELIVERY_OUTPUT_SCHEMA: z.ZodObject<{
|
|
|
359
359
|
type: z.ZodEnum<{
|
|
360
360
|
depends_on: "depends_on";
|
|
361
361
|
calls: "calls";
|
|
362
|
+
references: "references";
|
|
362
363
|
imports: "imports";
|
|
363
364
|
contains: "contains";
|
|
364
365
|
implements: "implements";
|
|
365
366
|
supersedes: "supersedes";
|
|
366
367
|
related_to: "related_to";
|
|
367
|
-
references: "references";
|
|
368
368
|
provides: "provides";
|
|
369
369
|
belongs_to: "belongs_to";
|
|
370
370
|
implemented_by: "implemented_by";
|
|
@@ -624,10 +624,9 @@ export declare function compactEnvelope(envelope: z.infer<typeof DELIVERY_OUTPUT
|
|
|
624
624
|
* receipts and receipt_id are unchanged. */
|
|
625
625
|
export declare function compactOmissions(envelope: z.infer<typeof DELIVERY_OUTPUT_SCHEMA>): z.infer<typeof DELIVERY_ADVERTISED_OUTPUT_SCHEMA>;
|
|
626
626
|
export declare function decisionSubstanceChanged(before: Decision, after: Decision): boolean;
|
|
627
|
-
/** Resolve a free-form target (symbol id
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
* apart on the same question. */
|
|
627
|
+
/** Resolve a free-form target (symbol id, name, or file path) to symbol records using
|
|
628
|
+
* tiered matching — exact id, then exact name, then exact file, then a path-segment-anchored
|
|
629
|
+
* suffix. Shared with HunchStore.why() through matchSymbolsTiered so the two never disagree. */
|
|
631
630
|
export declare function resolveSymbols(store: HunchStore, target: string): Symbol[];
|
|
632
631
|
/** Resolve a target to canonical indexed file path(s) (for file-granular blast
|
|
633
632
|
* radius). Falls back to the literal target so direct-scope checks still run. */
|