@davesheffer/hunch 1.31.1 → 1.32.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.
Files changed (223) hide show
  1. package/README.md +2 -1
  2. package/dist/cli/automaticReviewMemory.d.ts +13 -0
  3. package/dist/cli/dna.d.ts +2 -0
  4. package/dist/cli/index.d.ts +16 -0
  5. package/dist/cli/index.js +606 -43
  6. package/dist/cli/integrations.d.ts +2 -0
  7. package/dist/cli/integrations.js +5 -2
  8. package/dist/cli/invocation.d.ts +35 -0
  9. package/dist/cli/preflight.d.ts +1 -0
  10. package/dist/cli/reviewMemory.d.ts +4 -0
  11. package/dist/cli/reviewMemoryProvider.d.ts +14 -0
  12. package/dist/cli/serve.d.ts +2 -0
  13. package/dist/cli/taskReport.d.ts +6 -0
  14. package/dist/cli/taskReport.js +128 -0
  15. package/dist/cli/update.d.ts +14 -0
  16. package/dist/client/state.d.ts +248 -0
  17. package/dist/constitution/adapters.d.ts +29 -0
  18. package/dist/constitution/behaviorAttestationBinding.d.ts +3 -0
  19. package/dist/constitution/behaviorEvaluator.d.ts +8 -0
  20. package/dist/constitution/behaviorProof.d.ts +9 -0
  21. package/dist/constitution/behaviorWorkspace.d.ts +25 -0
  22. package/dist/constitution/bootstrap.d.ts +30 -0
  23. package/dist/constitution/canonical.d.ts +15 -0
  24. package/dist/constitution/card.d.ts +58 -0
  25. package/dist/constitution/compiler.d.ts +52 -0
  26. package/dist/constitution/composition.d.ts +10 -0
  27. package/dist/constitution/corpus.d.ts +7 -0
  28. package/dist/constitution/correctionPolicyMaterializer.d.ts +57 -0
  29. package/dist/constitution/delta.d.ts +4 -0
  30. package/dist/constitution/disposition.d.ts +20 -0
  31. package/dist/constitution/evaluator.d.ts +47 -0
  32. package/dist/constitution/experiment.d.ts +466 -0
  33. package/dist/constitution/experimentRunner.d.ts +7 -0
  34. package/dist/constitution/g2.d.ts +209 -0
  35. package/dist/constitution/g2BehaviorAttestation.d.ts +45 -0
  36. package/dist/constitution/g2BehaviorCandidates.d.ts +123 -0
  37. package/dist/constitution/g2BehaviorDependencies.d.ts +83 -0
  38. package/dist/constitution/g2BehaviorMaterialization.d.ts +77 -0
  39. package/dist/constitution/g2BehaviorPolicyMaterializer.d.ts +48 -0
  40. package/dist/constitution/g2CandidateAttestation.d.ts +43 -0
  41. package/dist/constitution/g2Candidates.d.ts +80 -0
  42. package/dist/constitution/g2Drills.d.ts +33 -0
  43. package/dist/constitution/g3.d.ts +311 -0
  44. package/dist/constitution/g3Conformance.d.ts +33 -0
  45. package/dist/constitution/lifecycle.d.ts +25 -0
  46. package/dist/constitution/mutation.d.ts +15 -0
  47. package/dist/constitution/nodeTestEvidence.d.ts +13 -0
  48. package/dist/constitution/plan.d.ts +18 -0
  49. package/dist/constitution/policyRuntime.d.ts +5 -0
  50. package/dist/constitution/proof.d.ts +11 -0
  51. package/dist/constitution/repairPolicies.d.ts +34 -0
  52. package/dist/constitution/replacementFreeGit.d.ts +14 -0
  53. package/dist/constitution/replay.d.ts +40 -0
  54. package/dist/constitution/replayCache.d.ts +14 -0
  55. package/dist/constitution/replayWorker.d.ts +1 -0
  56. package/dist/constitution/repository.d.ts +129 -0
  57. package/dist/constitution/safeCheckout.d.ts +9 -0
  58. package/dist/constitution/schema.d.ts +1763 -0
  59. package/dist/constitution/scorecard.d.ts +219 -0
  60. package/dist/constitution/service.d.ts +398 -0
  61. package/dist/constitution/shadow.d.ts +54 -0
  62. package/dist/constitution/sourceMutation.d.ts +15 -0
  63. package/dist/constitution/staticGraphBaseline.d.ts +13 -0
  64. package/dist/constitution/structural.d.ts +64 -0
  65. package/dist/core/agenthook.d.ts +63 -0
  66. package/dist/core/agenthook.js +1 -0
  67. package/dist/core/automaticReviewMemory.d.ts +60 -0
  68. package/dist/core/autoreview.d.ts +66 -0
  69. package/dist/core/canonicalOrder.d.ts +3 -0
  70. package/dist/core/capturetoken.d.ts +19 -0
  71. package/dist/core/changeIdentity.d.ts +22 -0
  72. package/dist/core/changeProof.d.ts +14 -0
  73. package/dist/core/checkreport.d.ts +140 -0
  74. package/dist/core/commitrepair.d.ts +171 -0
  75. package/dist/core/commitrepair.js +238 -0
  76. package/dist/core/compare.d.ts +21 -0
  77. package/dist/core/config.d.ts +20 -0
  78. package/dist/core/conformance.d.ts +35 -0
  79. package/dist/core/constraintmatch.d.ts +54 -0
  80. package/dist/core/correction.d.ts +42 -0
  81. package/dist/core/correctionStage.d.ts +224 -0
  82. package/dist/core/declarationClusters.d.ts +149 -0
  83. package/dist/core/delivery.d.ts +110 -0
  84. package/dist/core/docanchors.d.ts +32 -0
  85. package/dist/core/docscan.d.ts +26 -0
  86. package/dist/core/drift.d.ts +13 -0
  87. package/dist/core/drift.js +31 -1
  88. package/dist/core/dupdetect.d.ts +43 -0
  89. package/dist/core/escalations.d.ts +129 -0
  90. package/dist/core/escalations.js +165 -0
  91. package/dist/core/events.d.ts +29 -0
  92. package/dist/core/evidenceMap.d.ts +164 -0
  93. package/dist/core/externalImports.d.ts +8 -0
  94. package/dist/core/format.d.ts +13 -0
  95. package/dist/core/glob.d.ts +9 -0
  96. package/dist/core/groundingLag.d.ts +84 -0
  97. package/dist/core/groundingLag.js +16 -2
  98. package/dist/core/groundingMerge.d.ts +15 -0
  99. package/dist/core/groundingMerge.js +95 -0
  100. package/dist/core/hookObservations.d.ts +9 -0
  101. package/dist/core/hookObservations.js +33 -0
  102. package/dist/core/hookcache.d.ts +10 -0
  103. package/dist/core/hookpolicy.d.ts +47 -0
  104. package/dist/core/ids.d.ts +36 -0
  105. package/dist/core/importReview.d.ts +30 -0
  106. package/dist/core/io.d.ts +23 -0
  107. package/dist/core/jsonc.d.ts +4 -0
  108. package/dist/core/landscapeAdoption.d.ts +66 -0
  109. package/dist/core/landscapeDelivery.d.ts +76 -0
  110. package/dist/core/memorylog.d.ts +37 -0
  111. package/dist/core/migrate.d.ts +28 -0
  112. package/dist/core/outcomeExperience.d.ts +89 -0
  113. package/dist/core/overlaySafety.d.ts +24 -0
  114. package/dist/core/overlaySafety.js +7 -1
  115. package/dist/core/paths.d.ts +31 -0
  116. package/dist/core/pipeline.d.ts +289 -0
  117. package/dist/core/premises.d.ts +43 -0
  118. package/dist/core/provenance.d.ts +21 -0
  119. package/dist/core/publication.d.ts +54 -0
  120. package/dist/core/refrepair.d.ts +24 -0
  121. package/dist/core/refrepair.js +1 -1
  122. package/dist/core/relativeImports.d.ts +11 -0
  123. package/dist/core/repair.d.ts +44 -0
  124. package/dist/core/repairqueue.d.ts +40 -0
  125. package/dist/core/repairqueue.js +133 -0
  126. package/dist/core/reviewMemory.d.ts +40 -0
  127. package/dist/core/reviewqueue.d.ts +54 -0
  128. package/dist/core/safeRepoFile.d.ts +20 -0
  129. package/dist/core/served.d.ts +57 -0
  130. package/dist/core/served.js +14 -0
  131. package/dist/core/stateContract.d.ts +979 -0
  132. package/dist/core/stateDelivery.d.ts +78 -0
  133. package/dist/core/stateRecords.d.ts +383 -0
  134. package/dist/core/stats.d.ts +88 -0
  135. package/dist/core/strictgate.d.ts +33 -0
  136. package/dist/core/taskReport.d.ts +226 -0
  137. package/dist/core/taskReport.js +514 -0
  138. package/dist/core/taskReportCapture.d.ts +8 -0
  139. package/dist/core/taskReportCapture.js +36 -0
  140. package/dist/core/taskReportEvidence.d.ts +28 -0
  141. package/dist/core/taskReportEvidence.js +272 -0
  142. package/dist/core/taskReportHook.d.ts +20 -0
  143. package/dist/core/taskReportHook.js +76 -0
  144. package/dist/core/taskReportPaths.d.ts +3 -0
  145. package/dist/core/taskReportPaths.js +23 -0
  146. package/dist/core/taskReportPublic.d.ts +31 -0
  147. package/dist/core/taskReportPublic.js +45 -0
  148. package/dist/core/taskReportRender.d.ts +6 -0
  149. package/dist/core/taskReportRender.js +97 -0
  150. package/dist/core/topics.d.ts +73 -0
  151. package/dist/core/types.d.ts +1239 -0
  152. package/dist/core/version.d.ts +10 -0
  153. package/dist/eval/guards.d.ts +45 -0
  154. package/dist/eval/harness.d.ts +58 -0
  155. package/dist/extractors/adrImport.d.ts +59 -0
  156. package/dist/extractors/comments.d.ts +7 -0
  157. package/dist/extractors/correctionSources.d.ts +11 -0
  158. package/dist/extractors/diff.d.ts +40 -0
  159. package/dist/extractors/git.d.ts +304 -0
  160. package/dist/extractors/git.js +208 -20
  161. package/dist/extractors/helm.d.ts +31 -0
  162. package/dist/extractors/indexer.d.ts +55 -0
  163. package/dist/extractors/landscapeDiscovery.d.ts +41 -0
  164. package/dist/extractors/languages.d.ts +77 -0
  165. package/dist/extractors/nativeTreeSitter.d.ts +16 -0
  166. package/dist/extractors/parse.d.ts +54 -0
  167. package/dist/extractors/php.d.ts +26 -0
  168. package/dist/extractors/repoSource.d.ts +53 -0
  169. package/dist/extractors/testreport.d.ts +36 -0
  170. package/dist/integrations/ciAction.d.ts +8 -0
  171. package/dist/integrations/claudeConfig.d.ts +44 -0
  172. package/dist/integrations/claudemd.d.ts +13 -0
  173. package/dist/integrations/claudemd.js +11 -2
  174. package/dist/integrations/gitignore.d.ts +16 -0
  175. package/dist/integrations/gitignore.js +7 -0
  176. package/dist/integrations/health.d.ts +74 -0
  177. package/dist/integrations/health.js +32 -2
  178. package/dist/integrations/hooks.d.ts +41 -0
  179. package/dist/integrations/hooks.js +92 -66
  180. package/dist/integrations/madrExport.d.ts +39 -0
  181. package/dist/integrations/madrManifest.d.ts +69 -0
  182. package/dist/integrations/mergeDriver.d.ts +3 -0
  183. package/dist/integrations/mergeDriver.js +25 -8
  184. package/dist/integrations/probe.d.ts +4 -0
  185. package/dist/integrations/providers.d.ts +103 -0
  186. package/dist/integrations/scaffold.d.ts +29 -0
  187. package/dist/integrations/sync.d.ts +30 -0
  188. package/dist/integrations/sync.js +3 -2
  189. package/dist/integrations/team.d.ts +70 -0
  190. package/dist/integrations/worktree.d.ts +8 -0
  191. package/dist/mcp/roots.d.ts +16 -0
  192. package/dist/mcp/server.d.ts +36 -0
  193. package/dist/mcp/server.js +74 -22
  194. package/dist/mcp/taskReportTools.d.ts +188 -0
  195. package/dist/mcp/taskReportTools.js +147 -0
  196. package/dist/serve/app.d.ts +36 -0
  197. package/dist/serve/config.d.ts +100 -0
  198. package/dist/serve/writelock.d.ts +22 -0
  199. package/dist/store/changeLedger.d.ts +132 -0
  200. package/dist/store/compact.d.ts +33 -0
  201. package/dist/store/db.d.ts +9 -0
  202. package/dist/store/embedder.d.ts +32 -0
  203. package/dist/store/hunchStore.d.ts +624 -0
  204. package/dist/store/hunchStore.js +6 -3
  205. package/dist/store/jsonStore.d.ts +125 -0
  206. package/dist/store/merge.d.ts +39 -0
  207. package/dist/store/privateMigrate.d.ts +24 -0
  208. package/dist/store/replay.d.ts +56 -0
  209. package/dist/store/schema.d.ts +17 -0
  210. package/dist/store/stateBinding.d.ts +145 -0
  211. package/dist/store/stateCapture.d.ts +15 -0
  212. package/dist/synthesis/cliAdapter.d.ts +22 -0
  213. package/dist/synthesis/initiator.d.ts +13 -0
  214. package/dist/synthesis/provider.d.ts +307 -0
  215. package/dist/synthesis/synthesize.d.ts +117 -0
  216. package/dist/synthesis/tripwires.d.ts +9 -0
  217. package/dist/taskReports.d.ts +79 -0
  218. package/dist/taskReports.js +69 -0
  219. package/dist/wiki/adopt.d.ts +22 -0
  220. package/dist/wiki/graph.d.ts +87 -0
  221. package/dist/wiki/wiki.d.ts +242 -0
  222. package/package.json +6 -5
  223. package/server.json +2 -2
@@ -1,5 +1,40 @@
1
1
  import { topicCollisions } from "./topics.js";
2
+ import { liveRewrites, deadRewrites, commitRepairReviewHash, firstFor } from "./commitrepair.js";
2
3
  import { importedAdrReviewHash, importedAdrSourceHash, pendingImportedAdrReviews } from "./importReview.js";
4
+ /** Escalations gating surfaces (CI exit codes, "needs your call" counts/lists)
5
+ * should act on: everything except an entry explicitly marked
6
+ * actionable:false (#61). The full list — including non-actionable
7
+ * entries — still belongs in any transparency-facing output (e.g. --json). */
8
+ export function actionableEscalations(items) {
9
+ return items.filter((e) => e.actionable !== false);
10
+ }
11
+ /** One computation for every "N need your call (+M shown for context)"
12
+ * consumer (CLI `escalations`, SessionStart, the `hunch_now`/`hunch_escalations`
13
+ * MCP tools) — introduced alongside `actionable` (#61) specifically so a
14
+ * future consumer of Escalation[] can't repeat the mistake of hand-rolling
15
+ * the filter and forgetting it (the VS Code panel did, before this existed). */
16
+ export function summarizeEscalations(items) {
17
+ const actionable = actionableEscalations(items);
18
+ return { actionable, context: items.length - actionable.length };
19
+ }
20
+ /** The ONE "N need your call (+M shown for context)" headline, shared by the
21
+ * CLI `escalations` command and the `hunch_escalations` MCP tool (#61) —
22
+ * including its own "nothing actionable, but N item(s) queued for context"
23
+ * fallback, so that unreachable-today prose exists in exactly one place
24
+ * instead of being hand-copied per consumer (the copy-paste is how the VS
25
+ * Code panel missed the filter entirely before `actionable` existed).
26
+ * Callers are expected to have already special-cased `items.length === 0`
27
+ * (a plainer "nothing needs your decision" message, no tally at all). */
28
+ export function escalationHeadline(items, audience) {
29
+ const { actionable, context } = summarizeEscalations(items);
30
+ if (actionable.length) {
31
+ const call = audience === "cli" ? "your call — asked here, never decided for you" : "the human's call — ask each inline, don't decide it for them";
32
+ const note = audience === "cli" ? "not gating" : "not a decision";
33
+ return `${actionable.length} decision(s) need ${call}${context ? ` (+${context} shown below for context only, ${note})` : ""}:`;
34
+ }
35
+ const whose = audience === "cli" ? "your decision" : "the human's call";
36
+ return `Nothing needs ${whose} right now — ${items.length} entr${items.length === 1 ? "y" : "ies"} shown below for context only (resolving another entry will clear them):`;
37
+ }
3
38
  /** The decisions a human must make NOW, to be asked INLINE. Empty in a healthy graph. */
4
39
  export function pendingEscalations(decisions) {
5
40
  const out = [];
@@ -35,6 +70,136 @@ export function pendingEscalations(decisions) {
35
70
  }
36
71
  return out;
37
72
  }
73
+ /** A commit-provenance repair the post-merge hook detected and queued
74
+ * (src/core/repairqueue.ts) but never applied — the match signal (related_files
75
+ * overlap) isn't strong enough to write unattended, so it's a question for the
76
+ * human, exactly like every other entry here, never a silent write.
77
+ *
78
+ * `decisions` is the caller's VISIBLE scope (supplies the title only) —
79
+ * MCP/SessionStart pass store.advisoryRecs(), which is public-only in
80
+ * private mode. `live` is the scope liveRewrites checks the repair against;
81
+ * it defaults to `decisions` but callers on an advisory scope must pass the
82
+ * FULL store (store.recs()) here, since repair-provenance itself reads the
83
+ * full store — an overlay decision's repair is fully answerable even where
84
+ * its title isn't visible, and must not go silent just because the title is
85
+ * private.
86
+ *
87
+ * `withheld` (from repairqueue.ts's withheldRewrites — a git-facing check
88
+ * this module deliberately stays free of, so the caller runs it and passes
89
+ * the result in) names entries whose proposed `to` doesn't resolve to a
90
+ * real commit here. `--apply --only <id>` can never resolve one of these —
91
+ * advertising it as the answer would be advice guaranteed to no-op
92
+ * forever, which is worse than not asking at all. Such an entry still
93
+ * needs a human (only `--drop` can retire it), so it still escalates, just
94
+ * with wording that doesn't promise `--apply` will help.
95
+ *
96
+ * Keyed by OBJECT, not by id, matching withheldForUnresolvableTo's own
97
+ * contract: a corrupted queue file can carry two entries sharing an id
98
+ * (one resolvable, one not) — an id-keyed set couldn't tell them apart and
99
+ * would wrongly tag the resolvable sibling as unresolvable too. Every
100
+ * caller of this function already passes the SAME `queued` array to both
101
+ * this function and withheldRewrites, so identity holds. */
102
+ export function commitRepairEscalations(queued, decisions, live = decisions, withheld = new Set()) {
103
+ const reviewHash = commitRepairReviewHash(queued, withheld);
104
+ const byId = new Map(decisions.map((d) => [d.id, d]));
105
+ // Mirrors two of the three queue transformations --apply/--drop actually see
106
+ // (src/cli/index.ts): dead entries (deadRewrites) are pruned via `save()`
107
+ // before either command ever reads `queue`, so a duplicate-id check against
108
+ // the RAW `queued` array can name an entry that's already gone by the time a
109
+ // human acts (#59). Deliberately does NOT model the fresh-detection merge
110
+ // (mergeRewrites) that also runs before either command: a fresh match can
111
+ // silently evict firstFor(queue, id) with no tombstone. That gap predates
112
+ // this fix (see #56) and isn't new here — it just means a queue whose first
113
+ // entry is about to be evicted by a fresh match still reads as the ordinary,
114
+ // ask-normally case rather than something rarer.
115
+ // deadRewrites (below) and liveRewrites (in the return) are BOTH called with
116
+ // this same `live` array, so for any one id they agree on the same single
117
+ // decision (or the same absence) governing it. That decision's `commit`/
118
+ // `status` are fixed for the whole id-group — only each entry's OWN `from`
119
+ // varies — so liveRewrites' and deadRewrites' predicates are exact
120
+ // complements entry-by-entry (never uniform across the group: two entries
121
+ // sharing an id can freely land on opposite sides, see the "dead first
122
+ // sibling" test, #59). `survivors` (queued minus dead) restricted to one id
123
+ // is therefore exactly that id's live entries, in the same queue order.
124
+ // Consequence relied on below (#61): the FIRST live entry per id is always
125
+ // `dropTarget(id)`, so it always stays actionable — no id-group's
126
+ // escalations can ever be entirely actionable:false.
127
+ const deadSet = new Set(deadRewrites(queued, live));
128
+ const survivors = queued.filter((q) => !deadSet.has(q));
129
+ // What `--drop <id>` targets (src/cli/index.ts: `firstFor(queue, opts.drop)`
130
+ // on the post-prune queue) — --drop doesn't care whether an entry's `to`
131
+ // resolves, so a withheld entry can still be the drop target.
132
+ const dropTarget = (id) => firstFor(survivors, id);
133
+ // What `--apply --only <id>` targets: its plan is built from
134
+ // withheldForUnresolvableTo's `applicable` half (src/cli/index.ts:
135
+ // `plan.rewrites = applicable`), which excludes withheld entries entirely —
136
+ // so the apply-target can differ from the drop-target when an earlier
137
+ // survivor for the same id is withheld. Routed through the shared `firstFor`
138
+ // rather than hand-rolled, so this can't independently drift from
139
+ // commitrepair.ts's own first-match rule (already drifted three times
140
+ // across call sites there — #51/#53/#56 — before firstFor consolidated it).
141
+ const applyTarget = (id) => firstFor(survivors.filter((q) => !withheld.has(q)), id);
142
+ return liveRewrites(queued, live)
143
+ .map((r) => {
144
+ const title = byId.get(r.id)?.title;
145
+ const named = `${r.id}${title ? ` ("${title}")` : ""}`;
146
+ const base = { kind: "commit-repair-pending", topic: r.id, decisionIds: [r.id], detail: `${r.from} → ${r.to}` };
147
+ const applyQuestion = `${named}'s commit is no longer reachable from HEAD (likely squash-merged away), and one newly-merged commit touches all its related files — apply the proposed replacement?`;
148
+ const isDropTarget = dropTarget(r.id) === r;
149
+ const isApplyTarget = applyTarget(r.id) === r;
150
+ if (!isDropTarget && !isApplyTarget) {
151
+ // A duplicate-id queue (corrupted file, hand edit, or a bug upstream —
152
+ // #53/#55/#56/#58): an earlier survivor for this id is what BOTH
153
+ // commands would act on, never this entry (#59). If this entry is
154
+ // itself withheld, say so too — that fact doesn't disappear just
155
+ // because it's also unreachable by id right now, and it stays
156
+ // drop-only even once it's next in line (never permanently: a fresh
157
+ // detection run can still supersede it with a resolvable match, same
158
+ // as the ordinary withheld branch below).
159
+ const alsoWithheld = withheld.has(r)
160
+ ? " — and its own proposed replacement doesn't resolve here either, so even once it's next in line it stays drop-only unless a fresh detection run supersedes it with a resolvable match"
161
+ : "";
162
+ return {
163
+ ...base,
164
+ actionable: false,
165
+ question: `${named} has a further queued replacement candidate (${r.from} → ${r.to}) sitting behind another entry for the same decision — leave it queued for now?`,
166
+ // Dropping the entry ahead brings this one back into play on the next
167
+ // run (deadRewrites can no longer see a reason to prune it). Applying
168
+ // it does the OPPOSITE for this entry: repairDecisionCommit moves the
169
+ // decision's `commit` past the `from` both entries share, so the next
170
+ // run's deadRewrites prunes THIS entry as stale — it is never
171
+ // reconsidered, not "resolved the normal way".
172
+ resolution: `not directly actionable by id right now: \`hunch repair-provenance --apply --only ${r.id} --expect ${reviewHash}\`/\`hunch repair-provenance --drop ${r.id} --expect ${reviewHash}\` both act on an entry queued ahead of it, never this one${alsoWithheld}. Dropping that entry brings this one back into consideration on the next run; applying it instead retires this one as stale, since the decision moves past the \`from\` both entries share.`,
173
+ };
174
+ }
175
+ if (!isDropTarget) {
176
+ // isApplyTarget is true here: this entry IS what --apply --only would
177
+ // apply, but an earlier, WITHHELD sibling for the same id sits ahead
178
+ // of it, so --drop <id> would tombstone that sibling instead (#59).
179
+ return {
180
+ ...base,
181
+ question: applyQuestion,
182
+ resolution: `hunch repair-provenance --apply --only ${r.id} --expect ${reviewHash} to accept just this one — but \`--drop ${r.id}\` won't reject THIS entry: an earlier queued sibling for the same id (whose own replacement doesn't resolve here) sits ahead of it and would be tombstoned instead.`,
183
+ };
184
+ }
185
+ if (!isApplyTarget) {
186
+ // isDropTarget is true here, so this can only mean withheld.has(r):
187
+ // a non-withheld first survivor is always its own apply-target too
188
+ // (nothing withheld can precede it if it isn't withheld itself), so
189
+ // isDropTarget && !isApplyTarget and "withheld" are the same fact.
190
+ return {
191
+ ...base,
192
+ question: `${named}'s commit is no longer reachable from HEAD (likely squash-merged away), and the queued replacement commit doesn't resolve in this repository (corrupted queue entry, or the commit has since been garbage-collected) — reject it?`,
193
+ resolution: `this entry can never be applied as-is — \`hunch repair-provenance --apply\` will leave it queued every run; \`hunch repair-provenance --drop ${r.id} --expect ${reviewHash}\` to reject it, or wait for a fresh match to supersede it`,
194
+ };
195
+ }
196
+ return {
197
+ ...base,
198
+ question: applyQuestion,
199
+ resolution: `hunch repair-provenance --apply --only ${r.id} --expect ${reviewHash} to accept just this one, hunch repair-provenance --drop ${r.id} --expect ${reviewHash} to reject it (tombstoned durably — this same match won't resurface, though a genuinely different candidate still can), or leave it queued to decide later`,
200
+ };
201
+ });
202
+ }
38
203
  /** The Constitution's genuine human moments (§59.5.3), framed as inline questions:
39
204
  * a candidate awaiting review, and a proposed policy whose next step (prove, or
40
205
  * accept/reject) is a human call. Machine conclusions never appear here as
@@ -0,0 +1,29 @@
1
+ import type { HunchPaths } from "./paths.js";
2
+ /** One enforcement event. `kind` is open-ended so the conformance/drift gates can
3
+ * append their own shapes later; `subject`/`object`/`assert` stay OPTIONAL and
4
+ * are populated ONLY by a gate that genuinely has them (never a plain block). */
5
+ export interface HunchEvent {
6
+ /** ISO instant the gate fired. */
7
+ at: string;
8
+ kind: "constraint" | "veto" | "conformance" | "drift";
9
+ /** Repo-relative file the blocked edit targeted. */
10
+ file: string;
11
+ /** Decision the block enforces (veto/conformance carry this; a bare constraint may not). */
12
+ decision?: string;
13
+ /** Constraint id, when the block came from a blocking invariant. */
14
+ constraint?: string;
15
+ /** Human-readable rule text, for receipts. */
16
+ statement?: string;
17
+ /** Conformance-only predicate shape — never set by a constraint/veto block. */
18
+ subject?: string;
19
+ object?: string;
20
+ assert?: string;
21
+ }
22
+ export declare function eventsLogPath(paths: HunchPaths): string;
23
+ /** Append one event as a JSONL line. Best-effort and never throws: the primary
24
+ * call site is the edit hook, which MUST NEVER break an agent on failure
25
+ * (con_03a0b94b2e). A dropped catch-log line is an acceptable loss. */
26
+ export declare function appendEvent(paths: HunchPaths, event: HunchEvent): void;
27
+ /** Read + parse the catch-log. Malformed lines are skipped, not fatal (the log is
28
+ * derived; one bad line never poisons the aggregation). Missing log → []. */
29
+ export declare function readEvents(paths: HunchPaths): HunchEvent[];
@@ -0,0 +1,164 @@
1
+ import { z } from "zod";
2
+ export declare const EvidenceOutcomeSchema: z.ZodEnum<{
3
+ error: "error";
4
+ red: "red";
5
+ green: "green";
6
+ "not-run": "not-run";
7
+ }>;
8
+ export declare const EvidenceOwnerSchema: z.ZodString;
9
+ export declare const EvidenceProbeSchema: z.ZodObject<{
10
+ target_before: z.ZodEnum<{
11
+ error: "error";
12
+ red: "red";
13
+ green: "green";
14
+ "not-run": "not-run";
15
+ }>;
16
+ control_before: z.ZodEnum<{
17
+ error: "error";
18
+ red: "red";
19
+ green: "green";
20
+ "not-run": "not-run";
21
+ }>;
22
+ target_after: z.ZodOptional<z.ZodEnum<{
23
+ error: "error";
24
+ red: "red";
25
+ green: "green";
26
+ "not-run": "not-run";
27
+ }>>;
28
+ control_after: z.ZodOptional<z.ZodEnum<{
29
+ error: "error";
30
+ red: "red";
31
+ green: "green";
32
+ "not-run": "not-run";
33
+ }>>;
34
+ }, z.core.$strict>;
35
+ export declare const EvidenceExecutionSchema: z.ZodObject<{
36
+ owner: z.ZodString;
37
+ target_count: z.ZodNumber;
38
+ control_count: z.ZodNumber;
39
+ }, z.core.$strict>;
40
+ export declare const EvidenceInterventionSchema: z.ZodObject<{
41
+ owner: z.ZodString;
42
+ mutation_id: z.ZodOptional<z.ZodString>;
43
+ target_after: z.ZodEnum<{
44
+ error: "error";
45
+ red: "red";
46
+ green: "green";
47
+ "not-run": "not-run";
48
+ }>;
49
+ control_after: z.ZodEnum<{
50
+ error: "error";
51
+ red: "red";
52
+ green: "green";
53
+ "not-run": "not-run";
54
+ }>;
55
+ }, z.core.$strict>;
56
+ export declare const VerifiedEvidenceReceiptSchema: z.ZodObject<{
57
+ version: z.ZodLiteral<1>;
58
+ claim: z.ZodString;
59
+ probe: z.ZodObject<{
60
+ target_before: z.ZodEnum<{
61
+ error: "error";
62
+ red: "red";
63
+ green: "green";
64
+ "not-run": "not-run";
65
+ }>;
66
+ control_before: z.ZodEnum<{
67
+ error: "error";
68
+ red: "red";
69
+ green: "green";
70
+ "not-run": "not-run";
71
+ }>;
72
+ target_after: z.ZodOptional<z.ZodEnum<{
73
+ error: "error";
74
+ red: "red";
75
+ green: "green";
76
+ "not-run": "not-run";
77
+ }>>;
78
+ control_after: z.ZodOptional<z.ZodEnum<{
79
+ error: "error";
80
+ red: "red";
81
+ green: "green";
82
+ "not-run": "not-run";
83
+ }>>;
84
+ }, z.core.$strict>;
85
+ execution: z.ZodDefault<z.ZodArray<z.ZodObject<{
86
+ owner: z.ZodString;
87
+ target_count: z.ZodNumber;
88
+ control_count: z.ZodNumber;
89
+ }, z.core.$strict>>>;
90
+ interventions: z.ZodDefault<z.ZodArray<z.ZodObject<{
91
+ owner: z.ZodString;
92
+ mutation_id: z.ZodOptional<z.ZodString>;
93
+ target_after: z.ZodEnum<{
94
+ error: "error";
95
+ red: "red";
96
+ green: "green";
97
+ "not-run": "not-run";
98
+ }>;
99
+ control_after: z.ZodEnum<{
100
+ error: "error";
101
+ red: "red";
102
+ green: "green";
103
+ "not-run": "not-run";
104
+ }>;
105
+ }, z.core.$strict>>>;
106
+ }, z.core.$strict>;
107
+ export type EvidenceOutcome = z.infer<typeof EvidenceOutcomeSchema>;
108
+ export type VerifiedEvidenceReceipt = z.infer<typeof VerifiedEvidenceReceiptSchema>;
109
+ export interface EvidenceFile {
110
+ path: string;
111
+ target_execution_owners: string[];
112
+ target_only_owners: string[];
113
+ shared_execution_owners: string[];
114
+ strong_differential_owners: string[];
115
+ strong_differential_support: number;
116
+ behavior_sensitive_owners: string[];
117
+ evidence: Array<"target-execution" | "target-only-execution" | "shared-execution" | "strong-differential-execution" | "behavior-sensitive">;
118
+ }
119
+ export interface VerifiedEvidenceMap {
120
+ version: 1;
121
+ claim: string;
122
+ level: "unverified" | "probe-authenticated" | "execution-verified" | "behavior-sensitive";
123
+ verification: {
124
+ authenticated: boolean;
125
+ target_before: EvidenceOutcome;
126
+ control_before: EvidenceOutcome;
127
+ reason: string;
128
+ };
129
+ closure: {
130
+ status: "unverified" | "open" | "still-red" | "closed" | "control-unchecked" | "control-regressed" | "probe-error";
131
+ target_after: EvidenceOutcome | null;
132
+ control_after: EvidenceOutcome | null;
133
+ };
134
+ execution_slice: {
135
+ target_observed_owners: string[];
136
+ target_only_owners: string[];
137
+ shared_owners: string[];
138
+ strong_differential_owners: string[];
139
+ strong_differential_files: string[];
140
+ strong_differential: Array<{
141
+ owner: string;
142
+ target_count: number;
143
+ control_count: number;
144
+ ratio: number;
145
+ }>;
146
+ };
147
+ intervention_slice: {
148
+ admitted_receipts: number;
149
+ behavior_sensitive_owners: string[];
150
+ behavior_sensitive_files: string[];
151
+ };
152
+ files: EvidenceFile[];
153
+ owner_claim: {
154
+ enabled: false;
155
+ owner: null;
156
+ reason: string;
157
+ };
158
+ limitations: string[];
159
+ }
160
+ /** Compile externally observed receipts into a bounded evidence map. This is
161
+ * deliberately pure: it runs no probe, edits no source, and never turns causal
162
+ * influence into a correction-owner claim. */
163
+ export declare function compileVerifiedEvidenceMap(value: unknown): VerifiedEvidenceMap;
164
+ export declare function formatVerifiedEvidenceMap(map: VerifiedEvidenceMap): string;
@@ -0,0 +1,8 @@
1
+ /** Canonical package identity for a static module specifier. Relative, absolute,
2
+ * package-import-map (#), and URL-like specifiers are repository/runtime-local
3
+ * and deliberately outside the external-dependency evaluator. */
4
+ export declare function externalPackage(specifier: string): string | null;
5
+ /** Virtual graph target for an external package. It is intentionally not a
6
+ * Component record: package facts stay a bounded evaluator layer and do not
7
+ * inflate the human-curated component graph. */
8
+ export declare function externalImportNodeId(specifier: string): string | null;
@@ -0,0 +1,13 @@
1
+ /** Render an AssembledContext as a compact, agent-ready brief: invariants first
2
+ * (what must not break), then the why, blast radius, and bug history — each with
3
+ * provenance so the agent can weight it. Shared by the CLI and the MCP tool. */
4
+ import type { AssembledContext, SearchHit, StructureView } from "../store/hunchStore.js";
5
+ import { type DeliveryOptions } from "./delivery.js";
6
+ export declare function formatContext(ctx: AssembledContext, options?: DeliveryOptions): string;
7
+ /** One search hit as `hunch query` / hunch_query print it (headline + indented detail line).
8
+ * Graph records keep their `[kind] id — title` shape; a nuryel.state/1 hit renders through
9
+ * renderStateLine (`[commitment/in_force] customer:Site:7 — "send report" due … (owner …)`)
10
+ * with the record id on the detail line, so both readers say the same thing. */
11
+ export declare function formatSearchHit(hit: SearchHit, record: unknown): string;
12
+ /** Render a StructureView as a compact orientation brief (hunch_structure). */
13
+ export declare function formatStructure(v: StructureView): string;
@@ -0,0 +1,9 @@
1
+ /** Minimal, segment-aware glob matching for constraint `scope` / component
2
+ * `paths` (e.g. "src/auth/**"). Supports **, *, and ? with correct path-segment
3
+ * semantics (`**` spans separators; `*`/`?` stay within one segment). No dep. */
4
+ /** Does a concrete path match a glob? Also returns true when the glob is a bare
5
+ * directory prefix of the path (so "src/auth" matches "src/auth/x.ts"). */
6
+ export declare function pathMatchesGlob(path: string, glob: string): boolean;
7
+ /** Do two concrete repo paths identify the same path at different prefix depth?
8
+ * Segment-anchored: `x/scenario.ts` relates to `scenario.ts`, never `io.ts`. */
9
+ export declare function pathsRelated(left: string, right: string): boolean;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Grounding-block freshness classification (fnd_c402046ac7).
3
+ *
4
+ * The committed grounding docs (CLAUDE.md, AGENTS.md, copilot-instructions,
5
+ * hunch.mdc, hunch.md) carry a managed block whose first sentence states the
6
+ * public store's RECORD COUNTS. Those counts are a pure function of .hunch/*.json,
7
+ * so two branches that each capture one decision both regenerate the very same
8
+ * "N+1 decisions" line. Git merges identical lines silently, the merged store holds
9
+ * N+2, and the committed doc is one behind — with no conflict, no hook (the merge
10
+ * happened on the forge) and no human error. Every release-gate red of that class
11
+ * (PR #128, #135, v1.26.2's first run) was this lag.
12
+ *
13
+ * The lag is transient and self-healing: the next capture commit folds the
14
+ * regenerated docs in (refreshCommittableGrounding), and the release gate's
15
+ * repository-index stage regenerates them in the worktree and already treats the
16
+ * dirt as memory churn. What must STILL fail is a doc that is genuinely wrong:
17
+ *
18
+ * - prose in the block differs (a hand edit, a stale renderer, a template change);
19
+ * - an append-only count is AHEAD of the store: the doc counted a record the
20
+ * repository does not carry — the never-committed Video component of
21
+ * fnd_6391b4242f, the only defect the counts ever caught.
22
+ *
23
+ * So the rule is direction-aware and deterministic: decisions, bugs, constraints,
24
+ * components and policies only accrue, so a doc may lag behind them (merge) but never
25
+ * run ahead (missing record). Open findings move both ways (a finding resolved on one
26
+ * branch, a finding recorded on another), so a differing findings count alone is lag.
27
+ */
28
+ export interface GroundingCounts {
29
+ decisions: number;
30
+ bugs: number;
31
+ constraints: number;
32
+ components: number;
33
+ policies: number;
34
+ findings: number;
35
+ }
36
+ /** Record kinds whose committed count may only ever lag behind the store. */
37
+ export declare const APPEND_ONLY_COUNT_KINDS: readonly ["decisions", "bugs", "constraints", "components", "policies"];
38
+ /** Render the counts sentence's bold `**N decisions, ...**` segment — the
39
+ * exact text parseGroundingCounts looks for. The single source of truth for
40
+ * the format, shared by the doc generator (claudemd.ts) and the grounding
41
+ * merge driver (groundingMerge.ts) so they can never drift out of sync. */
42
+ export declare function renderCountsMatch(counts: GroundingCounts): string;
43
+ /** Replace a counts-sentence match with a neutral placeholder, so the rest of
44
+ * two texts can be compared for equality regardless of their counts. Shared
45
+ * by classifyGroundingBlock (committed doc vs computed) and the grounding
46
+ * merge driver (ours vs theirs) — both ask "do these two texts differ ONLY
47
+ * in the counts sentence?" */
48
+ export declare function stripCountsMatch(text: string, match: string): string;
49
+ export declare function parseGroundingCounts(block: string): {
50
+ counts: GroundingCounts;
51
+ match: string;
52
+ } | null;
53
+ export type GroundingFreshness =
54
+ /** Byte-identical managed block. */
55
+ {
56
+ kind: "fresh";
57
+ }
58
+ /** Only the counts sentence differs and no append-only count is ahead of the
59
+ * store: records merged in behind the doc. Heals on the next capture or
60
+ * `hunch grounding --refresh`; never a release blocker. */
61
+ | {
62
+ kind: "lagging";
63
+ committed: GroundingCounts;
64
+ generated: GroundingCounts;
65
+ behind: string[];
66
+ }
67
+ /** An append-only count in the committed doc exceeds the store: the doc knows a
68
+ * record the repository does not carry (never committed, or pruned by hand). */
69
+ | {
70
+ kind: "ahead";
71
+ committed: GroundingCounts;
72
+ generated: GroundingCounts;
73
+ ahead: string[];
74
+ }
75
+ /** The block differs outside the counts sentence (or a counts sentence is missing). */
76
+ | {
77
+ kind: "diverged";
78
+ reason: string;
79
+ };
80
+ /** Classify a committed managed block against the one the graph generates NOW.
81
+ * Both inputs are block CONTENT (markers stripped, trimmed). */
82
+ export declare function classifyGroundingBlock(committed: string, generated: string): GroundingFreshness;
83
+ /** One human line per verdict — shared by the freshness test and `hunch grounding`. */
84
+ export declare function describeGroundingFreshness(doc: string, verdict: GroundingFreshness): string;
@@ -28,6 +28,21 @@
28
28
  const COUNTS_RE = /\*\*(\d+) decisions?, (\d+) bugs?, (\d+) constraints?, (\d+) components?, (\d+) polic(?:y|ies)(?:, (\d+) open findings?)?\*\*/;
29
29
  /** Record kinds whose committed count may only ever lag behind the store. */
30
30
  export const APPEND_ONLY_COUNT_KINDS = ["decisions", "bugs", "constraints", "components", "policies"];
31
+ /** Render the counts sentence's bold `**N decisions, ...**` segment — the
32
+ * exact text parseGroundingCounts looks for. The single source of truth for
33
+ * the format, shared by the doc generator (claudemd.ts) and the grounding
34
+ * merge driver (groundingMerge.ts) so they can never drift out of sync. */
35
+ export function renderCountsMatch(counts) {
36
+ return `**${counts.decisions} decisions, ${counts.bugs} bugs, ${counts.constraints} constraints, ${counts.components} components, ${counts.policies} policies${counts.findings ? `, ${counts.findings} open findings` : ""}**`;
37
+ }
38
+ /** Replace a counts-sentence match with a neutral placeholder, so the rest of
39
+ * two texts can be compared for equality regardless of their counts. Shared
40
+ * by classifyGroundingBlock (committed doc vs computed) and the grounding
41
+ * merge driver (ours vs theirs) — both ask "do these two texts differ ONLY
42
+ * in the counts sentence?" */
43
+ export function stripCountsMatch(text, match) {
44
+ return text.replace(match, "<counts>");
45
+ }
31
46
  export function parseGroundingCounts(block) {
32
47
  const m = COUNTS_RE.exec(block);
33
48
  if (!m)
@@ -55,8 +70,7 @@ export function classifyGroundingBlock(committed, generated) {
55
70
  return { kind: "diverged", reason: "the committed block carries no record-counts sentence" };
56
71
  if (!g)
57
72
  return { kind: "diverged", reason: "the generated block carries no record-counts sentence" };
58
- const withoutCounts = (text, match) => text.replace(match, "<counts>");
59
- if (withoutCounts(committed, c.match) !== withoutCounts(generated, g.match)) {
73
+ if (stripCountsMatch(committed, c.match) !== stripCountsMatch(generated, g.match)) {
60
74
  return { kind: "diverged", reason: "the block differs outside the record-counts sentence" };
61
75
  }
62
76
  const ahead = APPEND_ONLY_COUNT_KINDS.filter((k) => c.counts[k] > g.counts[k]);
@@ -0,0 +1,15 @@
1
+ /** Given `git merge-file --diff3 -L ours -L base -L theirs` output for a
2
+ * generated grounding doc, auto-resolve it if every conflicting hunk is
3
+ * confined to the counts sentence; otherwise return it untouched. */
4
+ export declare function resolveGroundingConflicts(diff3Text: string): {
5
+ conflict: boolean;
6
+ text: string;
7
+ };
8
+ /** Run `git merge-file --diff3` on real files and resolve the result.
9
+ * `write: null` means git itself errored (e.g. one side is binary) — never
10
+ * guess content in that case; leave the file exactly as git already
11
+ * populated it before invoking this driver. */
12
+ export declare function mergeGroundingFile(basePath: string, oursPath: string, theirsPath: string): {
13
+ conflict: boolean;
14
+ write: string | null;
15
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Merge driver for the generated grounding block's record-counts sentence
3
+ * (dec_ba5b0dfa22, superseding dec_91da20a46b's "drop the counts sentence"):
4
+ * resolves a HARD git conflict when it is confined to that one line by
5
+ * taking the ELEMENT-WISE MAX of ours's and theirs's counts, per field, as a
6
+ * placeholder. Picking `ours` unconditionally would discard whichever side
7
+ * carried the HIGHER count — and a higher count can itself be the
8
+ * ahead-of-store signal fnd_6391b4242f depends on (a branch whose doc claims
9
+ * a record its own committed store never received). Taking the max instead:
10
+ * when neither side already carries that defect, the merged store's counts
11
+ * can only be >= max(ours, theirs) (decisions/bugs/constraints/components/
12
+ * policies are append-only, and the merged store is the union of both
13
+ * sides' committed records), so the doc reads as "lagging" — self-healed by
14
+ * the post-merge hook's `hunch grounding --refresh` — never falsely "ahead".
15
+ * But when one side DOES already carry the defect, its inflated count
16
+ * survives into the resolution and correctly still reads as "ahead" against
17
+ * the true merged store, instead of being silently discarded. Any conflict
18
+ * outside the counts sentence, anywhere in the file, is left untouched with
19
+ * standard diff3 markers for a human to resolve.
20
+ */
21
+ import { execFileSync } from "node:child_process";
22
+ import { parseGroundingCounts, stripCountsMatch, renderCountsMatch } from "./groundingLag.js";
23
+ // `\r?\n` (not a bare `\n`) throughout: on a CRLF worktree every diff3 marker
24
+ // line is itself `\r\n`-terminated, and a bare `\n` fails to match ANY of
25
+ // them. `^...$` with /m anchor each marker at its own line start rather than
26
+ // requiring a specific preceding/following literal newline, so a hunk whose
27
+ // ours or theirs side is EMPTY (one side deleted the line, the other edited
28
+ // it) still matches — with a literal `\n` requirement there, git's real
29
+ // output for that shape has no such newline to match, so the whole hunk
30
+ // silently fails to match at all.
31
+ const CONFLICT_RE = /^<<<<<<< ours\r?\n([\s\S]*?)^\|\|\|\|\|\|\| base\r?\n[\s\S]*?^=======\r?\n([\s\S]*?)^>>>>>>> theirs[^\n]*\r?\n?/gm;
32
+ /** `null` when the hunk isn't confined to the counts sentence; otherwise the
33
+ * resolved sentence text, with each field taken as max(ours, theirs). */
34
+ function resolveCountsOnlyHunk(ours, theirs) {
35
+ const o = parseGroundingCounts(ours);
36
+ const t = parseGroundingCounts(theirs);
37
+ if (!o || !t)
38
+ return null;
39
+ if (stripCountsMatch(ours, o.match) !== stripCountsMatch(theirs, t.match))
40
+ return null;
41
+ const keys = Object.keys(o.counts);
42
+ if (keys.every((k) => o.counts[k] >= t.counts[k]))
43
+ return ours; // ours already dominates every field verbatim
44
+ const combined = { ...o.counts };
45
+ for (const k of keys)
46
+ combined[k] = Math.max(o.counts[k], t.counts[k]);
47
+ return ours.replace(o.match, renderCountsMatch(combined));
48
+ }
49
+ /** Given `git merge-file --diff3 -L ours -L base -L theirs` output for a
50
+ * generated grounding doc, auto-resolve it if every conflicting hunk is
51
+ * confined to the counts sentence; otherwise return it untouched. */
52
+ export function resolveGroundingConflicts(diff3Text) {
53
+ if (!diff3Text.includes("<<<<<<< ours"))
54
+ return { conflict: false, text: diff3Text };
55
+ let allResolved = true;
56
+ const resolved = diff3Text.replace(CONFLICT_RE, (whole, ours, theirs) => {
57
+ const merged = resolveCountsOnlyHunk(ours, theirs);
58
+ if (merged === null) {
59
+ allResolved = false;
60
+ return whole;
61
+ }
62
+ return merged;
63
+ });
64
+ // Defense in depth: a hunk shape CONFLICT_RE fails to recognize must never
65
+ // read as resolved just because the callback never ran on it — if any
66
+ // marker survives, this is a real conflict, full stop.
67
+ if (!allResolved || resolved.includes("<<<<<<< ours"))
68
+ return { conflict: true, text: diff3Text };
69
+ return { conflict: false, text: resolved };
70
+ }
71
+ /** Run `git merge-file --diff3` on real files and resolve the result.
72
+ * `write: null` means git itself errored (e.g. one side is binary) — never
73
+ * guess content in that case; leave the file exactly as git already
74
+ * populated it before invoking this driver. */
75
+ export function mergeGroundingFile(basePath, oursPath, theirsPath) {
76
+ try {
77
+ const merged = execFileSync("git", ["merge-file", "-p", "--diff3", "-L", "ours", "-L", "base", "-L", "theirs", oursPath, basePath, theirsPath], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
78
+ return { conflict: false, write: merged };
79
+ }
80
+ catch (e) {
81
+ const err = e;
82
+ const status = typeof err.status === "number" ? err.status : -1;
83
+ // `git merge-file`'s exit status is the number of conflicting hunks
84
+ // (1-127) on a genuine 3-way merge attempt. Anything else (a negative
85
+ // status, or >127) means git itself errored — e.g. "Cannot merge binary
86
+ // files" exits 255 with EMPTY stdout — and treating that as diff3 output
87
+ // silently truncates the file to nothing.
88
+ if (status < 1 || status > 127)
89
+ return { conflict: true, write: null };
90
+ const out = typeof err.stdout === "string" ? err.stdout : (err.stdout?.toString() ?? "");
91
+ const res = resolveGroundingConflicts(out);
92
+ return { conflict: res.conflict, write: res.text };
93
+ }
94
+ }
95
+ //# sourceMappingURL=groundingMerge.js.map
@@ -0,0 +1,9 @@
1
+ export interface HookObservation {
2
+ provider: string;
3
+ event: string;
4
+ at: string;
5
+ version: string;
6
+ }
7
+ /** Never throws (con_03a0b94b2e): a missing ledger costs evidence, not the edit. */
8
+ export declare function recordHookObservation(root: string, provider: string, event: string): void;
9
+ export declare function readHookObservations(root: string): HookObservation[];