@davesheffer/hunch 1.31.1 → 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 (220) hide show
  1. package/README.md +1 -0
  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/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/hookcache.d.ts +10 -0
  101. package/dist/core/hookpolicy.d.ts +47 -0
  102. package/dist/core/ids.d.ts +36 -0
  103. package/dist/core/importReview.d.ts +30 -0
  104. package/dist/core/io.d.ts +23 -0
  105. package/dist/core/jsonc.d.ts +4 -0
  106. package/dist/core/landscapeAdoption.d.ts +66 -0
  107. package/dist/core/landscapeDelivery.d.ts +76 -0
  108. package/dist/core/memorylog.d.ts +37 -0
  109. package/dist/core/migrate.d.ts +28 -0
  110. package/dist/core/outcomeExperience.d.ts +89 -0
  111. package/dist/core/overlaySafety.d.ts +24 -0
  112. package/dist/core/overlaySafety.js +7 -1
  113. package/dist/core/paths.d.ts +31 -0
  114. package/dist/core/pipeline.d.ts +289 -0
  115. package/dist/core/premises.d.ts +43 -0
  116. package/dist/core/provenance.d.ts +21 -0
  117. package/dist/core/publication.d.ts +54 -0
  118. package/dist/core/refrepair.d.ts +24 -0
  119. package/dist/core/refrepair.js +1 -1
  120. package/dist/core/relativeImports.d.ts +11 -0
  121. package/dist/core/repair.d.ts +44 -0
  122. package/dist/core/repairqueue.d.ts +40 -0
  123. package/dist/core/repairqueue.js +133 -0
  124. package/dist/core/reviewMemory.d.ts +40 -0
  125. package/dist/core/reviewqueue.d.ts +54 -0
  126. package/dist/core/safeRepoFile.d.ts +20 -0
  127. package/dist/core/served.d.ts +57 -0
  128. package/dist/core/served.js +14 -0
  129. package/dist/core/stateContract.d.ts +979 -0
  130. package/dist/core/stateDelivery.d.ts +78 -0
  131. package/dist/core/stateRecords.d.ts +383 -0
  132. package/dist/core/stats.d.ts +88 -0
  133. package/dist/core/strictgate.d.ts +33 -0
  134. package/dist/core/taskReport.d.ts +226 -0
  135. package/dist/core/taskReport.js +514 -0
  136. package/dist/core/taskReportCapture.d.ts +8 -0
  137. package/dist/core/taskReportCapture.js +36 -0
  138. package/dist/core/taskReportEvidence.d.ts +26 -0
  139. package/dist/core/taskReportEvidence.js +268 -0
  140. package/dist/core/taskReportHook.d.ts +20 -0
  141. package/dist/core/taskReportHook.js +76 -0
  142. package/dist/core/taskReportPaths.d.ts +3 -0
  143. package/dist/core/taskReportPaths.js +23 -0
  144. package/dist/core/taskReportPublic.d.ts +31 -0
  145. package/dist/core/taskReportPublic.js +45 -0
  146. package/dist/core/taskReportRender.d.ts +6 -0
  147. package/dist/core/taskReportRender.js +97 -0
  148. package/dist/core/topics.d.ts +73 -0
  149. package/dist/core/types.d.ts +1239 -0
  150. package/dist/core/version.d.ts +10 -0
  151. package/dist/eval/guards.d.ts +45 -0
  152. package/dist/eval/harness.d.ts +58 -0
  153. package/dist/extractors/adrImport.d.ts +59 -0
  154. package/dist/extractors/comments.d.ts +7 -0
  155. package/dist/extractors/correctionSources.d.ts +11 -0
  156. package/dist/extractors/diff.d.ts +40 -0
  157. package/dist/extractors/git.d.ts +304 -0
  158. package/dist/extractors/git.js +208 -20
  159. package/dist/extractors/helm.d.ts +31 -0
  160. package/dist/extractors/indexer.d.ts +55 -0
  161. package/dist/extractors/landscapeDiscovery.d.ts +41 -0
  162. package/dist/extractors/languages.d.ts +77 -0
  163. package/dist/extractors/nativeTreeSitter.d.ts +16 -0
  164. package/dist/extractors/parse.d.ts +54 -0
  165. package/dist/extractors/php.d.ts +26 -0
  166. package/dist/extractors/repoSource.d.ts +53 -0
  167. package/dist/extractors/testreport.d.ts +36 -0
  168. package/dist/integrations/ciAction.d.ts +8 -0
  169. package/dist/integrations/claudeConfig.d.ts +44 -0
  170. package/dist/integrations/claudemd.d.ts +13 -0
  171. package/dist/integrations/claudemd.js +11 -2
  172. package/dist/integrations/gitignore.d.ts +16 -0
  173. package/dist/integrations/gitignore.js +7 -0
  174. package/dist/integrations/health.d.ts +74 -0
  175. package/dist/integrations/hooks.d.ts +41 -0
  176. package/dist/integrations/hooks.js +92 -66
  177. package/dist/integrations/madrExport.d.ts +39 -0
  178. package/dist/integrations/madrManifest.d.ts +69 -0
  179. package/dist/integrations/mergeDriver.d.ts +3 -0
  180. package/dist/integrations/mergeDriver.js +25 -8
  181. package/dist/integrations/probe.d.ts +4 -0
  182. package/dist/integrations/providers.d.ts +103 -0
  183. package/dist/integrations/scaffold.d.ts +29 -0
  184. package/dist/integrations/sync.d.ts +30 -0
  185. package/dist/integrations/sync.js +3 -2
  186. package/dist/integrations/team.d.ts +70 -0
  187. package/dist/integrations/worktree.d.ts +8 -0
  188. package/dist/mcp/roots.d.ts +16 -0
  189. package/dist/mcp/server.d.ts +36 -0
  190. package/dist/mcp/server.js +74 -22
  191. package/dist/mcp/taskReportTools.d.ts +3 -0
  192. package/dist/mcp/taskReportTools.js +106 -0
  193. package/dist/serve/app.d.ts +36 -0
  194. package/dist/serve/config.d.ts +100 -0
  195. package/dist/serve/writelock.d.ts +22 -0
  196. package/dist/store/changeLedger.d.ts +132 -0
  197. package/dist/store/compact.d.ts +33 -0
  198. package/dist/store/db.d.ts +9 -0
  199. package/dist/store/embedder.d.ts +32 -0
  200. package/dist/store/hunchStore.d.ts +624 -0
  201. package/dist/store/hunchStore.js +6 -3
  202. package/dist/store/jsonStore.d.ts +125 -0
  203. package/dist/store/merge.d.ts +39 -0
  204. package/dist/store/privateMigrate.d.ts +24 -0
  205. package/dist/store/replay.d.ts +56 -0
  206. package/dist/store/schema.d.ts +17 -0
  207. package/dist/store/stateBinding.d.ts +145 -0
  208. package/dist/store/stateCapture.d.ts +15 -0
  209. package/dist/synthesis/cliAdapter.d.ts +22 -0
  210. package/dist/synthesis/initiator.d.ts +13 -0
  211. package/dist/synthesis/provider.d.ts +307 -0
  212. package/dist/synthesis/synthesize.d.ts +117 -0
  213. package/dist/synthesis/tripwires.d.ts +9 -0
  214. package/dist/taskReports.d.ts +76 -0
  215. package/dist/taskReports.js +67 -0
  216. package/dist/wiki/adopt.d.ts +22 -0
  217. package/dist/wiki/graph.d.ts +87 -0
  218. package/dist/wiki/wiki.d.ts +242 -0
  219. package/package.json +6 -5
  220. package/server.json +2 -2
@@ -0,0 +1,514 @@
1
+ /** Task-scoped observations. Never infer task identity from time or a transport
2
+ * connection. Memory remains in the Git store; these observations survive reindex. */
3
+ import { createHash, randomBytes } from "node:crypto";
4
+ import { existsSync, readFileSync, realpathSync, rmSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { z } from "zod";
7
+ import { assertDeliveryEnvelope } from "./delivery.js";
8
+ import { isCredentialFreeText } from "./types.js";
9
+ import { withServedDatabase } from "./served.js";
10
+ import { assertReportPath } from "./taskReportPaths.js";
11
+ export const TASK_REPORT_SCHEMA = "hunch.task-report/1";
12
+ export const TaskIdSchema = z.string().regex(/^htask_[a-f0-9]{24}$/);
13
+ const hashSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/);
14
+ const safeText = (max) => z.string().min(1).max(max).refine(isCredentialFreeText, "credential material is not report evidence");
15
+ const MAX_EVENTS = 10_000;
16
+ const MAX_EVENT_BYTES = 256_000;
17
+ const MAX_TASK_BYTES = 8_000_000;
18
+ export function reportHash(value) {
19
+ const canonical = (v) => {
20
+ if (Array.isArray(v))
21
+ return `[${v.map(canonical).join(",")}]`;
22
+ if (v && typeof v === "object")
23
+ return `{${Object.entries(v).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, x]) => `${JSON.stringify(k)}:${canonical(x)}`).join(",")}}`;
24
+ return JSON.stringify(v) ?? "null";
25
+ };
26
+ return `sha256:${createHash("sha256").update(canonical(value)).digest("hex")}`;
27
+ }
28
+ export const ReportRecordSchema = z.object({
29
+ record_id: safeText(512), kind: safeText(64), content_hash: hashSchema,
30
+ title: z.string().max(500), lesson: z.string().max(12_000),
31
+ recorded_at: z.string().max(64).nullable(),
32
+ }).strict();
33
+ export const ReportClaimSchema = z.object({
34
+ occurrence_id: z.string().regex(/^hocc_[a-f0-9]{24}$/),
35
+ record_id: safeText(512), content_hash: hashSchema,
36
+ action: safeText(1_000),
37
+ }).strict();
38
+ export const ReportCheckSchema = z.object({
39
+ check_id: z.string().regex(/^hev_[a-f0-9]{24}$/).optional(),
40
+ label: safeText(200), command: z.array(safeText(1_024)).min(1).max(64),
41
+ exit_code: z.number().int().nullable(), output_hash: hashSchema,
42
+ before_snapshot: hashSchema.nullable(), after_snapshot: hashSchema.nullable(),
43
+ snapshot_limitations: z.array(z.string().max(300)).max(16),
44
+ timed_out: z.boolean(),
45
+ cancelled: z.boolean().optional(),
46
+ source: z.literal("local-command-runner"),
47
+ }).strict();
48
+ export const ReportSaveSchema = z.object({
49
+ source: z.literal("store-write"), record: ReportRecordSchema,
50
+ home: z.enum(["public", "private"]), operation: z.enum(["created", "updated"]),
51
+ }).strict();
52
+ export const ReportDurabilitySchema = z.object({
53
+ save_id: z.string().regex(/^hev_[a-f0-9]{24}$/),
54
+ durability: z.enum(["committed", "pushed"]), commit: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/),
55
+ record_hash: hashSchema, source: z.literal("git-record-proof"),
56
+ publication: z.object({ ref: safeText(512), basis: z.enum(["push-status", "remote-ref-confirmed"]) }).strict().optional(),
57
+ }).strict().superRefine((proof, ctx) => {
58
+ if ((proof.durability === "pushed") !== !!proof.publication)
59
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "publication proof is required exactly for pushed revisions" });
60
+ });
61
+ export const ReportRefusalSchema = z.object({
62
+ source: z.literal("native-edit-gate"), outcome: z.literal("denial-emitted"),
63
+ kind: z.enum(["constraint", "veto"]), record_id: safeText(512),
64
+ target: safeText(1024), reason_hash: hashSchema,
65
+ }).strict();
66
+ /** Hunch's own deterministic evaluation of a delivered lesson's DECLARED rule
67
+ * (a constraint's forbids matcher or a decision's conformance predicate) against
68
+ * the files this task changed. Only the local runner writes it; an agent cannot
69
+ * submit one. File overlap alone never yields "satisfied": a lesson with no
70
+ * machine-checkable rule stays `unavailable`, and a rule whose scope this task
71
+ * never touched stays `not-exercised`. */
72
+ export const ReportConformanceSchema = z.object({
73
+ source: z.literal("local-rule-check"),
74
+ record_id: safeText(512), kind: z.enum(["constraints", "decisions"]), content_hash: hashSchema,
75
+ rule: z.enum(["constraint-forbids", "decision-conformance"]),
76
+ outcome: z.enum(["satisfied", "violated", "not-exercised", "unavailable"]),
77
+ files: z.array(safeText(1024)).max(64),
78
+ snapshot: hashSchema.nullable(),
79
+ detail: z.string().max(1000),
80
+ }).strict();
81
+ const TaskSchema = z.object({
82
+ task_id: TaskIdSchema, scope: hashSchema, title: safeText(200),
83
+ started_at: z.string().datetime(), finished_at: z.string().datetime().nullable(),
84
+ state: z.enum(["open", "completed", "interrupted"]),
85
+ }).strict();
86
+ export const LessonReferenceSchema = z.object({
87
+ kind: safeText(64), record_id: safeText(512), content_hash: hashSchema.optional(),
88
+ }).strict();
89
+ function taskDb(root, run) {
90
+ return withServedDatabase(root, (db) => {
91
+ db.exec(`CREATE TABLE IF NOT EXISTS report_tasks (
92
+ task_id TEXT PRIMARY KEY, scope TEXT NOT NULL, body TEXT NOT NULL
93
+ );
94
+ CREATE TABLE IF NOT EXISTS report_events (
95
+ event_id TEXT PRIMARY KEY, task_id TEXT NOT NULL, kind TEXT NOT NULL,
96
+ at TEXT NOT NULL, body TEXT NOT NULL, content_hash TEXT NOT NULL
97
+ );
98
+ CREATE INDEX IF NOT EXISTS report_events_task ON report_events(task_id);
99
+ CREATE TABLE IF NOT EXISTS report_record_links (
100
+ event_id TEXT NOT NULL, kind TEXT NOT NULL, record_id TEXT NOT NULL, content_hash TEXT NOT NULL,
101
+ PRIMARY KEY (event_id, kind, record_id, content_hash)
102
+ );
103
+ CREATE INDEX IF NOT EXISTS report_record_lookup ON report_record_links(kind, record_id, content_hash);
104
+ CREATE TABLE IF NOT EXISTS report_history_progress (id INTEGER PRIMARY KEY CHECK(id = 1), through_rowid INTEGER NOT NULL);
105
+ INSERT OR IGNORE INTO report_history_progress VALUES (1, 0);`);
106
+ return run(db);
107
+ });
108
+ }
109
+ function deliveryRecords(kind, body) {
110
+ if (kind === "save")
111
+ return [ReportSaveSchema.parse(body).record];
112
+ if (kind !== "delivery")
113
+ return [];
114
+ const value = body;
115
+ assertDeliveryEnvelope(value.envelope);
116
+ const records = z.array(ReportRecordSchema).max(512).parse(value.records);
117
+ for (const record of records) {
118
+ if (!value.envelope.delivered.some(r => r.kind === record.kind && r.record_id === record.record_id))
119
+ throw new Error("history record was not delivered");
120
+ }
121
+ return records;
122
+ }
123
+ function indexDeliveryRecords(db, eventId, kind, body) {
124
+ const insert = db.prepare("INSERT OR IGNORE INTO report_record_links VALUES (?, ?, ?, ?)");
125
+ for (const record of deliveryRecords(kind, body))
126
+ insert.run(eventId, record.kind, record.record_id, record.content_hash);
127
+ }
128
+ /** The lookup is derived, never evidence. Backfill at most 64 events and
129
+ * 512 KB per read. Parse outside the writer transaction so a history view cannot
130
+ * hold the writer lock while validating a large legacy ledger. */
131
+ export function readLessonHistory(root, reference, options = {}) {
132
+ const ref = LessonReferenceSchema.parse(reference);
133
+ const { limit, before } = z.object({ limit: z.number().int().min(1).max(30).default(20), before: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional() }).strict().parse(options);
134
+ return taskDb(root, db => {
135
+ const { progress, pending } = transaction(db, () => {
136
+ const { through_rowid: progress } = db.prepare("SELECT through_rowid FROM report_history_progress WHERE id = 1").get();
137
+ const sizes = db.prepare("SELECT rowid AS seq, length(CAST(body AS BLOB)) AS bytes FROM report_events WHERE rowid > ? ORDER BY rowid LIMIT 64").all(progress);
138
+ let bytes = 0, through = progress;
139
+ for (const row of sizes) {
140
+ if (row.bytes > MAX_EVENT_BYTES)
141
+ throw new Error("history event exceeds the bounded evidence limit");
142
+ if (bytes + row.bytes > 512_000)
143
+ break;
144
+ bytes += row.bytes;
145
+ through = row.seq;
146
+ }
147
+ const pending = db.prepare("SELECT rowid AS seq, event_id, kind, body, content_hash FROM report_events WHERE rowid > ? AND rowid <= ? ORDER BY rowid").all(progress, through);
148
+ return { progress, pending };
149
+ }, true);
150
+ const prepared = pending.map(event => {
151
+ const body = JSON.parse(event.body);
152
+ if (reportHash(body) !== event.content_hash)
153
+ throw new Error("report evidence hash mismatch");
154
+ return { ...event, records: deliveryRecords(event.kind, body) };
155
+ });
156
+ if (prepared.length)
157
+ transaction(db, () => {
158
+ const current = db.prepare("SELECT through_rowid FROM report_history_progress WHERE id = 1").get();
159
+ // A concurrent reader or expiry may have changed the derived cursor.
160
+ // Leave it alone; the next read resumes from the current state.
161
+ if (current.through_rowid !== progress)
162
+ return;
163
+ const exists = db.prepare("SELECT 1 FROM report_events WHERE rowid = ? AND event_id = ? AND content_hash = ?");
164
+ if (prepared.some(event => !exists.get(event.seq, event.event_id, event.content_hash)))
165
+ return;
166
+ const insert = db.prepare("INSERT OR IGNORE INTO report_record_links VALUES (?, ?, ?, ?)");
167
+ for (const event of prepared)
168
+ for (const record of event.records)
169
+ insert.run(event.event_id, record.kind, record.record_id, record.content_hash);
170
+ db.prepare("UPDATE report_history_progress SET through_rowid = ? WHERE id = 1").run(prepared.at(-1).seq);
171
+ });
172
+ const { more, rows } = transaction(db, () => {
173
+ const { through_rowid: through } = db.prepare("SELECT through_rowid FROM report_history_progress WHERE id = 1").get();
174
+ const more = !!db.prepare("SELECT 1 FROM report_events WHERE rowid > ? LIMIT 1").get(through);
175
+ const rows = db.prepare(`SELECT e.rowid AS seq, e.event_id, e.kind, e.at, e.body, e.content_hash, t.body AS task
176
+ FROM report_record_links l JOIN report_events e ON e.event_id = l.event_id
177
+ JOIN report_tasks t ON t.task_id = e.task_id
178
+ WHERE t.scope = ? AND l.kind = ? AND l.record_id = ?
179
+ AND (? IS NULL OR l.content_hash = ?) AND e.rowid < ?
180
+ ORDER BY e.rowid DESC LIMIT ?`).all(scopeOf(root), ref.kind, ref.record_id, ref.content_hash ?? null, ref.content_hash ?? null, before ?? Number.MAX_SAFE_INTEGER, limit + 1);
181
+ return { more, rows };
182
+ }, true);
183
+ const entries = rows.slice(0, limit).map(row => {
184
+ if (Buffer.byteLength(row.body) > MAX_EVENT_BYTES)
185
+ throw new Error("history event exceeds the bounded evidence limit");
186
+ const body = JSON.parse(row.body);
187
+ if (reportHash(body) !== row.content_hash)
188
+ throw new Error("report evidence hash mismatch");
189
+ const record = deliveryRecords(row.kind, body).find(r => r.kind === ref.kind && r.record_id === ref.record_id && (!ref.content_hash || r.content_hash === ref.content_hash));
190
+ if (!record || (row.kind !== "delivery" && row.kind !== "save"))
191
+ throw new Error("history index does not match retained evidence");
192
+ return { task: TaskSchema.parse(JSON.parse(row.task)), event_id: row.event_id, at: row.at, event: row.kind, record, receipt_id: row.kind === "delivery" ? body.envelope.receipt_id : null };
193
+ });
194
+ // A cursor from a partial backfill would skip newer historical entries
195
+ // indexed on the next read. Refresh this page until indexing completes.
196
+ return { schema: "hunch.lesson-history/1", reference: ref, entries, index_complete: !more, truncated: rows.length > limit, next_before: !more && rows.length > limit ? rows[limit - 1].seq : null };
197
+ });
198
+ }
199
+ function scopeOf(root) { return reportHash(realpathSync(root)); }
200
+ function readTask(db, root, id) {
201
+ TaskIdSchema.parse(id);
202
+ const row = db.prepare("SELECT body FROM report_tasks WHERE task_id = ? AND scope = ?").get(id, scopeOf(root));
203
+ if (!row)
204
+ throw new Error("task not found in this repository/worktree; use its exact task ID and working directory");
205
+ return TaskSchema.parse(JSON.parse(row.body));
206
+ }
207
+ function transaction(db, run, readOnly = false) {
208
+ db.exec(readOnly ? "BEGIN" : "BEGIN IMMEDIATE");
209
+ try {
210
+ const result = run();
211
+ db.exec("COMMIT");
212
+ return result;
213
+ }
214
+ catch (error) {
215
+ db.exec("ROLLBACK");
216
+ throw error;
217
+ }
218
+ }
219
+ export function startReportTask(root, title, taskId) {
220
+ const task = TaskSchema.parse({ task_id: taskId ?? `htask_${randomBytes(12).toString("hex")}`,
221
+ scope: scopeOf(root), title, started_at: new Date().toISOString(), finished_at: null, state: "open" });
222
+ // Local observations have a bounded lifetime; durable project memory is untouched.
223
+ pruneReportHistory(root);
224
+ return taskDb(root, db => transaction(db, () => {
225
+ const prior = db.prepare("SELECT body FROM report_tasks WHERE task_id = ?").get(task.task_id);
226
+ if (prior) {
227
+ const existing = readTask(db, root, task.task_id);
228
+ if (existing.title !== title)
229
+ throw new Error("task identity already exists with a different title");
230
+ return existing;
231
+ }
232
+ db.prepare("INSERT INTO report_tasks VALUES (?, ?, ?)").run(task.task_id, task.scope, JSON.stringify(task));
233
+ return task;
234
+ }));
235
+ }
236
+ function appendEvent(root, taskId, kind, body, eventId) {
237
+ const encoded = JSON.stringify(body);
238
+ if (Buffer.byteLength(encoded) > MAX_EVENT_BYTES)
239
+ throw new Error("report event exceeds the bounded evidence limit");
240
+ const id = eventId ?? `hev_${randomBytes(12).toString("hex")}`;
241
+ if (!/^(hev|hocc)_[a-f0-9]{24}$/.test(id))
242
+ throw new Error("invalid report event identity");
243
+ const contentHash = reportHash(body);
244
+ return taskDb(root, db => transaction(db, () => {
245
+ const task = readTask(db, root, taskId);
246
+ const prior = db.prepare("SELECT task_id, kind, content_hash FROM report_events WHERE event_id = ?").get(id);
247
+ if (prior) {
248
+ if (prior.task_id !== taskId || prior.kind !== kind || prior.content_hash !== contentHash)
249
+ throw new Error("report event identity conflicts with existing evidence");
250
+ return id;
251
+ }
252
+ if (task.state !== "open" && !(kind === "check" && task.state === "interrupted"))
253
+ throw new Error("task is already closed; start a new task for new work");
254
+ if (kind === "check") {
255
+ const check = ReportCheckSchema.parse(body);
256
+ if (!check.check_id || !db.prepare("SELECT event_id FROM report_events WHERE event_id = ? AND task_id = ? AND kind = 'check-start'").get(check.check_id, taskId))
257
+ throw new Error("verification result has no matching start in this task");
258
+ }
259
+ const { total, bytes, pending } = db.prepare(`SELECT COUNT(*) AS total,
260
+ COALESCE(SUM(length(CAST(body AS BLOB))), 0) AS bytes,
261
+ SUM(CASE WHEN kind = 'check-start' THEN 1 WHEN kind = 'check' AND json_extract(body, '$.check_id') IS NOT NULL THEN -1 ELSE 0 END) AS pending
262
+ FROM report_events WHERE task_id = ?`).get(taskId);
263
+ const reserved = Math.max(0, (pending ?? 0) + (kind === "check-start" ? 1 : kind === "check" ? -1 : 0));
264
+ if (total + 1 + reserved > MAX_EVENTS || bytes + Buffer.byteLength(encoded) + reserved * MAX_EVENT_BYTES > MAX_TASK_BYTES)
265
+ throw new Error("task observation limit reached; start a new task");
266
+ if (kind === "claim") {
267
+ const claim = ReportClaimSchema.parse(body);
268
+ const row = db.prepare("SELECT body FROM report_events WHERE event_id = ? AND task_id = ? AND kind = 'delivery'").get(claim.occurrence_id, taskId);
269
+ if (!row)
270
+ throw new Error("claim does not refer to a delivery in this task");
271
+ const delivery = JSON.parse(row.body);
272
+ if (!delivery.records.some(r => r.record_id === claim.record_id && r.content_hash === claim.content_hash))
273
+ throw new Error("claim record revision was not delivered in this task");
274
+ }
275
+ const inserted = db.prepare("INSERT INTO report_events VALUES (?, ?, ?, ?, ?, ?)").run(id, taskId, kind, new Date().toISOString(), encoded, contentHash);
276
+ indexDeliveryRecords(db, id, kind, body);
277
+ // Advance only over contiguous observed inserts; an older writer may have
278
+ // left unindexed events. Historical gaps are filled by bounded reads above.
279
+ const seq = Number(inserted.lastInsertRowid);
280
+ db.prepare("UPDATE report_history_progress SET through_rowid = ? WHERE id = 1 AND through_rowid = ?").run(seq, seq - 1);
281
+ return id;
282
+ }));
283
+ }
284
+ /** Strict operation for explicit callers. Passive integrations catch failure
285
+ * and disclose it without blocking context delivery. Empty envelopes count. */
286
+ export function recordTaskDelivery(root, taskId, envelope, records, occurrenceId = `hocc_${randomBytes(12).toString("hex")}`) {
287
+ assertDeliveryEnvelope(envelope);
288
+ const snapshots = z.array(ReportRecordSchema).max(512).parse(records);
289
+ const seen = new Set();
290
+ for (const record of snapshots) {
291
+ const key = `${record.kind}:${record.record_id}`;
292
+ if (seen.has(key) || !envelope.delivered.some(r => r.record_id === record.record_id && r.kind === record.kind))
293
+ throw new Error("snapshot is duplicated or was not delivered");
294
+ seen.add(key);
295
+ }
296
+ return appendEvent(root, taskId, "delivery", { envelope, records: snapshots }, occurrenceId);
297
+ }
298
+ export function recordReportClaim(root, taskId, claim) {
299
+ const value = ReportClaimSchema.parse(claim);
300
+ return appendEvent(root, taskId, "claim", value, `hev_${reportHash({ taskId, value }).slice(7, 31)}`);
301
+ }
302
+ /** Internal write-path observers; no MCP endpoint accepts fabricated saves or
303
+ * publication claims. Retained observations never authorize memory mutations. */
304
+ export function recordReportSave(root, taskId, save) {
305
+ return appendEvent(root, taskId, "save", ReportSaveSchema.parse(save));
306
+ }
307
+ export function recordReportDurability(root, taskId, proof) {
308
+ const value = ReportDurabilitySchema.parse(proof);
309
+ const report = readTaskReport(root, taskId);
310
+ if (!report.saves.some(s => s.event_id === value.save_id && s.record.content_hash === value.record_hash))
311
+ throw new Error("durability proof does not name an exact save in this task");
312
+ return appendEvent(root, taskId, "durability", value);
313
+ }
314
+ /** Internal observer of an actual emitted edit denial. This is not a model
315
+ * assertion and does not establish that the host honored the response. */
316
+ export function recordReportRefusal(root, taskId, refusal) {
317
+ return appendEvent(root, taskId, "refusal", ReportRefusalSchema.parse(refusal));
318
+ }
319
+ /** Only the local rule runner calls this; no MCP endpoint accepts a submitted
320
+ * outcome. The evaluated revision must be one this task actually received. */
321
+ export function recordReportConformance(root, taskId, conformance) {
322
+ const value = ReportConformanceSchema.parse(conformance);
323
+ const report = readTaskReport(root, taskId);
324
+ if (!report.deliveries.some(d => d.records.some(r => r.kind === value.kind && r.record_id === value.record_id && r.content_hash === value.content_hash)))
325
+ throw new Error("rule evaluation does not name a record revision delivered in this task");
326
+ return appendEvent(root, taskId, "conformance", value);
327
+ }
328
+ /** Only the local runner calls this. MCP never accepts a claimed successful check. */
329
+ export function recordReportCheck(root, taskId, check) {
330
+ const value = ReportCheckSchema.parse(check);
331
+ if (!value.check_id)
332
+ throw new Error("verification result requires a reserved check identity");
333
+ return appendEvent(root, taskId, "check", value, `hev_${reportHash({ taskId, check: value.check_id }).slice(7, 31)}`);
334
+ }
335
+ export function beginReportCheck(root, taskId, label) {
336
+ return appendEvent(root, taskId, "check-start", { label: safeText(200).parse(label) });
337
+ }
338
+ export function finishReportTask(root, taskId, state = "completed") {
339
+ return taskDb(root, db => transaction(db, () => {
340
+ const task = readTask(db, root, taskId);
341
+ if (task.state !== "open") {
342
+ if (task.state !== state)
343
+ throw new Error("task already closed with a different outcome");
344
+ return task;
345
+ }
346
+ if (state === "completed") {
347
+ const { pending } = db.prepare(`SELECT SUM(CASE WHEN kind = 'check-start' THEN 1 WHEN kind = 'check' AND json_extract(body, '$.check_id') IS NOT NULL THEN -1 ELSE 0 END) AS pending FROM report_events WHERE task_id = ?`).get(taskId);
348
+ if ((pending ?? 0) > 0)
349
+ throw new Error("verification is still running or was interrupted; wait for its result or close the task as interrupted");
350
+ }
351
+ const finished = TaskSchema.parse({ ...task, state, finished_at: new Date().toISOString() });
352
+ db.prepare("UPDATE report_tasks SET body = ? WHERE task_id = ?").run(JSON.stringify(finished), taskId);
353
+ return finished;
354
+ }));
355
+ }
356
+ export function listReportTasks(root) {
357
+ return taskDb(root, db => db.prepare("SELECT body FROM report_tasks WHERE scope = ? ORDER BY rowid DESC LIMIT 30").all(scopeOf(root)).map(r => TaskSchema.parse(JSON.parse(r.body))));
358
+ }
359
+ export function reportActivity(root) {
360
+ if (!existsSync(join(root, ".hunch-cache", "served.db")))
361
+ return "Task reporting: no task activity observed yet. Reconnect the agent after updating; inspect with `hunch report`.";
362
+ try {
363
+ return taskDb(root, db => {
364
+ const row = db.prepare(`SELECT COUNT(*) AS total, SUM(CASE WHEN json_extract(body, '$.state') = 'completed' THEN 1 ELSE 0 END) AS completed FROM report_tasks WHERE scope = ?`).get(scopeOf(root));
365
+ const { deliveries } = db.prepare("SELECT COUNT(*) AS deliveries FROM report_events WHERE kind = 'delivery' AND task_id IN (SELECT task_id FROM report_tasks WHERE scope = ?)").get(scopeOf(root));
366
+ return `Task reporting: ${row.total} observed task(s), ${row.completed ?? 0} completed report(s), ${deliveries} linked context delivery(s). Activity alone does not prove contribution; inspect with \`hunch report\`.`;
367
+ });
368
+ }
369
+ catch {
370
+ return "Task reporting: observation ledger unavailable; activity and contribution are unverified.";
371
+ }
372
+ }
373
+ /** Exact task deletion is a user-invoked operation, never a memory deletion. */
374
+ export function forgetReportTask(root, taskId) {
375
+ taskDb(root, db => transaction(db, () => {
376
+ const task = readTask(db, root, taskId);
377
+ if (task.state === "open")
378
+ throw new Error("close the task before deleting its report history");
379
+ const file = assertReportPath(root, ".hunch-cache", "reports", `${task.task_id}.html`);
380
+ rmSync(file, { force: true });
381
+ rmSync(assertReportPath(root, ".hunch-cache", "reports", `${task.task_id}.public.html`), { force: true });
382
+ db.prepare("DELETE FROM report_record_links WHERE event_id IN (SELECT event_id FROM report_events WHERE task_id = ?)").run(taskId);
383
+ db.prepare("DELETE FROM report_events WHERE task_id = ?").run(taskId);
384
+ db.prepare("DELETE FROM report_tasks WHERE task_id = ?").run(taskId);
385
+ db.exec("UPDATE report_history_progress SET through_rowid = 0 WHERE id = 1");
386
+ }));
387
+ }
388
+ export function pruneReportHistory(root, olderThanDays = 90) {
389
+ if (!Number.isInteger(olderThanDays) || olderThanDays < 1 || olderThanDays > 3650)
390
+ throw new Error("retention must be 1–3650 days");
391
+ const cutoff = new Date(Date.now() - olderThanDays * 86_400_000).toISOString();
392
+ return taskDb(root, db => transaction(db, () => {
393
+ const expired = db.prepare(`SELECT task_id FROM report_tasks
394
+ WHERE scope = ? AND ((json_extract(body, '$.state') != 'open' AND json_extract(body, '$.finished_at') < ?)
395
+ OR (json_extract(body, '$.state') = 'open' AND json_extract(body, '$.started_at') < ?)) LIMIT 1000`)
396
+ .all(scopeOf(root), cutoff, cutoff);
397
+ for (const task of expired) {
398
+ // Serialize expiry with concurrent starts/results. Discard abandoned
399
+ // evidence without inventing a completion or interruption observation.
400
+ TaskIdSchema.parse(task.task_id);
401
+ rmSync(assertReportPath(root, ".hunch-cache", "reports", `${task.task_id}.html`), { force: true });
402
+ rmSync(assertReportPath(root, ".hunch-cache", "reports", `${task.task_id}.public.html`), { force: true });
403
+ db.prepare("DELETE FROM report_record_links WHERE event_id IN (SELECT event_id FROM report_events WHERE task_id = ?)").run(task.task_id);
404
+ db.prepare("DELETE FROM report_events WHERE task_id = ?").run(task.task_id);
405
+ db.prepare("DELETE FROM report_tasks WHERE task_id = ?").run(task.task_id);
406
+ }
407
+ if (expired.length)
408
+ db.exec("UPDATE report_history_progress SET through_rowid = 0 WHERE id = 1");
409
+ return expired.length;
410
+ }));
411
+ }
412
+ export function reportPresentationEnabled(root) {
413
+ try {
414
+ return JSON.parse(readFileSync(join(root, ".hunch", "local.json"), "utf8")).reportPresentation !== false;
415
+ }
416
+ catch {
417
+ return true;
418
+ }
419
+ }
420
+ export function readTaskReport(root, taskId, currentSnapshot = null) {
421
+ const { task, events } = taskDb(root, db => transaction(db, () => {
422
+ const task = readTask(db, root, taskId);
423
+ const { bytes } = db.prepare("SELECT COALESCE(SUM(length(CAST(body AS BLOB))), 0) AS bytes FROM report_events WHERE task_id = ?").get(taskId);
424
+ if (bytes > MAX_TASK_BYTES)
425
+ throw new Error("report exceeds the bounded evidence byte limit");
426
+ const events = db.prepare("SELECT event_id, kind, at, body, content_hash FROM report_events WHERE task_id = ? ORDER BY rowid LIMIT ?").all(taskId, MAX_EVENTS + 1);
427
+ if (events.length > MAX_EVENTS)
428
+ throw new Error("report exceeds the bounded observation limit");
429
+ if (events.reduce((sum, event) => sum + Buffer.byteLength(event.body), 0) > MAX_TASK_BYTES)
430
+ throw new Error("report exceeds the bounded evidence byte limit");
431
+ return { task, events };
432
+ }, true));
433
+ // Parse and derive outside the transaction: rendering must not hold a write lock.
434
+ const deliveries = [], claims = [], checks = [], refusals = [], saves = [], conformance = [];
435
+ for (const event of events) {
436
+ const value = JSON.parse(event.body);
437
+ if (reportHash(value) !== event.content_hash)
438
+ throw new Error("report evidence hash mismatch");
439
+ if (event.kind === "delivery") {
440
+ assertDeliveryEnvelope(value.envelope);
441
+ deliveries.push({ occurrence_id: event.event_id, at: event.at, receipt_id: value.envelope.receipt_id, envelope_hash: reportHash(value.envelope), envelope: value.envelope, records: z.array(ReportRecordSchema).parse(value.records) });
442
+ }
443
+ else if (event.kind === "claim")
444
+ claims.push({ ...ReportClaimSchema.parse(value), at: event.at, attribution: "agent-reported", supported_by: null });
445
+ else if (event.kind === "conformance") {
446
+ const rule = ReportConformanceSchema.parse(value);
447
+ conformance.push({ ...rule, event_id: event.event_id, at: event.at, current: currentSnapshot !== null && rule.snapshot === currentSnapshot });
448
+ }
449
+ else if (event.kind === "save")
450
+ saves.push({ ...ReportSaveSchema.parse(value), event_id: event.event_id, at: event.at, durability: "local", proofs: [] });
451
+ else if (event.kind === "durability") {
452
+ const proof = ReportDurabilitySchema.parse(value);
453
+ const save = saves.find(s => s.event_id === proof.save_id && s.record.content_hash === proof.record_hash);
454
+ if (!save)
455
+ throw new Error("durability proof does not match retained save");
456
+ save.proofs.push({ ...proof, at: event.at });
457
+ if (save.durability !== "pushed")
458
+ save.durability = proof.durability;
459
+ }
460
+ else if (event.kind === "refusal")
461
+ refusals.push({ ...ReportRefusalSchema.parse(value), event_id: event.event_id, at: event.at });
462
+ else if (event.kind === "check") {
463
+ const check = ReportCheckSchema.parse(value);
464
+ checks.push({ ...check, at: event.at, current: currentSnapshot !== null && check.before_snapshot === currentSnapshot && check.after_snapshot === currentSnapshot });
465
+ }
466
+ else if (event.kind !== "check-start")
467
+ throw new Error("unsupported report event kind; update Hunch before reading this report");
468
+ }
469
+ // The LAST evaluation of a record revision is its standing; a satisfied rule
470
+ // supports a claim only while the evaluated source is still the current source.
471
+ const standing = new Map();
472
+ for (const rule of conformance)
473
+ standing.set(`${rule.kind}:${rule.record_id}:${rule.content_hash}`, rule);
474
+ const supported = [...standing.values()].filter(rule => rule.outcome === "satisfied" && rule.current);
475
+ for (const claim of claims) {
476
+ const rule = supported.find(r => r.record_id === claim.record_id && r.content_hash === claim.content_hash);
477
+ claim.supported_by = rule?.event_id ?? null;
478
+ }
479
+ const delivered = deliveries.reduce((n, d) => n + d.envelope.delivered.length, 0);
480
+ const unknowns = [];
481
+ if (!deliveries.length)
482
+ unknowns.push("No task-linked context delivery was observed. This does not establish that the agent did not use Hunch.");
483
+ if (deliveries.some(d => d.records.length !== d.envelope.delivered.length))
484
+ unknowns.push("Some delivered records lack an exact retained snapshot.");
485
+ if (deliveries.some(d => d.envelope.abstention.active))
486
+ unknowns.push("Some memory was withheld because relevance or confidence was insufficient.");
487
+ if (!claims.length && delivered && !supported.length)
488
+ unknowns.push("Memory was delivered; its contribution to the result is unverified.");
489
+ if (claims.some(c => !c.supported_by))
490
+ unknowns.push("An agent-reported application has no current, satisfied rule evaluation supporting it.");
491
+ if ([...standing.values()].some(r => r.outcome === "violated"))
492
+ unknowns.push("The changed files violate a delivered lesson's declared rule.");
493
+ if ([...standing.values()].some(r => r.outcome === "satisfied" && !r.current))
494
+ unknowns.push("A delivered rule was satisfied when evaluated, but source has changed since; re-run the evaluation.");
495
+ if ([...standing.values()].some(r => r.outcome === "not-exercised"))
496
+ unknowns.push("Some delivered rules were not exercised: no changed file falls in their scope.");
497
+ if ([...standing.values()].some(r => r.outcome === "unavailable"))
498
+ unknowns.push("Some delivered lessons declare no machine-checkable rule or could not be evaluated; their application is agent-reported at best.");
499
+ if (saves.some(s => s.durability === "local"))
500
+ unknowns.push("Some saved revisions have no exact Git commit or push proof; they were observed written locally.");
501
+ if (refusals.length)
502
+ unknowns.push("A denial response was emitted. Whether the host honored it, or a bug was prevented, is not established.");
503
+ if (!checks.length)
504
+ unknowns.push("No independent command verification was recorded for this task.");
505
+ if (events.filter(e => e.kind === "check-start").length > checks.filter(c => c.check_id).length)
506
+ unknowns.push("A verification command is still running or its result was not retained.");
507
+ if (task.state === "open")
508
+ unknowns.push("Task is still open; completion has not been observed.");
509
+ if (task.state === "interrupted")
510
+ unknowns.push("Task was interrupted; no successful completion is implied.");
511
+ const unsigned = { schema: TASK_REPORT_SCHEMA, task, deliveries, claims, checks, conformance, refusals, saves, coverage: !deliveries.length ? "no-delivery-observed" : !delivered ? "no-relevant-memory" : "delivered", unknowns };
512
+ return { ...unsigned, content_hash: reportHash(unsigned) };
513
+ }
514
+ //# sourceMappingURL=taskReport.js.map
@@ -0,0 +1,8 @@
1
+ import { type GitMemoryObserver } from "../extractors/git.js";
2
+ export declare function observeReportCapture(root: string, taskId: string | undefined, kind: "decisions" | "constraints" | "findings", stored: {
3
+ id: string;
4
+ }, home: "public" | "private", existed: boolean, hunchDir?: string): {
5
+ saveId: string | null;
6
+ note: string;
7
+ observe?: GitMemoryObserver;
8
+ };
@@ -0,0 +1,36 @@
1
+ /** Observes the return of an actual store write. A missing report cannot roll
2
+ * back durable memory or turn the primary successful capture into an error. */
3
+ import { execFileSync } from "node:child_process";
4
+ import { foreignRepoEnv } from "../extractors/git.js";
5
+ import { ReportRecordSchema, recordReportSave, recordReportDurability, reportHash } from "./taskReport.js";
6
+ export function observeReportCapture(root, taskId, kind, stored, home, existed, hunchDir) {
7
+ if (!taskId)
8
+ return { saveId: null, note: "" };
9
+ try {
10
+ // The store's atomic JSON writer omits undefined properties. Match those
11
+ // persisted bytes' semantic value, not an in-memory optional-property shape.
12
+ const raw = JSON.parse(JSON.stringify(stored));
13
+ const text = (...keys) => keys.map(k => raw[k]).find(v => typeof v === "string");
14
+ const record = ReportRecordSchema.parse({ record_id: stored.id, kind, content_hash: reportHash(raw),
15
+ title: (text("title", "statement") ?? stored.id).slice(0, 500),
16
+ lesson: (text("decision", "statement", "observation", "title") ?? stored.id).slice(0, 12_000),
17
+ recorded_at: text("date", "observed_at", "valid_from") ?? null });
18
+ const saveId = recordReportSave(root, taskId, { source: "store-write", record, home, operation: existed ? "updated" : "created" });
19
+ const observe = hunchDir ? event => {
20
+ try {
21
+ const env = { ...foreignRepoEnv(process.env), GIT_NO_REPLACE_OBJECTS: "1", GIT_OPTIONAL_LOCKS: "0" };
22
+ const prefix = execFileSync("git", ["-C", hunchDir, "rev-parse", "--show-prefix"], { env, encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"] }).trim();
23
+ const rawBlob = execFileSync("git", ["-C", hunchDir, "show", `${event.commitSha}:${prefix}${kind}/${stored.id}.json`], { env, encoding: "utf8", timeout: 2_000, maxBuffer: 2_000_000, stdio: ["ignore", "pipe", "ignore"] });
24
+ if (reportHash(JSON.parse(rawBlob)) !== record.content_hash)
25
+ return;
26
+ recordReportDurability(root, taskId, { save_id: saveId, durability: event.kind === "published" ? "pushed" : "committed", commit: event.commitSha, record_hash: record.content_hash, source: "git-record-proof", ...(event.kind === "published" ? { publication: { ref: event.ref, basis: event.basis } } : {}) });
27
+ }
28
+ catch { /* skipped, changed, or unavailable proof remains unverified */ }
29
+ } : undefined;
30
+ return { saveId, observe, note: `\nTask evidence: saved revision retained in ${taskId}.` };
31
+ }
32
+ catch {
33
+ return { saveId: null, note: "\nTask report unavailable for this capture. The primary memory write succeeded; its task association is unverified." };
34
+ }
35
+ }
36
+ //# sourceMappingURL=taskReportCapture.js.map
@@ -0,0 +1,26 @@
1
+ import type { HunchStore } from "../store/hunchStore.js";
2
+ import type { DeliveryEnvelope } from "./delivery.js";
3
+ import { type ReportCheck, type ReportConformance, type ReportRecord } from "./taskReport.js";
4
+ export declare function snapshotDeliveredRecords(store: HunchStore, envelope: DeliveryEnvelope): ReportRecord[];
5
+ export interface ReportSnapshot {
6
+ hash: string | null;
7
+ limitations: string[];
8
+ }
9
+ /** Bounded source snapshot, not a committed change proof. Ignored files and
10
+ * Hunch's own memory/cache are excluded and explicitly disclosed. */
11
+ export declare function reportSourceSnapshot(root: string): ReportSnapshot;
12
+ /** Evaluate every delivered lesson's DECLARED rule against the files this task
13
+ * changed (working tree vs HEAD). Deterministic and Hunch-owned: the agent
14
+ * neither chooses the rule nor submits the verdict. A constraint is judged by its
15
+ * forbids matcher over the added lines of its scoped changed files; a decision by
16
+ * its conformance predicates over the current working graph, and only when a
17
+ * predicate's subject lives in a changed file. Everything else stays
18
+ * `not-exercised` or `unavailable` — never "satisfied" by file overlap. */
19
+ export declare function runReportConformance(root: string, store: HunchStore, taskId: string): ReportConformance[];
20
+ /** A deliberately explicit command wrapper. The caller chooses the command;
21
+ * reports never execute commands automatically to validate submitted claims. */
22
+ export declare function runReportCheck(root: string, taskId: string, command: string[], label: string, timeoutMs?: number, options?: {
23
+ signal?: AbortSignal;
24
+ onStdout?: (chunk: Buffer) => void;
25
+ onStderr?: (chunk: Buffer) => void;
26
+ }): Promise<ReportCheck>;