@davesheffer/hunch 1.46.0 → 1.47.1
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 +280 -358
- package/dist/cli/invocation.d.ts +8 -13
- package/dist/cli/invocation.js +26 -29
- package/dist/cli/taskReport.d.ts +6 -0
- package/dist/cli/taskReport.js +31 -8
- package/dist/constitution/delta.js +2 -2
- package/dist/constitution/sourceMutation.js +28 -18
- package/dist/core/commitrepair.d.ts +43 -138
- package/dist/core/commitrepair.js +76 -167
- package/dist/core/correction.d.ts +2 -0
- package/dist/core/correction.js +23 -10
- 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/paths.d.ts +17 -24
- package/dist/core/paths.js +37 -43
- package/dist/core/refrepair.d.ts +2 -1
- package/dist/core/refrepair.js +4 -4
- package/dist/core/repairqueue.d.ts +17 -34
- package/dist/core/repairqueue.js +48 -98
- package/dist/core/taskRecord.d.ts +1 -1
- package/dist/core/taskRecord.js +2 -2
- package/dist/core/taskReport.d.ts +8 -3
- package/dist/core/taskReport.js +7 -2
- package/dist/core/taskReportEvidence.d.ts +7 -0
- package/dist/core/taskReportEvidence.js +16 -4
- package/dist/core/taskReportHook.js +2 -2
- package/dist/core/taskReportPublic.js +2 -2
- package/dist/core/taskReportRender.js +2 -2
- 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 +18 -23
- package/dist/extractors/git.js +72 -64
- 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 +126 -178
- package/dist/extractors/k8sManifest.d.ts +23 -80
- package/dist/extractors/k8sManifest.js +319 -690
- package/dist/extractors/languages.d.ts +28 -37
- package/dist/extractors/languages.js +55 -54
- package/dist/extractors/nativeTreeSitter.d.ts +4 -4
- package/dist/extractors/nativeTreeSitter.js +17 -14
- package/dist/extractors/parse.d.ts +7 -13
- package/dist/extractors/parse.js +39 -51
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.js +3 -5
- package/dist/integrations/health.js +1 -1
- package/dist/integrations/hooks.d.ts +9 -14
- package/dist/integrations/hooks.js +19 -28
- package/dist/integrations/mergeDriver.js +13 -14
- package/dist/integrations/probe.d.ts +11 -0
- package/dist/integrations/probe.js +29 -4
- package/dist/integrations/sync.d.ts +16 -4
- package/dist/integrations/sync.js +54 -3
- package/dist/integrations/workspaceLedger.d.ts +1 -1
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +71 -83
- package/dist/mcp/taskReportTools.js +4 -4
- package/dist/serve/app.js +2 -2
- package/dist/store/hunchStore.js +12 -18
- package/dist/synthesis/provider.d.ts +19 -21
- package/dist/synthesis/provider.js +67 -76
- package/dist/synthesis/synthesize.d.ts +8 -8
- package/dist/synthesis/synthesize.js +32 -49
- package/dist/taskReports.d.ts +1 -0
- package/dist/taskReports.js +2 -2
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -111,20 +111,15 @@ export declare function installPostCommitHook(root: string, invocation: string,
|
|
|
111
111
|
* blocking invariant (see strictgate.ts), so it's safe on a shared repo.
|
|
112
112
|
* Preserves any existing pre-commit hook. */
|
|
113
113
|
export declare function installPreCommitHook(root: string, invocation: string, strict?: boolean): HookInstall;
|
|
114
|
-
/** Install
|
|
115
|
-
* re-
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* no commit of its own and so can't reuse HUNCH_SYNC's re-trigger guard).
|
|
124
|
-
* Each block is keyed by its own marker pair (installManagedBlock), so
|
|
125
|
-
* re-running updates only its own block, preserves the other untouched, and
|
|
126
|
-
* a repo carrying only one half (an older install, or a hand-edited hook)
|
|
127
|
-
* gets the other appended rather than clobbered. */
|
|
114
|
+
/** Install the post-merge hook. It carries two independently managed blocks:
|
|
115
|
+
* - grounding: re-syncs committed grounding docs when a merge brought in memory.
|
|
116
|
+
* Guarded by HUNCH_SYNC, runs in the foreground, never fails the merge.
|
|
117
|
+
* - repair: detects a decision's commit provenance being orphaned by a
|
|
118
|
+
* squash-merge (including a fast-forward from `git pull`) and queues the match
|
|
119
|
+
* for `hunch repair-provenance --apply`. Guarded by HUNCH_MERGE_SYNC, runs in
|
|
120
|
+
* the background.
|
|
121
|
+
* Each block is keyed by its own marker pair, so a re-run updates only its own
|
|
122
|
+
* block, and a hook that has only one half gets the other appended, not clobbered. */
|
|
128
123
|
export declare function installPostMergeHook(root: string, invocation: string): HookInstall;
|
|
129
124
|
export declare function installPostCheckoutHook(root: string, invocation: string): HookInstall;
|
|
130
125
|
export type HookState = BlockState;
|
|
@@ -793,14 +793,12 @@ export function installPreCommitHook(root, invocation, strict = false) {
|
|
|
793
793
|
};
|
|
794
794
|
return installManagedBlock(root, "pre-commit", PRE_MARK, PRE_END, build, invocation);
|
|
795
795
|
}
|
|
796
|
-
//
|
|
797
|
-
// grounding
|
|
798
|
-
// exact marker text (fnd_c402046ac7).
|
|
796
|
+
// The original grounding marker is kept unchanged for backward compatibility, so an
|
|
797
|
+
// existing grounding block is still found and updated in place.
|
|
799
798
|
const GROUNDING_MERGE_MARK = "# >>> hunch post-merge >>>";
|
|
800
799
|
const GROUNDING_MERGE_END = "# <<< hunch post-merge <<<";
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
// independently created/updated/removed without touching the other.
|
|
800
|
+
// The repair-provenance block gets a SEPARATE marker so the two blocks never collide
|
|
801
|
+
// in one post-merge file and each can be created, updated or removed on its own.
|
|
804
802
|
const REPAIR_MERGE_MARK = "# >>> hunch post-merge (repair-provenance) >>>";
|
|
805
803
|
const REPAIR_MERGE_END = "# <<< hunch post-merge (repair-provenance) <<<";
|
|
806
804
|
function groundingMergeBlock(invocation) {
|
|
@@ -819,37 +817,30 @@ function repairProvenanceMergeBlock(invocation) {
|
|
|
819
817
|
REPAIR_MERGE_MARK,
|
|
820
818
|
'if [ -z "$HUNCH_MERGE_SYNC" ]; then',
|
|
821
819
|
" export HUNCH_MERGE_SYNC=1",
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
// (file-set overlap, not git's own rename detection) isn't strong enough to
|
|
826
|
-
// trust an unattended, backgrounded write into shared team memory.
|
|
820
|
+
// Detect-and-queue only, never --apply: the file-overlap match is not strong
|
|
821
|
+
// enough to trust an unattended write to shared memory. A human confirms the
|
|
822
|
+
// queue (.hunch/pending-commit-repairs.json) with `repair-provenance --apply`.
|
|
827
823
|
` ( ${invocation} repair-provenance --from-hook --quiet >/dev/null 2>&1 || true ) &`,
|
|
828
824
|
"fi",
|
|
829
825
|
REPAIR_MERGE_END,
|
|
830
826
|
].join("\n");
|
|
831
827
|
}
|
|
832
|
-
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
828
|
+
/** Rank used to pick ONE HookInstall action out of two independent sub-installs
|
|
829
|
+
* into the same file. "created" (the file itself is new) outranks "appended" /
|
|
830
|
+
* "updated" (an existing file
|
|
835
831
|
* changed), which outrank "unchanged". Non-writing results are handled before
|
|
836
832
|
* ranking (they must never be masked by a sibling's success). */
|
|
837
833
|
const ACTION_RANK = { created: 3, appended: 2, updated: 2, unchanged: 1, "kept-shared": 1, "managed-elsewhere": 0, unreachable: 0, "opted-out": 0 };
|
|
838
834
|
const writes = (h) => h.action !== "managed-elsewhere" && h.action !== "unreachable" && h.action !== "opted-out";
|
|
839
|
-
/** Install
|
|
840
|
-
* re-
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
* no commit of its own and so can't reuse HUNCH_SYNC's re-trigger guard).
|
|
849
|
-
* Each block is keyed by its own marker pair (installManagedBlock), so
|
|
850
|
-
* re-running updates only its own block, preserves the other untouched, and
|
|
851
|
-
* a repo carrying only one half (an older install, or a hand-edited hook)
|
|
852
|
-
* gets the other appended rather than clobbered. */
|
|
835
|
+
/** Install the post-merge hook. It carries two independently managed blocks:
|
|
836
|
+
* - grounding: re-syncs committed grounding docs when a merge brought in memory.
|
|
837
|
+
* Guarded by HUNCH_SYNC, runs in the foreground, never fails the merge.
|
|
838
|
+
* - repair: detects a decision's commit provenance being orphaned by a
|
|
839
|
+
* squash-merge (including a fast-forward from `git pull`) and queues the match
|
|
840
|
+
* for `hunch repair-provenance --apply`. Guarded by HUNCH_MERGE_SYNC, runs in
|
|
841
|
+
* the background.
|
|
842
|
+
* Each block is keyed by its own marker pair, so a re-run updates only its own
|
|
843
|
+
* block, and a hook that has only one half gets the other appended, not clobbered. */
|
|
853
844
|
export function installPostMergeHook(root, invocation) {
|
|
854
845
|
// Only the grounding half regenerates committed docs; repair-provenance writes
|
|
855
846
|
// no docs (it only queues a match for a human to confirm), so it keeps the
|
|
@@ -1,13 +1,13 @@
|
|
|
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";
|
|
@@ -18,13 +18,12 @@ import { mergeDriverShell } from "../cli/invocation.js";
|
|
|
18
18
|
// Route the .hunch JSON records through the structured driver — but NOT the
|
|
19
19
|
// manifest (an id-less `{schema_version}` object the driver can't merge by id; a
|
|
20
20
|
// normal text merge with conflict markers is the right behavior for it).
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// (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.
|
|
24
23
|
export const ATTR_LINES = [
|
|
25
24
|
".hunch/**/*.json merge=hunch",
|
|
26
25
|
".hunch/manifest.json merge=text",
|
|
27
|
-
...GROUNDING_DOC_PATHS.map((
|
|
26
|
+
...GROUNDING_DOC_PATHS.map((p) => `${p} merge=hunch-grounding`),
|
|
28
27
|
];
|
|
29
28
|
export function targetRepositoryEnv() {
|
|
30
29
|
const env = { ...process.env };
|
|
@@ -57,8 +56,8 @@ export function installMergeDriver(root, invShell) {
|
|
|
57
56
|
assertSafeTopLevelConfigFile(root, ".gitattributes");
|
|
58
57
|
writeFileAtomic(attrPath, text);
|
|
59
58
|
}
|
|
60
|
-
// 2.
|
|
61
|
-
// this machine's node
|
|
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
|
|
62
61
|
// re-run init. Dev mode swaps `npx tsx` for Hunch's own loader (#492).
|
|
63
62
|
const shell = mergeDriverShell(invShell);
|
|
64
63
|
const driver = `${shell} merge-driver "%O" "%A" "%B" "%P"`;
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { type Harness, type IntegrationHealth } from "./health.js";
|
|
2
|
+
/** A launcher that runs THIS Hunch build directly (`node <build>/cli/index.js mcp`) —
|
|
3
|
+
* a local checkout or an unpacked test build. It is exactly the code already
|
|
4
|
+
* running the probe, so starting it executes nothing new; any other custom
|
|
5
|
+
* command stays inspect-only. The server identity/version check still applies.
|
|
6
|
+
* The command is a bare `node` (PATH lookup) or this exact Node binary — a
|
|
7
|
+
* directory-qualified `./tools/node` in a cloned repo is not, whatever its name —
|
|
8
|
+
* and the probe spawns process.execPath itself, never the configured name. */
|
|
9
|
+
export declare function isSelfLauncher(launcher: {
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
}, entry?: string): boolean;
|
|
2
13
|
/** Opt-in: starts a fresh configured MCP process, never claims the current
|
|
3
14
|
* harness connection or hook delivery has been tested. No model is invoked. */
|
|
4
15
|
export declare function probeIntegration(root: string, harness: Harness, report: IntegrationHealth, timeoutMs?: number): Promise<void>;
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { sameFilesystemEntry } from "../extractors/git.js";
|
|
3
7
|
import { HARNESSES, readLauncher } from "./health.js";
|
|
8
|
+
/** A launcher that runs THIS Hunch build directly (`node <build>/cli/index.js mcp`) —
|
|
9
|
+
* a local checkout or an unpacked test build. It is exactly the code already
|
|
10
|
+
* running the probe, so starting it executes nothing new; any other custom
|
|
11
|
+
* command stays inspect-only. The server identity/version check still applies.
|
|
12
|
+
* The command is a bare `node` (PATH lookup) or this exact Node binary — a
|
|
13
|
+
* directory-qualified `./tools/node` in a cloned repo is not, whatever its name —
|
|
14
|
+
* and the probe spawns process.execPath itself, never the configured name. */
|
|
15
|
+
export function isSelfLauncher(launcher, entry = fileURLToPath(new URL("../cli/index.js", import.meta.url))) {
|
|
16
|
+
const bareNode = /^node(?:\.exe)?$/i.test(launcher.command);
|
|
17
|
+
const node = bareNode || (isAbsolute(launcher.command) && existsSync(launcher.command) && sameFilesystemEntry(launcher.command, process.execPath));
|
|
18
|
+
if (!node || launcher.args.length !== 2 || launcher.args[1] !== "mcp")
|
|
19
|
+
return false;
|
|
20
|
+
const script = launcher.args[0];
|
|
21
|
+
// Generated launchers name the build entry absolutely; a relative script would resolve
|
|
22
|
+
// against whatever cwd the host spawns in.
|
|
23
|
+
return isAbsolute(script) && existsSync(script) && existsSync(entry) && sameFilesystemEntry(script, entry);
|
|
24
|
+
}
|
|
4
25
|
/** Opt-in: starts a fresh configured MCP process, never claims the current
|
|
5
26
|
* harness connection or hook delivery has been tested. No model is invoked. */
|
|
6
27
|
export async function probeIntegration(root, harness, report, timeoutMs = 15_000) {
|
|
@@ -16,11 +37,15 @@ export async function probeIntegration(root, harness, report, timeoutMs = 15_000
|
|
|
16
37
|
throw new Error("custom MCP environment or working directory requires a host-side probe; no settings were ignored");
|
|
17
38
|
// Only execute the exact published launcher Hunch generates. Custom
|
|
18
39
|
// commands may need credentials or have unrelated effects; inspect only.
|
|
19
|
-
|
|
40
|
+
const generated = /^npx(?:\.cmd)?$/.test(launcher.command) && JSON.stringify(launcher.args) === JSON.stringify([
|
|
20
41
|
"-y", `--package=hunch-exact@npm:@davesheffer/hunch@${report.expectedVersion}`, "hunch", "mcp",
|
|
21
|
-
])
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
]);
|
|
43
|
+
const self = !generated && isSelfLauncher(launcher);
|
|
44
|
+
if (!generated && !self)
|
|
45
|
+
throw new Error("probe requires the generated exact-version npx launcher, or a `node <build>/cli/index.js mcp` launcher probed by that same build; repair stale pins first; other custom launchers remain untested");
|
|
46
|
+
// A self launcher runs this exact Node binary: no PATH/cwd lookup of `node` can
|
|
47
|
+
// resolve to an executable planted in the repository.
|
|
48
|
+
transport = new StdioClientTransport({ ...launcher, ...(self ? { command: process.execPath } : {}), cwd: root, stderr: "pipe" });
|
|
24
49
|
transport.stderr?.on("data", () => { });
|
|
25
50
|
await Promise.race([
|
|
26
51
|
(async () => {
|
|
@@ -11,11 +11,11 @@ export type MemoryHome = "public" | "private";
|
|
|
11
11
|
/** Flush one exact artifact home. Constitution repositories can explicitly
|
|
12
12
|
* choose public even in unified mode, so this must not infer routing through
|
|
13
13
|
* captureHome(isPrivate). The caller supplies the repository's actual home. */
|
|
14
|
-
export declare function flushMemoryHome(store: HunchStore, publicHunchDir: string, home: MemoryHome, message: string, remoteOverride?: HunchRemoteContract): "pushed" | "committed" | null;
|
|
14
|
+
export declare function flushMemoryHome(store: HunchStore, publicHunchDir: string, home: MemoryHome, message: string, remoteOverride?: HunchRemoteContract): "pushed" | "committed" | "held" | null;
|
|
15
15
|
/** One completion flush per touched home. A mixed ingest/bootstrap can write
|
|
16
16
|
* both homes; unchanged homes are cheap no-ops after the memory-only stage
|
|
17
17
|
* check, while each real home becomes durable exactly once. */
|
|
18
|
-
export declare function flushMemoryHomes(store: HunchStore, publicHunchDir: string, homes: Iterable<MemoryHome>, message: string, remoteOverride?: HunchRemoteContract): Partial<Record<MemoryHome, "pushed" | "committed" | null>>;
|
|
18
|
+
export declare function flushMemoryHomes(store: HunchStore, publicHunchDir: string, homes: Iterable<MemoryHome>, message: string, remoteOverride?: HunchRemoteContract): Partial<Record<MemoryHome, "pushed" | "committed" | "held" | null>>;
|
|
19
19
|
/** Auto-commit + push the overlay after a private write, when auto-commit is on. No-op
|
|
20
20
|
* otherwise (manual `hunch private --sync` still works). Never throws. */
|
|
21
21
|
export declare function flushPrivate(store: HunchStore, message: string): void;
|
|
@@ -24,7 +24,19 @@ export declare function flushPrivate(store: HunchStore, message: string): void;
|
|
|
24
24
|
* "committed" (commit created but not pushed — public .hunch/ rides the next push; an
|
|
25
25
|
* overlay commit whose merge/push failed retries on the next flush), or null (auto-commit
|
|
26
26
|
* off, no overlay for a private record, or the commit was skipped — lock held, safety
|
|
27
|
-
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up)
|
|
27
|
+
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up),
|
|
28
|
+
* or "held" (a public capture left uncommitted because the default branch is checked
|
|
29
|
+
* out; callers say so with HELD_ON_DEFAULT_BRANCH). */
|
|
28
30
|
export declare function flushCapture(store: HunchStore, publicHunchDir: string, isPrivate: boolean, message: string,
|
|
29
31
|
/** Long-lived callers can pin the route snapshot that admitted the write. */
|
|
30
|
-
remoteOverride?: HunchRemoteContract, observe?: GitMemoryObserver): "pushed" | "committed" | null;
|
|
32
|
+
remoteOverride?: HunchRemoteContract, observe?: GitMemoryObserver): "pushed" | "committed" | "held" | null;
|
|
33
|
+
/** Auto-commit a state-contract write (`nuryel_write`, `hunch serve`). A state root is a
|
|
34
|
+
* dedicated data repository whose writes commit on whatever branch it serves, so the
|
|
35
|
+
* default-branch hold for captures never applies; outcomes as flushCapture otherwise. */
|
|
36
|
+
export declare function flushStateWrite(store: HunchStore, publicHunchDir: string, isPrivate: boolean, message: string, remoteOverride?: HunchRemoteContract): "pushed" | "committed" | null;
|
|
37
|
+
/** What a caller prints for a "held" flush, so a solo user on main is never left
|
|
38
|
+
* wondering why the capture commit stopped appearing. */
|
|
39
|
+
export declare const HELD_ON_DEFAULT_BRANCH = "not auto-committed: the default branch is checked out \u2014 your next commit that adds .hunch/ carries it (to restore auto-commit there, set \"autoCommitDefaultBranch\": true in .hunch/local.json)";
|
|
40
|
+
/** Whether an automatic public memory commit must be held: the default branch is checked
|
|
41
|
+
* out and the user has not opted back in. Shared by flushStore and `hunch sync`. */
|
|
42
|
+
export declare function holdsOnDefaultBranch(publicHunchDir: string): boolean;
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* commits (bug_overlay_clobber); the memory commit rides their next push instead.
|
|
8
8
|
* Used by every CLI/MCP path that writes a record.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { commitAndPushHunch, hunchDirHasChanges, onDefaultBranch } from "../extractors/git.js";
|
|
12
13
|
import { refreshCommittableGrounding } from "./providers.js";
|
|
13
14
|
import { advertisedTeamRemoteContract } from "./team.js";
|
|
14
15
|
const pinnedSharedRoutes = new WeakMap();
|
|
@@ -41,6 +42,10 @@ export function flushMemoryHome(store, publicHunchDir, home, message, remoteOver
|
|
|
41
42
|
}
|
|
42
43
|
if (!store.autoCommit)
|
|
43
44
|
return null;
|
|
45
|
+
// Same hold as flushCapture: the public commit stages all of .hunch/, so it would
|
|
46
|
+
// otherwise sweep a capture that was just reported as held onto the default branch.
|
|
47
|
+
if (holdsOnDefaultBranch(publicHunchDir))
|
|
48
|
+
return heldOrNothing(publicHunchDir);
|
|
44
49
|
const grounding = refreshCommittableGrounding(dirname(publicHunchDir), store);
|
|
45
50
|
return commitAndPushHunch(publicHunchDir, message, { push: false, alsoStage: grounding });
|
|
46
51
|
}
|
|
@@ -70,10 +75,21 @@ export function flushPrivate(store, message) {
|
|
|
70
75
|
* "committed" (commit created but not pushed — public .hunch/ rides the next push; an
|
|
71
76
|
* overlay commit whose merge/push failed retries on the next flush), or null (auto-commit
|
|
72
77
|
* off, no overlay for a private record, or the commit was skipped — lock held, safety
|
|
73
|
-
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up)
|
|
78
|
+
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up),
|
|
79
|
+
* or "held" (a public capture left uncommitted because the default branch is checked
|
|
80
|
+
* out; callers say so with HELD_ON_DEFAULT_BRANCH). */
|
|
74
81
|
export function flushCapture(store, publicHunchDir, isPrivate, message,
|
|
75
82
|
/** Long-lived callers can pin the route snapshot that admitted the write. */
|
|
76
83
|
remoteOverride, observe) {
|
|
84
|
+
return flushStore(store, publicHunchDir, isPrivate, message, remoteOverride, observe, true);
|
|
85
|
+
}
|
|
86
|
+
/** Auto-commit a state-contract write (`nuryel_write`, `hunch serve`). A state root is a
|
|
87
|
+
* dedicated data repository whose writes commit on whatever branch it serves, so the
|
|
88
|
+
* default-branch hold for captures never applies; outcomes as flushCapture otherwise. */
|
|
89
|
+
export function flushStateWrite(store, publicHunchDir, isPrivate, message, remoteOverride) {
|
|
90
|
+
return flushStore(store, publicHunchDir, isPrivate, message, remoteOverride, undefined, false);
|
|
91
|
+
}
|
|
92
|
+
function flushStore(store, publicHunchDir, isPrivate, message, remoteOverride, observe, holdOnDefaultBranch) {
|
|
77
93
|
// Follow the same routing as HunchStore.captureHome: unified ("shared") mode homes
|
|
78
94
|
// EVERY capture in the overlay, so the flush must go there too — one source of truth.
|
|
79
95
|
if (store.captureHome(isPrivate) === "private") {
|
|
@@ -89,10 +105,45 @@ remoteOverride, observe) {
|
|
|
89
105
|
}
|
|
90
106
|
if (!store.autoCommit)
|
|
91
107
|
return null;
|
|
108
|
+
// A capture never commits onto the default branch by itself: task and capture commits
|
|
109
|
+
// landed on main between the user's own. The record stays on disk; the user's next
|
|
110
|
+
// commit carries it, or the next capture on a feature branch sweeps it up. Held before
|
|
111
|
+
// the grounding refresh, so an automatic capture (hook sync, MCP) leaves tracked docs at
|
|
112
|
+
// HEAD instead of counting a record that `git commit -a` would leave behind; by-hand CLI
|
|
113
|
+
// commands refresh them on disk themselves (publicFlushCommitsGrounding in the CLI).
|
|
114
|
+
if (holdOnDefaultBranch && holdsOnDefaultBranch(publicHunchDir))
|
|
115
|
+
return heldOrNothing(publicHunchDir);
|
|
92
116
|
// A public capture changes record counts, so refresh git-clean grounding docs and fold
|
|
93
117
|
// them into the SAME memory commit — otherwise every capture re-stales the committed
|
|
94
118
|
// counts and the release gate's clean-tree check fails on the next CI index.
|
|
95
119
|
const grounding = refreshCommittableGrounding(dirname(publicHunchDir), store);
|
|
96
120
|
return commitAndPushHunch(publicHunchDir, message, { push: false, alsoStage: grounding, observe });
|
|
97
121
|
}
|
|
122
|
+
/** What a caller prints for a "held" flush, so a solo user on main is never left
|
|
123
|
+
* wondering why the capture commit stopped appearing. */
|
|
124
|
+
export const HELD_ON_DEFAULT_BRANCH = "not auto-committed: the default branch is checked out — your next commit that adds .hunch/ carries it (to restore auto-commit there, set \"autoCommitDefaultBranch\": true in .hunch/local.json)";
|
|
125
|
+
/** A hold reports "held" only when there is something left for the next commit; a clean
|
|
126
|
+
* .hunch/ is null, as an unheld flush with nothing staged would be. */
|
|
127
|
+
function heldOrNothing(publicHunchDir) {
|
|
128
|
+
return hunchDirHasChanges(publicHunchDir) ? "held" : null;
|
|
129
|
+
}
|
|
130
|
+
/** Whether an automatic public memory commit must be held: the default branch is checked
|
|
131
|
+
* out and the user has not opted back in. Shared by flushStore and `hunch sync`. */
|
|
132
|
+
export function holdsOnDefaultBranch(publicHunchDir) {
|
|
133
|
+
return !defaultBranchCommitsAllowed(publicHunchDir) && onDefaultBranch(dirname(publicHunchDir));
|
|
134
|
+
}
|
|
135
|
+
/** Opt back in to capture commits on the default branch: `autoCommitDefaultBranch: true`
|
|
136
|
+
* in the per-machine .hunch/local.json, or HUNCH_AUTO_COMMIT_DEFAULT_BRANCH=1. */
|
|
137
|
+
function defaultBranchCommitsAllowed(publicHunchDir) {
|
|
138
|
+
if (process.env.HUNCH_AUTO_COMMIT_DEFAULT_BRANCH === "1")
|
|
139
|
+
return true;
|
|
140
|
+
try {
|
|
141
|
+
const raw = readFileSync(join(publicHunchDir, "local.json"), "utf8");
|
|
142
|
+
const local = JSON.parse(raw.startsWith("\uFEFF") ? raw.slice(1) : raw);
|
|
143
|
+
return !!local && typeof local === "object" && local.autoCommitDefaultBranch === true;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
98
149
|
//# sourceMappingURL=sync.js.map
|
|
@@ -47,7 +47,7 @@ export type SnapshotOutcome = {
|
|
|
47
47
|
status: "written";
|
|
48
48
|
record: Workspace;
|
|
49
49
|
home: "private" | "public";
|
|
50
|
-
flushed: "pushed" | "committed" | null;
|
|
50
|
+
flushed: "pushed" | "committed" | "held" | null;
|
|
51
51
|
};
|
|
52
52
|
/** Record this machine's snapshot. Honors `workspaces.publish`, skips a write when the
|
|
53
53
|
* content is unchanged and the stored record is under a day old (an idle machine's hooks
|
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. */
|