@davesheffer/hunch 1.47.1 → 1.48.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.
@@ -1,10 +1,11 @@
1
1
  import { commitMeta, commitDiff, headSha, currentBranch } from "../extractors/git.js";
2
2
  import { analyzeDiff } from "../extractors/diff.js";
3
- import { selectProvider, selectEnsemble, selectVerifier, verifyDecisionSafe, DeterministicProvider } from "./provider.js";
3
+ import { selectProvider, selectEnsemble, selectVerifier, verifyDecisionSafe, DeterministicProvider, UnusableOutputError } from "./provider.js";
4
4
  import { decisionId, bugId, constraintId } from "../core/ids.js";
5
5
  import { commitCoveredBy } from "../core/dupdetect.js";
6
6
  import { pathMatchesGlob } from "../core/glob.js";
7
7
  import { draftTripwires, knownRepoDeps } from "./tripwires.js";
8
+ import { prContextFor } from "./prContext.js";
8
9
  import { isSubstantive } from "../extractors/languages.js";
9
10
  // Subjects that signal mechanical churn. `chore(deps):` is matched as a literal
10
11
  // prefix on its own: a word-boundary check after the closing parenthesis would
@@ -124,6 +125,15 @@ export async function syncCommit(store, root, sha, opts = {}) {
124
125
  ? await selectProvider({ root })
125
126
  : new DeterministicProvider();
126
127
  const input = { subject: meta.subject, body: meta.body, files: substantiveFiles, diff, analysis };
128
+ // The merged PR's description often holds the why a squash commit drops (#562). Fetch
129
+ // it only when a model will read it: never for a local-only capture (no PR text goes
130
+ // to any provider) and not for the free deterministic draft.
131
+ // The review thread feeds only the Critic-verified path (gated by a noise audit).
132
+ if (!localOnly && provider.name !== "deterministic") {
133
+ const pr = await (opts.prContext ?? prContextFor)(root, { sha: meta.sha, subject: meta.subject }, { thread: wantVerify });
134
+ if (pr)
135
+ input.pr = pr;
136
+ }
127
137
  let draft = await draftDecisionSafe(provider, input);
128
138
  // The Critic pass: audit the draft against the commit, PRUNE unsupported alternatives
129
139
  // (BEFORE they scaffold tripwires below) and consequences, and lower confidence on weak
@@ -137,6 +147,10 @@ export async function syncCommit(store, root, sha, opts = {}) {
137
147
  // were reconciled, their agreement, and the verifier's grounding. Rides in `evidence`
138
148
  // (no schema change → respects forward-migration invariant con_947c578b2c).
139
149
  const synthBits = [`provider=${actualProvider}`];
150
+ if (input.pr)
151
+ synthBits.push(`pr=#${input.pr.number}${input.pr.thread ? "+thread" : ""}`);
152
+ if (draft.fallbackKind)
153
+ synthBits.push(`fallback=${draft.fallbackKind}`);
140
154
  if (draft.samples)
141
155
  synthBits.push(`samples=${draft.samples}`);
142
156
  if (draft.agreement != null)
@@ -227,7 +241,7 @@ export async function syncCommit(store, root, sha, opts = {}) {
227
241
  store.putPrivate("decisions", decision);
228
242
  else
229
243
  store.json.put("decisions", decision);
230
- return { status: "written", decision, provider: actualProvider };
244
+ return { status: "written", decision, provider: actualProvider, ...(draft.fallbackReason ? { fallbackReason: draft.fallbackReason } : {}) };
231
245
  }
232
246
  function putBugInHome(store, bug, home) {
233
247
  return home === "private" ? store.putPrivate("bugs", bug) : store.json.put("bugs", bug);
@@ -516,9 +530,35 @@ function recentDiffFor(root) {
516
530
  const head = headSha(root);
517
531
  return head ? commitDiff(head, root, 12_000) : "";
518
532
  }
533
+ /** Extra samples when a provider answered but its output held no usable record
534
+ * (issue #561): a model that echoed the schema usually answers properly on the
535
+ * next sample. Transport failures (timeout, crash, auth) are not retried, since a
536
+ * second hang would only double the wait before the same fallback. */
537
+ export const UNUSABLE_OUTPUT_RETRIES = 1;
538
+ async function sampleUntilUsable(draft) {
539
+ for (let attempt = 0;; attempt++) {
540
+ try {
541
+ return await draft();
542
+ }
543
+ catch (err) {
544
+ if (!(err instanceof UnusableOutputError) || attempt >= UNUSABLE_OUTPUT_RETRIES)
545
+ throw err;
546
+ }
547
+ }
548
+ }
549
+ /** Short, print-safe label for the `synth:` evidence line, so a fallback from the
550
+ * quiet post-commit hook still leaves a trace `hunch review` can show. */
551
+ function fallbackKindOf(err) {
552
+ return err instanceof UnusableOutputError ? err.kind : "provider-error";
553
+ }
554
+ function fallbackReasonOf(err) {
555
+ if (err instanceof UnusableOutputError)
556
+ return `${err.message}, ${UNUSABLE_OUTPUT_RETRIES + 1} attempts`;
557
+ return err instanceof Error ? err.message : String(err);
558
+ }
519
559
  export async function draftDecisionSafe(provider, input) {
520
560
  try {
521
- return await provider.draftDecision(input);
561
+ return await sampleUntilUsable(() => provider.draftDecision(input));
522
562
  }
523
563
  catch (err) {
524
564
  // A provider failure (network, bad creds, CLI crash, unparseable output) must
@@ -527,17 +567,17 @@ export async function draftDecisionSafe(provider, input) {
527
567
  // Tell the caller a fallback happened (and why) so it reports the provider that
528
568
  // actually drafted the record rather than the one that was selected.
529
569
  const fallback = await new DeterministicProvider().draftDecision(input);
530
- return { ...fallback, fellBackTo: "deterministic", fallbackReason: err instanceof Error ? err.message : String(err) };
570
+ return { ...fallback, fellBackTo: "deterministic", fallbackReason: fallbackReasonOf(err), fallbackKind: fallbackKindOf(err) };
531
571
  }
532
572
  }
533
573
  export async function draftBugSafe(provider, input) {
534
574
  try {
535
- return await provider.draftBug(input);
575
+ return await sampleUntilUsable(() => provider.draftBug(input));
536
576
  }
537
577
  catch (err) {
538
578
  // Same contract as draftDecisionSafe: surface the fallback and its cause.
539
579
  const fallback = await new DeterministicProvider().draftBug(input);
540
- return { ...fallback, fellBackTo: "deterministic", fallbackReason: err instanceof Error ? err.message : String(err) };
580
+ return { ...fallback, fellBackTo: "deterministic", fallbackReason: fallbackReasonOf(err) };
541
581
  }
542
582
  }
543
583
  //# sourceMappingURL=synthesize.js.map
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.47.1",
3
+ "version": "1.48.0",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
- "license": "Apache-2.0",
5
+ "license": "FSL-1.1-ALv2",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
7
7
  "description": "A shared record for AI agents: decisions, completed work, and commitments, with sources. Git-native engineering memory and a self-hosted state server.",
8
8
  "homepage": "https://www.hunchmemory.com",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://www.hunchmemory.com",
10
- "version": "1.47.1",
10
+ "version": "1.48.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.47.1",
16
+ "version": "1.48.0",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {