@davesheffer/hunch 1.31.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +3 -6
  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 +602 -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 +124 -0
  15. package/dist/cli/update.d.ts +14 -0
  16. package/dist/client/state.d.ts +248 -0
  17. package/dist/client/state.js +2 -0
  18. package/dist/constitution/adapters.d.ts +29 -0
  19. package/dist/constitution/behaviorAttestationBinding.d.ts +3 -0
  20. package/dist/constitution/behaviorEvaluator.d.ts +8 -0
  21. package/dist/constitution/behaviorProof.d.ts +9 -0
  22. package/dist/constitution/behaviorWorkspace.d.ts +25 -0
  23. package/dist/constitution/bootstrap.d.ts +30 -0
  24. package/dist/constitution/canonical.d.ts +15 -0
  25. package/dist/constitution/card.d.ts +58 -0
  26. package/dist/constitution/compiler.d.ts +52 -0
  27. package/dist/constitution/composition.d.ts +10 -0
  28. package/dist/constitution/corpus.d.ts +7 -0
  29. package/dist/constitution/correctionPolicyMaterializer.d.ts +57 -0
  30. package/dist/constitution/delta.d.ts +4 -0
  31. package/dist/constitution/disposition.d.ts +20 -0
  32. package/dist/constitution/evaluator.d.ts +47 -0
  33. package/dist/constitution/experiment.d.ts +466 -0
  34. package/dist/constitution/experimentRunner.d.ts +7 -0
  35. package/dist/constitution/g2.d.ts +209 -0
  36. package/dist/constitution/g2BehaviorAttestation.d.ts +45 -0
  37. package/dist/constitution/g2BehaviorCandidates.d.ts +123 -0
  38. package/dist/constitution/g2BehaviorDependencies.d.ts +83 -0
  39. package/dist/constitution/g2BehaviorMaterialization.d.ts +77 -0
  40. package/dist/constitution/g2BehaviorPolicyMaterializer.d.ts +48 -0
  41. package/dist/constitution/g2CandidateAttestation.d.ts +43 -0
  42. package/dist/constitution/g2Candidates.d.ts +80 -0
  43. package/dist/constitution/g2Drills.d.ts +33 -0
  44. package/dist/constitution/g3.d.ts +311 -0
  45. package/dist/constitution/g3Conformance.d.ts +33 -0
  46. package/dist/constitution/lifecycle.d.ts +25 -0
  47. package/dist/constitution/mutation.d.ts +15 -0
  48. package/dist/constitution/nodeTestEvidence.d.ts +13 -0
  49. package/dist/constitution/plan.d.ts +18 -0
  50. package/dist/constitution/policyRuntime.d.ts +5 -0
  51. package/dist/constitution/proof.d.ts +11 -0
  52. package/dist/constitution/repairPolicies.d.ts +34 -0
  53. package/dist/constitution/replacementFreeGit.d.ts +14 -0
  54. package/dist/constitution/replay.d.ts +40 -0
  55. package/dist/constitution/replayCache.d.ts +14 -0
  56. package/dist/constitution/replayWorker.d.ts +1 -0
  57. package/dist/constitution/repository.d.ts +129 -0
  58. package/dist/constitution/safeCheckout.d.ts +9 -0
  59. package/dist/constitution/schema.d.ts +1763 -0
  60. package/dist/constitution/scorecard.d.ts +219 -0
  61. package/dist/constitution/service.d.ts +398 -0
  62. package/dist/constitution/shadow.d.ts +54 -0
  63. package/dist/constitution/sourceMutation.d.ts +15 -0
  64. package/dist/constitution/staticGraphBaseline.d.ts +13 -0
  65. package/dist/constitution/structural.d.ts +64 -0
  66. package/dist/core/agenthook.d.ts +63 -0
  67. package/dist/core/agenthook.js +1 -0
  68. package/dist/core/automaticReviewMemory.d.ts +60 -0
  69. package/dist/core/autoreview.d.ts +66 -0
  70. package/dist/core/canonicalOrder.d.ts +3 -0
  71. package/dist/core/capturetoken.d.ts +19 -0
  72. package/dist/core/changeIdentity.d.ts +22 -0
  73. package/dist/core/changeProof.d.ts +14 -0
  74. package/dist/core/checkreport.d.ts +140 -0
  75. package/dist/core/commitrepair.d.ts +171 -0
  76. package/dist/core/commitrepair.js +238 -0
  77. package/dist/core/compare.d.ts +21 -0
  78. package/dist/core/config.d.ts +20 -0
  79. package/dist/core/conformance.d.ts +35 -0
  80. package/dist/core/constraintmatch.d.ts +54 -0
  81. package/dist/core/correction.d.ts +42 -0
  82. package/dist/core/correctionStage.d.ts +224 -0
  83. package/dist/core/declarationClusters.d.ts +149 -0
  84. package/dist/core/delivery.d.ts +110 -0
  85. package/dist/core/docanchors.d.ts +32 -0
  86. package/dist/core/docscan.d.ts +26 -0
  87. package/dist/core/drift.d.ts +13 -0
  88. package/dist/core/drift.js +31 -1
  89. package/dist/core/dupdetect.d.ts +43 -0
  90. package/dist/core/escalations.d.ts +129 -0
  91. package/dist/core/escalations.js +165 -0
  92. package/dist/core/events.d.ts +29 -0
  93. package/dist/core/evidenceMap.d.ts +164 -0
  94. package/dist/core/externalImports.d.ts +8 -0
  95. package/dist/core/format.d.ts +13 -0
  96. package/dist/core/glob.d.ts +9 -0
  97. package/dist/core/groundingLag.d.ts +84 -0
  98. package/dist/core/groundingLag.js +16 -2
  99. package/dist/core/groundingMerge.d.ts +15 -0
  100. package/dist/core/groundingMerge.js +95 -0
  101. package/dist/core/hookcache.d.ts +10 -0
  102. package/dist/core/hookpolicy.d.ts +47 -0
  103. package/dist/core/ids.d.ts +36 -0
  104. package/dist/core/importReview.d.ts +30 -0
  105. package/dist/core/io.d.ts +23 -0
  106. package/dist/core/jsonc.d.ts +4 -0
  107. package/dist/core/landscapeAdoption.d.ts +66 -0
  108. package/dist/core/landscapeDelivery.d.ts +76 -0
  109. package/dist/core/memorylog.d.ts +37 -0
  110. package/dist/core/migrate.d.ts +28 -0
  111. package/dist/core/outcomeExperience.d.ts +89 -0
  112. package/dist/core/overlaySafety.d.ts +24 -0
  113. package/dist/core/overlaySafety.js +7 -1
  114. package/dist/core/paths.d.ts +31 -0
  115. package/dist/core/pipeline.d.ts +289 -0
  116. package/dist/core/premises.d.ts +43 -0
  117. package/dist/core/provenance.d.ts +21 -0
  118. package/dist/core/publication.d.ts +54 -0
  119. package/dist/core/refrepair.d.ts +24 -0
  120. package/dist/core/refrepair.js +1 -1
  121. package/dist/core/relativeImports.d.ts +11 -0
  122. package/dist/core/repair.d.ts +44 -0
  123. package/dist/core/repairqueue.d.ts +40 -0
  124. package/dist/core/repairqueue.js +133 -0
  125. package/dist/core/reviewMemory.d.ts +40 -0
  126. package/dist/core/reviewqueue.d.ts +54 -0
  127. package/dist/core/safeRepoFile.d.ts +20 -0
  128. package/dist/core/served.d.ts +57 -0
  129. package/dist/core/served.js +14 -0
  130. package/dist/core/stateContract.d.ts +979 -0
  131. package/dist/core/stateContract.js +87 -4
  132. package/dist/core/stateDelivery.d.ts +78 -0
  133. package/dist/core/stateRecords.d.ts +383 -0
  134. package/dist/core/stateRecords.js +16 -1
  135. package/dist/core/stats.d.ts +88 -0
  136. package/dist/core/strictgate.d.ts +33 -0
  137. package/dist/core/taskReport.d.ts +226 -0
  138. package/dist/core/taskReport.js +514 -0
  139. package/dist/core/taskReportCapture.d.ts +8 -0
  140. package/dist/core/taskReportCapture.js +36 -0
  141. package/dist/core/taskReportEvidence.d.ts +26 -0
  142. package/dist/core/taskReportEvidence.js +268 -0
  143. package/dist/core/taskReportHook.d.ts +20 -0
  144. package/dist/core/taskReportHook.js +76 -0
  145. package/dist/core/taskReportPaths.d.ts +3 -0
  146. package/dist/core/taskReportPaths.js +23 -0
  147. package/dist/core/taskReportPublic.d.ts +31 -0
  148. package/dist/core/taskReportPublic.js +45 -0
  149. package/dist/core/taskReportRender.d.ts +6 -0
  150. package/dist/core/taskReportRender.js +97 -0
  151. package/dist/core/topics.d.ts +73 -0
  152. package/dist/core/types.d.ts +1239 -0
  153. package/dist/core/version.d.ts +10 -0
  154. package/dist/eval/guards.d.ts +45 -0
  155. package/dist/eval/harness.d.ts +58 -0
  156. package/dist/extractors/adrImport.d.ts +59 -0
  157. package/dist/extractors/comments.d.ts +7 -0
  158. package/dist/extractors/correctionSources.d.ts +11 -0
  159. package/dist/extractors/diff.d.ts +40 -0
  160. package/dist/extractors/git.d.ts +304 -0
  161. package/dist/extractors/git.js +208 -20
  162. package/dist/extractors/helm.d.ts +31 -0
  163. package/dist/extractors/indexer.d.ts +55 -0
  164. package/dist/extractors/landscapeDiscovery.d.ts +41 -0
  165. package/dist/extractors/languages.d.ts +77 -0
  166. package/dist/extractors/nativeTreeSitter.d.ts +16 -0
  167. package/dist/extractors/parse.d.ts +54 -0
  168. package/dist/extractors/php.d.ts +26 -0
  169. package/dist/extractors/repoSource.d.ts +53 -0
  170. package/dist/extractors/testreport.d.ts +36 -0
  171. package/dist/integrations/ciAction.d.ts +8 -0
  172. package/dist/integrations/claudeConfig.d.ts +44 -0
  173. package/dist/integrations/claudemd.d.ts +13 -0
  174. package/dist/integrations/claudemd.js +11 -2
  175. package/dist/integrations/gitignore.d.ts +16 -0
  176. package/dist/integrations/gitignore.js +7 -0
  177. package/dist/integrations/health.d.ts +74 -0
  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 +114 -25
  194. package/dist/mcp/taskReportTools.d.ts +3 -0
  195. package/dist/mcp/taskReportTools.js +106 -0
  196. package/dist/serve/app.d.ts +36 -0
  197. package/dist/serve/app.js +20 -0
  198. package/dist/serve/config.d.ts +100 -0
  199. package/dist/serve/writelock.d.ts +22 -0
  200. package/dist/store/changeLedger.d.ts +132 -0
  201. package/dist/store/changeLedger.js +40 -9
  202. package/dist/store/compact.d.ts +33 -0
  203. package/dist/store/db.d.ts +9 -0
  204. package/dist/store/embedder.d.ts +32 -0
  205. package/dist/store/hunchStore.d.ts +624 -0
  206. package/dist/store/hunchStore.js +13 -5
  207. package/dist/store/jsonStore.d.ts +125 -0
  208. package/dist/store/jsonStore.js +15 -0
  209. package/dist/store/merge.d.ts +39 -0
  210. package/dist/store/privateMigrate.d.ts +24 -0
  211. package/dist/store/replay.d.ts +56 -0
  212. package/dist/store/schema.d.ts +17 -0
  213. package/dist/store/stateBinding.d.ts +145 -0
  214. package/dist/store/stateBinding.js +110 -15
  215. package/dist/store/stateCapture.d.ts +15 -0
  216. package/dist/store/stateCapture.js +145 -0
  217. package/dist/synthesis/cliAdapter.d.ts +22 -0
  218. package/dist/synthesis/initiator.d.ts +13 -0
  219. package/dist/synthesis/provider.d.ts +307 -0
  220. package/dist/synthesis/synthesize.d.ts +117 -0
  221. package/dist/synthesis/tripwires.d.ts +9 -0
  222. package/dist/taskReports.d.ts +76 -0
  223. package/dist/taskReports.js +67 -0
  224. package/dist/wiki/adopt.d.ts +22 -0
  225. package/dist/wiki/graph.d.ts +87 -0
  226. package/dist/wiki/wiki.d.ts +242 -0
  227. package/package.json +6 -5
  228. package/server.json +2 -2
@@ -0,0 +1,268 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile, execFileSync, spawn } from "node:child_process";
3
+ import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
4
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
5
+ import { analyzeDiff } from "../extractors/diff.js";
6
+ import { workingDiff, workingFiles } from "../extractors/git.js";
7
+ import { assertCompleteRepoScan, scanRepo } from "../extractors/indexer.js";
8
+ import { checkConformance } from "./conformance.js";
9
+ import { effectiveForbids, matchForbids } from "./constraintmatch.js";
10
+ import { ReportCheckSchema, ReportRecordSchema, beginReportCheck, readTaskReport, recordReportCheck, recordReportConformance, reportHash } from "./taskReport.js";
11
+ export function snapshotDeliveredRecords(store, envelope) {
12
+ const records = [];
13
+ for (const item of envelope.delivered) {
14
+ const record = store.recs(item.kind === "relationships" ? "edges" : item.kind).find(r => r.id === item.record_id);
15
+ if (!record)
16
+ continue;
17
+ const raw = record;
18
+ const text = (...keys) => keys.map(k => raw[k]).find(v => typeof v === "string");
19
+ const candidate = ReportRecordSchema.safeParse({
20
+ record_id: item.record_id, kind: item.kind, content_hash: reportHash(record),
21
+ title: (text("title", "statement", "name", "reason") ?? item.record_id).slice(0, 500),
22
+ lesson: (text("decision", "statement", "description", "root_cause", "reason", "title", "name") ?? item.record_id).slice(0, 12_000),
23
+ recorded_at: text("date", "created_at", "observed_at", "valid_from") ?? null,
24
+ });
25
+ if (candidate.success)
26
+ records.push(candidate.data);
27
+ }
28
+ return records;
29
+ }
30
+ /** Bounded source snapshot, not a committed change proof. Ignored files and
31
+ * Hunch's own memory/cache are excluded and explicitly disclosed. */
32
+ export function reportSourceSnapshot(root) {
33
+ const limitations = ["Git-ignored files, Hunch memory/cache, and external dependencies are outside this source snapshot."];
34
+ try {
35
+ const base = realpathSync(root);
36
+ const env = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
37
+ for (const key of Object.keys(env))
38
+ if (key.startsWith("GIT_") && key !== "GIT_OPTIONAL_LOCKS")
39
+ delete env[key];
40
+ const paths = execFileSync("git", ["-C", base, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { env, timeout: 5_000, maxBuffer: 4_000_000 }).toString("utf8").split("\0").filter(Boolean);
41
+ const sorted = [...new Set(paths)].sort();
42
+ if (sorted.length > 20_000)
43
+ throw new Error("source file count exceeds snapshot limit");
44
+ const entries = [];
45
+ let bytes = 0;
46
+ for (const path of sorted) {
47
+ if (path.split("/").some(p => p === ".hunch" || p === ".hunch-cache" || p === ".git"))
48
+ continue;
49
+ const file = resolve(base, path);
50
+ const rel = relative(base, file);
51
+ if (isAbsolute(rel) || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\"))
52
+ throw new Error("source path escapes repository");
53
+ let stat;
54
+ try {
55
+ stat = lstatSync(file);
56
+ }
57
+ catch (e) {
58
+ if (e.code === "ENOENT") {
59
+ entries.push([path, null, 0]);
60
+ continue;
61
+ }
62
+ throw e;
63
+ }
64
+ const parent = relative(base, realpathSync(dirname(file)));
65
+ if (isAbsolute(parent) || parent === ".." || parent.startsWith("../") || parent.startsWith("..\\"))
66
+ throw new Error("source parent escapes repository");
67
+ if (stat.isSymbolicLink()) {
68
+ entries.push([path, reportHash(readlinkSync(file)), 0o120000]);
69
+ continue;
70
+ }
71
+ if (!stat.isFile())
72
+ throw new Error("submodules or non-file sources require separate verification");
73
+ bytes += stat.size;
74
+ if (bytes > 64_000_000 || stat.size > 8_000_000)
75
+ throw new Error("source bytes exceed snapshot limit");
76
+ const content = readFileSync(file);
77
+ const after = lstatSync(file);
78
+ if (stat.mtimeMs !== after.mtimeMs || stat.ctimeMs !== after.ctimeMs || stat.size !== after.size || content.length !== stat.size)
79
+ throw new Error("source changed during snapshot");
80
+ entries.push([path, `sha256:${createHash("sha256").update(content).digest("hex")}`, stat.mode & 0o111]);
81
+ }
82
+ return { hash: reportHash(entries), limitations };
83
+ }
84
+ catch {
85
+ return { hash: null, limitations: [...limitations, "A complete bounded source snapshot was unavailable; no current-change verification is claimed."] };
86
+ }
87
+ }
88
+ /** Evaluate every delivered lesson's DECLARED rule against the files this task
89
+ * changed (working tree vs HEAD). Deterministic and Hunch-owned: the agent
90
+ * neither chooses the rule nor submits the verdict. A constraint is judged by its
91
+ * forbids matcher over the added lines of its scoped changed files; a decision by
92
+ * its conformance predicates over the current working graph, and only when a
93
+ * predicate's subject lives in a changed file. Everything else stays
94
+ * `not-exercised` or `unavailable` — never "satisfied" by file overlap. */
95
+ export function runReportConformance(root, store, taskId) {
96
+ const report = readTaskReport(root, taskId);
97
+ if (report.task.state !== "open")
98
+ throw new Error("cannot evaluate rules for a closed task");
99
+ const delivered = [...new Map(report.deliveries.flatMap(d => d.records).filter(r => r.kind === "constraints" || r.kind === "decisions").map(r => [`${r.kind}:${r.record_id}:${r.content_hash}`, r])).values()];
100
+ if (!delivered.length)
101
+ return [];
102
+ const before = reportSourceSnapshot(root).hash;
103
+ const files = workingFiles(root);
104
+ const changed = new Set(files);
105
+ const diff = workingDiff(root);
106
+ const analysis = analyzeDiff(diff);
107
+ const truncated = diff.endsWith("…(diff truncated)…");
108
+ let graph;
109
+ const workingGraph = () => {
110
+ if (graph !== undefined)
111
+ return graph;
112
+ try {
113
+ const scan = scanRepo(store, root, { churn: false, source: { kind: "working" } });
114
+ assertCompleteRepoScan(scan);
115
+ graph = { symbols: scan.symbols, edges: scan.edges };
116
+ }
117
+ catch {
118
+ graph = null;
119
+ }
120
+ return graph;
121
+ };
122
+ const results = [];
123
+ for (const record of delivered) {
124
+ const base = { source: "local-rule-check", record_id: record.record_id, kind: record.kind, content_hash: record.content_hash, snapshot: null };
125
+ const note = (rule, outcome, detail, scoped = []) => results.push({ ...base, rule, outcome, files: scoped.slice(0, 64), detail: detail.slice(0, 1000) });
126
+ if (record.kind === "constraints") {
127
+ const constraint = store.recs("constraints").find(c => c.id === record.record_id);
128
+ if (!constraint || reportHash(constraint) !== record.content_hash) {
129
+ note("constraint-forbids", "unavailable", "The delivered record revision is no longer the stored revision; the rule of a different revision is not evaluated.");
130
+ continue;
131
+ }
132
+ const forbids = effectiveForbids(constraint);
133
+ if (!forbids) {
134
+ note("constraint-forbids", "unavailable", "This constraint declares no forbids matcher; a scope-only rule cannot be verified deterministically.");
135
+ continue;
136
+ }
137
+ const scoped = files.filter(f => store.checkConstraints(f).some(c => c.id === constraint.id));
138
+ if (!scoped.length) {
139
+ note("constraint-forbids", "not-exercised", "No changed file falls in this constraint's scope.");
140
+ continue;
141
+ }
142
+ if (truncated) {
143
+ note("constraint-forbids", "unavailable", "The working diff exceeds the bounded analysis budget; added lines were not fully inspected.", scoped);
144
+ continue;
145
+ }
146
+ const match = matchForbids(forbids, new Set(analysis.addedDeps), scoped.flatMap(f => analysis.addedLinesByFile.get(f) ?? []));
147
+ if (match)
148
+ note("constraint-forbids", "violated", `Added code trips the ${match.tier} rule: ${match.evidence.slice(0, 3).join("; ")}`, scoped);
149
+ else
150
+ note("constraint-forbids", "satisfied", `Added lines in ${scoped.length} scoped file(s) trip none of the forbidden ${[forbids.deps.length && "dependencies", forbids.symbols.length && "symbols", forbids.patterns.length && "patterns"].filter(Boolean).join("/")}.`, scoped);
151
+ continue;
152
+ }
153
+ const decision = store.recs("decisions").find(d => d.id === record.record_id);
154
+ if (!decision || reportHash(decision) !== record.content_hash) {
155
+ note("decision-conformance", "unavailable", "The delivered record revision is no longer the stored revision; the predicates of a different revision are not evaluated.");
156
+ continue;
157
+ }
158
+ if (!decision.conformance?.length) {
159
+ note("decision-conformance", "unavailable", "This decision declares no conformance predicate; its intent cannot be verified deterministically.");
160
+ continue;
161
+ }
162
+ const current = workingGraph();
163
+ if (!current) {
164
+ note("decision-conformance", "unavailable", "The working source graph could not be scanned completely; no predicate result is claimed.");
165
+ continue;
166
+ }
167
+ const subjects = decision.conformance.map(p => p.subject);
168
+ const exercised = current.symbols.filter(s => changed.has(s.file) && subjects.some(ref => ref === s.id || ref === s.name || (ref.includes(":") && s.name === ref.slice(ref.lastIndexOf(":") + 1) && (s.file === ref.slice(0, ref.lastIndexOf(":")) || s.file.endsWith("/" + ref.slice(0, ref.lastIndexOf(":")))))));
169
+ if (!exercised.length) {
170
+ note("decision-conformance", "not-exercised", "No predicate subject is defined in a changed file.");
171
+ continue;
172
+ }
173
+ const verdicts = checkConformance(store, { graph: current }).filter(r => r.decision === decision.id);
174
+ if (!verdicts.length) {
175
+ note("decision-conformance", "unavailable", "The decision is not in force; its predicates are not evaluated.");
176
+ continue;
177
+ }
178
+ const scoped = [...new Set(exercised.map(s => s.file))].sort();
179
+ const failed = verdicts.filter(v => !v.satisfied);
180
+ if (failed.length)
181
+ note("decision-conformance", "violated", failed.map(v => v.detail).join("; "), scoped);
182
+ else
183
+ note("decision-conformance", "satisfied", verdicts.map(v => v.detail).join("; "), scoped);
184
+ }
185
+ const after = reportSourceSnapshot(root).hash;
186
+ const snapshot = before !== null && before === after ? before : null;
187
+ return results.map(result => {
188
+ const value = { ...result, snapshot, ...(snapshot === null ? { detail: `${result.detail} Source changed during evaluation, so this result is not bound to a current snapshot.`.slice(0, 1000) } : {}) };
189
+ recordReportConformance(root, taskId, value);
190
+ return value;
191
+ });
192
+ }
193
+ /** A deliberately explicit command wrapper. The caller chooses the command;
194
+ * reports never execute commands automatically to validate submitted claims. */
195
+ export async function runReportCheck(root, taskId, command, label, timeoutMs = 120_000, options = {}) {
196
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 600_000)
197
+ throw new Error("verification timeout must be between 1 and 600000 ms");
198
+ options.signal?.throwIfAborted();
199
+ const task = readTaskReport(root, taskId).task;
200
+ if (task.state !== "open")
201
+ throw new Error("cannot verify a closed task");
202
+ const before = reportSourceSnapshot(root);
203
+ // Validate sensitive arguments before executing or writing anything.
204
+ ReportCheckSchema.parse({ label, command, exit_code: null, output_hash: reportHash(""), before_snapshot: before.hash, after_snapshot: null, snapshot_limitations: before.limitations, timed_out: false, source: "local-command-runner" });
205
+ const checkId = beginReportCheck(root, taskId, label);
206
+ const result = await new Promise((resolveResult) => {
207
+ const child = spawn(command[0], command.slice(1), { cwd: root, shell: false, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, detached: process.platform !== "win32" });
208
+ const stdout = createHash("sha256"), stderr = createHash("sha256");
209
+ let timedOut = false, cancelled = false, settled = false;
210
+ let cleanupTimer;
211
+ const settle = (code) => {
212
+ if (settled)
213
+ return;
214
+ settled = true;
215
+ clearTimeout(timer);
216
+ options.signal?.removeEventListener("abort", cancel);
217
+ if (cleanupTimer)
218
+ clearTimeout(cleanupTimer);
219
+ child.stdout.destroy();
220
+ child.stderr.destroy();
221
+ resolveResult({ code, timedOut, cancelled, hash: reportHash({ stdout: stdout.digest("hex"), stderr: stderr.digest("hex") }) });
222
+ };
223
+ const stopTree = () => {
224
+ if (settled || cleanupTimer)
225
+ return;
226
+ if (child.pid) {
227
+ if (process.platform === "win32")
228
+ execFile("taskkill", ["/pid", String(child.pid), "/T", "/F"], { timeout: 1_000, windowsHide: true }, () => { });
229
+ else {
230
+ try {
231
+ process.kill(-child.pid, "SIGKILL");
232
+ }
233
+ catch {
234
+ child.kill("SIGKILL");
235
+ }
236
+ }
237
+ }
238
+ // A descendant can detach or inherit pipes. Always bound settlement even
239
+ // if the OS cannot confirm process-tree cleanup; timed-out is never pass.
240
+ cleanupTimer = setTimeout(() => settle(null), 1_200);
241
+ };
242
+ const cancel = () => { cancelled = true; stopTree(); };
243
+ const timer = setTimeout(() => {
244
+ timedOut = true;
245
+ stopTree();
246
+ }, timeoutMs);
247
+ // Separate streaming digests are stable across stdout/stderr chunk ordering.
248
+ child.stdout.on("data", chunk => { if (!settled) {
249
+ stdout.update(chunk);
250
+ options.onStdout?.(chunk);
251
+ } });
252
+ child.stderr.on("data", chunk => { if (!settled) {
253
+ stderr.update(chunk);
254
+ options.onStderr?.(chunk);
255
+ } });
256
+ child.once("error", () => settle(null));
257
+ child.once("close", code => settle(code));
258
+ options.signal?.addEventListener("abort", cancel, { once: true });
259
+ if (options.signal?.aborted)
260
+ cancel();
261
+ });
262
+ const after = reportSourceSnapshot(root);
263
+ const check = { check_id: checkId, label, command, exit_code: result.code, output_hash: result.hash, before_snapshot: before.hash, after_snapshot: after.hash,
264
+ snapshot_limitations: [...new Set([...before.limitations, ...after.limitations])], timed_out: result.timedOut, cancelled: result.cancelled, source: "local-command-runner" };
265
+ recordReportCheck(root, taskId, check);
266
+ return check;
267
+ }
268
+ //# sourceMappingURL=taskReportEvidence.js.map
@@ -0,0 +1,20 @@
1
+ import type { HookProvider, HunchHookInput } from "./agenthook.js";
2
+ export declare function hookReportTaskId(root: string, provider: HookProvider, event: HunchHookInput): string | null;
3
+ /** Every prompt receives its exact ID, even when ambient reminders were deduped.
4
+ * No raw prompt, host session identifier, or transcript is retained. */
5
+ export declare function startHookReport(root: string, provider: HookProvider, event: HunchHookInput): string | null;
6
+ /** A presentation notice never denies Stop or injects another model turn. Stop
7
+ * can precede another hook's continuation, so it does not close an open task. */
8
+ export declare function stopHookReport(root: string, provider: HookProvider, event: HunchHookInput): {
9
+ systemMessage: string;
10
+ } | null;
11
+ /** Call only after emitting the native denial. Evidence failures must never
12
+ * suppress the gate response or attach it to a guessed task. */
13
+ export declare function observeHookDenial(root: string, provider: HookProvider, event: HunchHookInput, target: string, denial: {
14
+ reason: string;
15
+ event: {
16
+ kind: "constraint" | "veto";
17
+ constraint?: string;
18
+ decision?: string;
19
+ };
20
+ }): void;
@@ -0,0 +1,76 @@
1
+ /** Native lifecycle coverage is independent of whether a model follows reporting
2
+ * instructions. Only an authoritative prompt identity may join its evidence. */
3
+ import { realpathSync } from "node:fs";
4
+ import { pathToFileURL } from "node:url";
5
+ import { findRoot } from "./paths.js";
6
+ import { readTaskReport, recordReportRefusal, reportHash, reportPresentationEnabled, startReportTask } from "./taskReport.js";
7
+ import { reportSourceSnapshot } from "./taskReportEvidence.js";
8
+ import { renderTaskReport, writeTaskReportHtml } from "./taskReportRender.js";
9
+ function identity(root, provider, event) {
10
+ if (provider !== "claude" || !event.cwd || realpathSync(findRoot(event.cwd)) !== realpathSync(root))
11
+ return null;
12
+ for (const value of [event.session_id, event.prompt_id, event.agent_id]) {
13
+ if (value !== undefined && (!value.length || value.length > 1024 || /[\u0000-\u001f\u007f]/.test(value)))
14
+ return null;
15
+ }
16
+ if (!event.session_id)
17
+ return null;
18
+ if (!event.prompt_id)
19
+ return "legacy";
20
+ return `htask_${reportHash([realpathSync(root), provider, event.session_id, event.prompt_id, event.agent_id ?? null]).slice(7, 31)}`;
21
+ }
22
+ export function hookReportTaskId(root, provider, event) {
23
+ try {
24
+ const id = identity(root, provider, event);
25
+ return id === "legacy" ? null : id;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /** Every prompt receives its exact ID, even when ambient reminders were deduped.
32
+ * No raw prompt, host session identifier, or transcript is retained. */
33
+ export function startHookReport(root, provider, event) {
34
+ const id = identity(root, provider, event);
35
+ if (!id || id === "legacy")
36
+ return null;
37
+ const task = startReportTask(root, "Claude task", id);
38
+ return `Hunch has opened this prompt's report: ${task.task_id}. Reuse this exact ID for this prompt. Call hunch_task(action: "start", task_id: "${task.task_id}", title: "Claude task") to obtain verification_argv; do not create another report. Pass this task_id to hunch_context and decision/correction/finding captures, and finish with hunch_task before responding. A host Stop notice will show the evidence even if no task-linked memory was observed.`;
39
+ }
40
+ /** A presentation notice never denies Stop or injects another model turn. Stop
41
+ * can precede another hook's continuation, so it does not close an open task. */
42
+ export function stopHookReport(root, provider, event) {
43
+ if (!reportPresentationEnabled(root))
44
+ return null;
45
+ const id = identity(root, provider, event);
46
+ if (!id)
47
+ return null;
48
+ if (id === "legacy")
49
+ return { systemMessage: "Hunch hook active. This Claude version does not provide an exact prompt identifier, so contribution for this response is unverified. Explicit task reports remain available with hunch report." };
50
+ try {
51
+ const report = readTaskReport(root, id, reportSourceSnapshot(root).hash);
52
+ let card = renderTaskReport(report);
53
+ try {
54
+ const file = writeTaskReportHtml(root, id);
55
+ card = card.replace(/^Evidence .*$/m, `Evidence ${pathToFileURL(file).href}`);
56
+ }
57
+ catch { /* exact CLI evidence link remains available */ }
58
+ return { systemMessage: card };
59
+ }
60
+ catch {
61
+ return { systemMessage: `Hunch report unavailable for ${id}. Contribution is unverified; inspect with hunch report ${id}.` };
62
+ }
63
+ }
64
+ /** Call only after emitting the native denial. Evidence failures must never
65
+ * suppress the gate response or attach it to a guessed task. */
66
+ export function observeHookDenial(root, provider, event, target, denial) {
67
+ try {
68
+ const taskId = hookReportTaskId(root, provider, event);
69
+ const recordId = denial.event.kind === "constraint" ? denial.event.constraint : denial.event.decision;
70
+ if (!taskId || !recordId)
71
+ return;
72
+ recordReportRefusal(root, taskId, { source: "native-edit-gate", outcome: "denial-emitted", kind: denial.event.kind, record_id: recordId, target, reason_hash: reportHash(denial.reason) });
73
+ }
74
+ catch { /* the already emitted gate response remains authoritative */ }
75
+ }
76
+ //# sourceMappingURL=taskReportHook.js.map
@@ -0,0 +1,3 @@
1
+ /** Report paths are local to this physical worktree. Existing symlinks and
2
+ * hard-linked files are refused; never follow a cache pointer into another scope. */
3
+ export declare function assertReportPath(root: string, ...parts: string[]): string;
@@ -0,0 +1,23 @@
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /** Report paths are local to this physical worktree. Existing symlinks and
4
+ * hard-linked files are refused; never follow a cache pointer into another scope. */
5
+ export function assertReportPath(root, ...parts) {
6
+ let path = realpathSync(root);
7
+ for (const part of parts) {
8
+ if (!/^[A-Za-z0-9._-]+$/.test(part) || part === "." || part === "..")
9
+ throw new Error("invalid local report path component");
10
+ path = join(path, part);
11
+ try {
12
+ const stat = lstatSync(path);
13
+ if (stat.isSymbolicLink() || (stat.isFile() && stat.nlink > 1))
14
+ throw new Error("report path must not follow symlinks or hard links");
15
+ }
16
+ catch (error) {
17
+ if (error.code !== "ENOENT")
18
+ throw error;
19
+ }
20
+ }
21
+ return path;
22
+ }
23
+ //# sourceMappingURL=taskReportPaths.js.map
@@ -0,0 +1,31 @@
1
+ export declare function publicTaskReport(root: string, taskId: string): {
2
+ content_hash: string;
3
+ schema: "hunch.public-task-report/1";
4
+ title: string;
5
+ state: "open" | "completed" | "interrupted";
6
+ records: {
7
+ record_id: string;
8
+ kind: string;
9
+ content_hash: string;
10
+ title: string;
11
+ lesson: string;
12
+ recorded_at: string | null;
13
+ }[];
14
+ deliveries: {
15
+ occurrence_id: string;
16
+ receipt_id: string;
17
+ envelope_hash: string;
18
+ record_id: string;
19
+ record_hash: string;
20
+ }[];
21
+ applications: {
22
+ occurrence_id: string;
23
+ record_id: string;
24
+ content_hash: string;
25
+ attribution: "agent-reported";
26
+ detail: string;
27
+ }[];
28
+ limitations: string[];
29
+ };
30
+ export type PublicTaskReport = ReturnType<typeof publicTaskReport>;
31
+ export declare function renderPublicTaskReportHtml(report: PublicTaskReport): string;
@@ -0,0 +1,45 @@
1
+ /** Publication is a fresh public-store projection, never redaction of a local
2
+ * report's prose. Task titles, actions, commands and native envelope text are private. */
3
+ import { JsonStore } from "../store/jsonStore.js";
4
+ import { hunchPaths } from "./paths.js";
5
+ import { readTaskReport, reportHash } from "./taskReport.js";
6
+ import { reportSourceSnapshot } from "./taskReportEvidence.js";
7
+ export function publicTaskReport(root, taskId) {
8
+ const report = readTaskReport(root, taskId, reportSourceSnapshot(root).hash);
9
+ const publicStore = new JsonStore(hunchPaths(root));
10
+ const records = new Map();
11
+ const deliveries = [];
12
+ for (const delivery of report.deliveries) {
13
+ for (const snapshot of delivery.records) {
14
+ const item = delivery.envelope.delivered.find(r => r.record_id === snapshot.record_id && r.kind === snapshot.kind);
15
+ if (!item)
16
+ continue;
17
+ const record = publicStore.loadAll(item.kind === "relationships" ? "edges" : item.kind).find(r => r.id === item.record_id);
18
+ if (!record || reportHash(record) !== snapshot.content_hash)
19
+ continue;
20
+ // Reconstruct human-readable content from public records, not caller-supplied snapshots.
21
+ const raw = record;
22
+ const text = (...keys) => keys.map(k => raw[k]).find(v => typeof v === "string");
23
+ const safe = { record_id: record.id, kind: item.kind, content_hash: snapshot.content_hash,
24
+ title: (text("title", "statement", "name", "reason") ?? record.id).slice(0, 500),
25
+ lesson: (text("decision", "statement", "description", "root_cause", "reason", "title", "name") ?? record.id).slice(0, 12_000),
26
+ recorded_at: text("date", "created_at", "observed_at", "valid_from") ?? null };
27
+ records.set(`${item.kind}:${record.id}:${snapshot.content_hash}`, safe);
28
+ deliveries.push({ occurrence_id: delivery.occurrence_id, receipt_id: delivery.receipt_id, envelope_hash: delivery.envelope_hash, record_id: record.id, record_hash: snapshot.content_hash });
29
+ }
30
+ }
31
+ const allowed = new Set(deliveries.map(d => `${d.occurrence_id}:${d.record_id}:${d.record_hash}`));
32
+ const applications = report.claims.filter(c => allowed.has(`${c.occurrence_id}:${c.record_id}:${c.content_hash}`)).map(c => ({ occurrence_id: c.occurrence_id, record_id: c.record_id, content_hash: c.content_hash, attribution: "agent-reported", detail: "Action details withheld from public export." }));
33
+ const unsigned = {
34
+ schema: "hunch.public-task-report/1",
35
+ title: "Hunch contribution · public evidence", state: report.task.state,
36
+ records: [...records.values()], deliveries, applications,
37
+ limitations: ["Only exact record revisions currently present in the public store are included.", "Task identity, task prose, private memory, command results and native delivery payloads are omitted. Receipt hashes identify local evidence; their full payloads are not published.", "Agent-reported application does not establish causal impact."],
38
+ };
39
+ return { ...unsigned, content_hash: reportHash(unsigned) };
40
+ }
41
+ export function renderPublicTaskReportHtml(report) {
42
+ const esc = (x) => String(x ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
43
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'"><title>${esc(report.title)}</title><style>body{margin:40px auto;padding:0 24px;max-width:850px;font:16px/1.6 system-ui,sans-serif;color:#193725;background:#f6f8f5}article{padding:24px;border:1px solid #b9cbbd;border-radius:12px;background:white;margin:18px 0}h1{font-size:36px}code{overflow-wrap:anywhere}pre{white-space:pre-wrap}summary{cursor:pointer;padding:8px 0}:focus-visible{outline:3px solid #276540}</style></head><body><h1>${esc(report.title)}</h1><p>${report.records.length} public lesson(s) · ${esc(report.state)}</p>${report.records.map(r => `<article><h2>${esc(r.title)}</h2><p>${esc(r.lesson)}</p><p>${esc(r.record_id)}</p><code>${esc(r.content_hash)}</code><details><summary>Inspect public delivery references</summary><pre>${esc(JSON.stringify(report.deliveries.filter(d => d.record_id === r.record_id && d.record_hash === r.content_hash), null, 2))}</pre></details></article>`).join("")}<h2>Limits of this export</h2><ul>${report.limitations.map(l => `<li>${esc(l)}</li>`).join("")}</ul><p>${report.applications.length} agent-reported application(s); action details withheld.</p><footer><code>${esc(report.content_hash)}</code></footer></body></html>`;
44
+ }
45
+ //# sourceMappingURL=taskReportPublic.js.map
@@ -0,0 +1,6 @@
1
+ import { type LessonHistory, type TaskReport } from "./taskReport.js";
2
+ export declare function writeTaskReportHtml(root: string, taskId: string, publicOnly?: boolean): string;
3
+ export declare function renderTaskReport(report: TaskReport): string;
4
+ /** Standalone, local-only projection. No active content, external assets or
5
+ * untrusted outbound URLs; evidence references are internal anchors. */
6
+ export declare function renderTaskReportHtml(report: TaskReport, generatedAt?: string, histories?: LessonHistory[] | null): string;
@@ -0,0 +1,97 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { writeFileAtomic } from "./io.js";
3
+ import { readTaskReport, readLessonHistory } from "./taskReport.js";
4
+ import { reportSourceSnapshot } from "./taskReportEvidence.js";
5
+ import { assertReportPath } from "./taskReportPaths.js";
6
+ import { publicTaskReport, renderPublicTaskReportHtml } from "./taskReportPublic.js";
7
+ export function writeTaskReportHtml(root, taskId, publicOnly = false) {
8
+ const report = readTaskReport(root, taskId, reportSourceSnapshot(root).hash);
9
+ const directory = assertReportPath(root, ".hunch-cache", "reports");
10
+ mkdirSync(directory, { recursive: true });
11
+ const file = assertReportPath(root, ".hunch-cache", "reports", `${report.task.task_id}${publicOnly ? ".public" : ""}.html`);
12
+ let histories = [];
13
+ if (!publicOnly) {
14
+ try {
15
+ histories = historyRecords(report).slice(0, 3).map(record => readLessonHistory(root, { kind: record.kind, record_id: record.record_id, content_hash: record.content_hash }, { limit: 8 }));
16
+ }
17
+ catch {
18
+ histories = null;
19
+ } // History is optional; retain the primary task evidence if unavailable.
20
+ }
21
+ writeFileAtomic(file, publicOnly ? renderPublicTaskReportHtml(publicTaskReport(root, taskId)) : renderTaskReportHtml(report, undefined, histories));
22
+ return file;
23
+ }
24
+ function plain(value) { return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim(); }
25
+ function clip(value, size = 66) { const chars = [...plain(value)]; return chars.length > size ? chars.slice(0, size - 1).join("") + "…" : chars.join(""); }
26
+ function uniqueRecords(report) {
27
+ return [...new Map(report.deliveries.flatMap(d => d.records).map(r => [`${r.kind}:${r.record_id}:${r.content_hash}`, r])).values()];
28
+ }
29
+ function historyRecords(report) {
30
+ return [...new Map([...uniqueRecords(report), ...report.saves.map(s => s.record)].map(r => [`${r.kind}:${r.record_id}:${r.content_hash}`, r])).values()];
31
+ }
32
+ /** The last evaluation of each delivered record revision is its standing. */
33
+ function ruleStanding(report) {
34
+ return [...new Map(report.conformance.map(r => [`${r.kind}:${r.record_id}:${r.content_hash}`, r])).values()];
35
+ }
36
+ function recordTitle(report, rule) {
37
+ return uniqueRecords(report).find(r => r.kind === rule.kind && r.record_id === rule.record_id && r.content_hash === rule.content_hash)?.title ?? rule.record_id;
38
+ }
39
+ export function renderTaskReport(report) {
40
+ const records = uniqueRecords(report);
41
+ const lines = [`Hunch · ${clip(report.task.title)}`, `Task ${report.task.task_id} · ${report.task.state}`];
42
+ if (!report.deliveries.length)
43
+ lines.push("No task-linked delivery observed; connection/use is unverified.");
44
+ else if (!records.length)
45
+ lines.push(report.coverage === "no-relevant-memory" ? "No relevant memory returned for this task." : "Memory delivered; exact record snapshots unavailable.");
46
+ else {
47
+ lines.push(`Recalled ${clip(records[0].title, 65)}`);
48
+ if (records.length > 1)
49
+ lines.push(` ${records.length - 1} more lesson(s) in the evidence view`);
50
+ }
51
+ const standing = ruleStanding(report);
52
+ const violated = standing.find(r => r.outcome === "violated");
53
+ const held = standing.find(r => r.outcome === "satisfied" && r.current);
54
+ if (report.claims.length) {
55
+ const claim = report.claims.find(c => c.supported_by) ?? report.claims[0];
56
+ lines.push(`Applied ${clip(claim.action, 42)} · ${claim.supported_by ? "rule-supported" : "agent-reported"}`);
57
+ }
58
+ else if (held)
59
+ lines.push(`Conformed ${clip(recordTitle(report, held), 34)} · rule held on ${held.files.length} changed file(s)`);
60
+ else if (records.length)
61
+ lines.push("Impact Memory delivered; contribution unverified.");
62
+ if (violated)
63
+ lines.push(`Violated ${clip(recordTitle(report, violated), 34)} · rule broken on changed files`);
64
+ if (report.saves.length)
65
+ lines.push(`Saved ${clip(report.saves[0].record.title, 38)} · ${report.saves[0].durability}`);
66
+ if (report.refusals.length)
67
+ lines.push(`Guarded Denial emitted · ${clip(report.refusals[0].record_id, 40)}`);
68
+ const check = report.checks.at(-1);
69
+ if (check) {
70
+ const state = check.cancelled ? "cancelled" : check.timed_out ? "timed out" : check.exit_code === 0 ? "passed" : "failed";
71
+ lines.push(`Checked ${clip(check.label, 28)} · ${state}${check.current ? " · current source snapshot" : " · current source unverified"}`);
72
+ }
73
+ else
74
+ lines.push("Checked No independent command result recorded.");
75
+ lines.push(`Evidence hunch report ${report.task.task_id} --html`);
76
+ return lines.join("\n");
77
+ }
78
+ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]); }
79
+ /** Standalone, local-only projection. No active content, external assets or
80
+ * untrusted outbound URLs; evidence references are internal anchors. */
81
+ export function renderTaskReportHtml(report, generatedAt = new Date().toISOString(), histories = []) {
82
+ const records = uniqueRecords(report);
83
+ const section = (title, body) => `<section><h2>${title}</h2>${body}</section>`;
84
+ const empty = (message) => `<p class="muted">${esc(message)}</p>`;
85
+ const deliveries = report.deliveries.map((d, index) => `<article id="delivery-${index}"><div class="eyebrow">DELIVERED · ${esc(d.at)}</div><h3>${d.records.length ? esc(d.records.map(r => r.title).join(" · ")) : "No retained lesson snapshot"}</h3><p>${d.envelope.delivered.length} record(s) returned. ${d.envelope.abstention.active ? "Some memory withheld for insufficient relevance or confidence." : ""}</p><details><summary>Inspect exact delivery</summary><dl><dt>Receipt</dt><dd>${esc(d.receipt_id)}</dd><dt>Occurrence</dt><dd>${esc(d.occurrence_id)}</dd><dt>Envelope hash</dt><dd>${esc(d.envelope_hash)}</dd></dl><pre>${esc(d.envelope.text)}</pre></details></article>`).join("");
86
+ const lessons = records.map(r => `<article><div class="eyebrow">${esc(r.kind)} · ${esc(r.recorded_at ?? "creation date unavailable")}</div><h3>${esc(r.title)}</h3><p class="lesson">${esc(r.lesson)}</p><details><summary>Original record identity</summary><p>${esc(r.record_id)}</p><code>${esc(r.content_hash)}</code></details></article>`).join("");
87
+ const claims = report.claims.map(c => { const index = report.deliveries.findIndex(d => d.occurrence_id === c.occurrence_id); return `<article><span class="badge">${c.supported_by ? "RULE-SUPPORTED · AGENT-REPORTED" : "AGENT-REPORTED"}</span><h3>${esc(c.action)}</h3><p>${c.supported_by ? `This is the agent's attribution. Hunch separately evaluated the lesson's declared rule against the changed files and it held on the current source (<a href="#rule-${esc(c.supported_by)}">open the rule evaluation</a>).` : "This is the agent's attribution. No current, satisfied rule evaluation supports it; verification below establishes only the recorded command result."}</p><a href="#delivery-${index}">Open the lesson's delivery</a></article>`; }).join("");
88
+ const rules = ruleStanding(report).map(r => { const label = r.outcome === "satisfied" ? (r.current ? "RULE HELD" : "RULE HELD · SOURCE CHANGED SINCE") : r.outcome === "violated" ? "RULE VIOLATED" : r.outcome === "not-exercised" ? "NOT EXERCISED" : "NO CHECKABLE RULE"; return `<article id="rule-${esc(r.event_id)}"><span class="badge">${label}</span><h3>${esc(recordTitle(report, r))}</h3><p>${esc(r.detail)}</p>${r.files.length ? `<p class="muted">Changed files in scope: ${esc(r.files.join(", "))}</p>` : ""}<details><summary>Inspect rule evaluation</summary><dl><dt>Rule</dt><dd>${esc(r.rule)}</dd><dt>Evaluated</dt><dd>${esc(r.at)}</dd><dt>Record revision</dt><dd>${esc(r.content_hash)}</dd><dt>Source snapshot</dt><dd>${esc(r.snapshot ?? "unavailable")}</dd></dl><p>Hunch evaluated the lesson's own declared rule on the files this task changed. A held rule shows the change conforms to that lesson; it is not proof the agent read it.</p></details></article>`; }).join("");
89
+ const checks = report.checks.map(c => `<article><span class="badge">${c.cancelled ? "CANCELLED" : c.timed_out ? "TIMED OUT" : c.exit_code === 0 ? "COMMAND PASSED" : "COMMAND FAILED"}</span><h3>${esc(c.label)}</h3><p>${c.current ? "When this report was generated, source matched the snapshots observed before and after this command." : "At report generation, source verification was unavailable or the source had changed."}</p><pre>${esc(c.command.map(x => JSON.stringify(x)).join(" "))}</pre><details><summary>Inspect verification evidence</summary><dl><dt>Observed</dt><dd>${esc(c.at)}</dd><dt>Exit code</dt><dd>${esc(c.exit_code ?? "unavailable")}</dd><dt>Output digest (raw output not retained)</dt><dd>${esc(c.output_hash)}</dd><dt>Before snapshot</dt><dd>${esc(c.before_snapshot ?? "unavailable")}</dd><dt>After snapshot</dt><dd>${esc(c.after_snapshot ?? "unavailable")}</dd></dl>${c.snapshot_limitations.map(x => `<p>${esc(x)}</p>`).join("")}</details></article>`).join("");
90
+ const saves = report.saves.map(s => `<article><span class="badge">${esc(s.durability.toUpperCase())}</span><h3>${esc(s.record.title)}</h3><p>${esc(s.operation)} in ${esc(s.home)} memory. Exact saved revision: <code>${esc(s.record.content_hash)}</code>.</p><p>${s.durability === "local" ? "No exact commit or push proof is retained for this revision." : "Git proof applies to this revision at the observed time, not the current remote state."}</p><details><summary>Inspect save evidence</summary><p>${esc(s.event_id)} · ${esc(s.at)}</p>${s.proofs.map(p => `<p>${esc(p.durability)} · ${esc(p.at)} · <code>${esc(p.commit)}</code>${p.publication ? ` · ${esc(p.publication.ref)} · ${esc(p.publication.basis)}` : ""}</p>`).join("")}</details></article>`).join("");
91
+ const refusals = report.refusals.map(r => `<article><span class="badge">DENIAL EMITTED</span><h3>${esc(r.record_id)}</h3><p>Hunch emitted a ${esc(r.kind)} denial for ${esc(r.target)}.</p><p>This does not establish that the host honored the denial or that a bug was prevented.</p><details><summary>Inspect gate evidence</summary><dl><dt>Observed</dt><dd>${esc(r.at)}</dd><dt>Event</dt><dd>${esc(r.event_id)}</dd><dt>Exact response reason digest</dt><dd>${esc(r.reason_hash)}</dd></dl></details></article>`).join("");
92
+ const history = histories === null ? empty("Lesson history is unavailable. The task evidence above is retained.") : histories.map(h => `<article><h3>${esc(h.entries[0]?.record.title ?? h.reference.record_id)}</h3><p>Save and delivery observations show where a lesson appeared; they do not prove use or additional impact.</p>${h.entries.map(e => `<details><summary>${esc(e.task.title)} · ${esc(e.task.state)}</summary><p>${e.event === "save" ? "Saved" : "Delivered"} ${esc(e.at)}</p><p class="lesson">${esc(e.record.lesson)}</p><dl><dt>Exact revision</dt><dd>${esc(e.record.content_hash)}</dd><dt>Evidence reference</dt><dd>${esc(e.receipt_id ?? e.event_id)}</dd></dl><p>Inspect this task: <code>hunch report ${esc(e.task.task_id)} --html</code></p></details>`).join("")}${!h.index_complete ? empty("History indexing is incomplete. Refresh for additional retained evidence.") : ""}${h.truncated ? empty("Additional deliveries are available through the lesson history command.") : ""}</article>`).join("");
93
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'"><title>Hunch · ${esc(report.task.title)}</title><style>
94
+ :root{color-scheme:light dark;--bg:#f4f7f4;--paper:#fff;--ink:#162e24;--muted:#53685b;--line:#d5e1d8;--accent:#276540}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.65 system-ui,sans-serif}main{max-width:920px;margin:auto;padding:48px 24px 80px}header{border-top:5px solid var(--accent);padding-top:24px;margin-bottom:40px}.eyebrow{font-size:12px;letter-spacing:.09em;color:var(--muted);font-weight:650}h1{font-size:clamp(28px,5vw,46px);line-height:1.15;letter-spacing:-.035em;margin:16px 0}h2{font-size:22px;margin:32px 0 12px}h3{font-size:18px;margin:12px 0 6px}p{margin:8px 0}article{background:var(--paper);border:1px solid var(--line);border-radius:12px;padding:22px;margin:12px 0}.muted,dt{color:var(--muted)}.badge{font-size:11px;letter-spacing:.06em;border:1px solid var(--line);border-radius:30px;padding:5px 10px}.lesson{white-space:pre-wrap}a{color:var(--accent);text-underline-offset:3px}a:focus-visible,summary:focus-visible{outline:3px solid var(--accent);outline-offset:5px}summary{cursor:pointer;font-weight:600;padding:8px 0}details{margin-top:16px}pre,code,dd{overflow-wrap:anywhere;word-break:break-word}pre{white-space:pre-wrap;font:13px/1.6 ui-monospace,monospace;padding:14px;background:var(--bg);border-radius:8px}dd{margin:0 0 12px}footer{border-top:1px solid var(--line);margin-top:36px;padding-top:20px;font-size:12px;color:var(--muted)}@media(prefers-color-scheme:dark){:root{--bg:#101b16;--paper:#17271e;--ink:#e3eee6;--muted:#a7b9ac;--line:#365041;--accent:#9cdbb0}}@media print{details>*{display:block}body{background:white;color:black}article{break-inside:avoid}}
95
+ </style></head><body><main><header><div class="eyebrow">HUNCH · TASK EVIDENCE · LOCAL REPORT</div><h1>${esc(report.task.title)}</h1><p>${records.length ? "Project experience carried into this task." : "What Hunch could observe in this task."}</p><p class="muted">${esc(report.task.state)} · ${esc(report.task.started_at)} · ${esc(report.task.task_id)}</p></header>${section("1. What the project remembered", lessons || empty("No lesson snapshot is available for this task."))}${section("2. What reached the agent", deliveries || empty("No task-linked delivery was observed. This does not prove the agent was disconnected."))}${section("3. How the agent says it applied", claims || empty("Contribution is unverified. A context delivery alone does not establish use."))}${section("4. What was checked", checks || empty("No independent command result was recorded."))}${section("5. How the change conforms to delivered rules", rules || empty("No delivered rule was evaluated. Finishing a task, or hunch task conform, evaluates each delivered lesson's declared rule against the changed files."))}${saves ? section("What was saved for future tasks", saves) : ""}${refusals ? section("When Hunch intervened", refusals) : ""}${section("Where this lesson appeared", (history || empty("No lesson history is included in this view.")) + (histories && historyRecords(report).length > histories.length ? empty(`History shown for ${histories.length} of ${historyRecords(report).length} lesson revisions. Inspect another revision with hunch report --lesson <record-id> --kind <kind> --revision <content-hash>.`) : ""))}${section("What remains unknown", report.unknowns.length ? `<ul>${report.unknowns.map(x => `<li>${esc(x)}</li>`).join("")}</ul>` : empty("The report makes no additional causal claim."))}<footer>Generated ${esc(generatedAt)}. This is a saved snapshot, not a live source check.<br>After editing source, refresh with <code>hunch report ${esc(report.task.task_id)} --html</code>.<br>Observed evidence, not a score or an estimate of bugs prevented.<br>Report ${esc(report.content_hash)}<br>Local report may contain private project memory. Keep it within the project's authorized audience.</footer></main></body></html>`;
96
+ }
97
+ //# sourceMappingURL=taskReportRender.js.map