@davesheffer/hunch 1.32.0 → 1.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,7 +59,7 @@ hunch integrations check --harness claude --probe --require mcp
59
59
  hunch integrations check --harness codex --require context,edit-blocking
60
60
  ```
61
61
 
62
- Capabilities are reported as **verified**, **advisory-only**, **unsupported** or **untested**. `--require` fails unless every named capability is verified.
62
+ Capabilities are reported as **verified**, **advisory-only**, **unsupported** or **untested**. `--require` fails unless every named capability is verified. `mcp` is verified by a fresh-server probe; hook capabilities become verified only from lifecycle events actually delivered to Hunch's hook on the expected version within the last 30 days (machine-local evidence, the same trust level as the served ledger), so a repository whose agent has actually run shows it, and one that only has configuration does not.
63
63
 
64
64
  The Codex integration currently supplies MCP and instructions, with no native lifecycle adapter. The opt-in `--probe` verifies a fresh MCP process, not whether an existing host session or model actually followed the memory.
65
65
 
package/dist/cli/index.js CHANGED
@@ -83,6 +83,7 @@ import { recordServed, servedSummary } from "../core/served.js";
83
83
  import { recordTaskDelivery, reportActivity } from "../core/taskReport.js";
84
84
  import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
85
85
  import { hookReportTaskId, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
86
+ import { recordHookObservation } from "../core/hookObservations.js";
86
87
  import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
87
88
  import { PIPELINE_LOOP, armExecutionObligations, beforeEditProbeVerdict, compileExecutableProbes, environmentExecutableProbes, environmentExecutionObligations, executionObligationBrief, isProductPath, loadPipelineState, onCommand, onEdit, onPrompt, onSkill, pipelineEnabled, proofCheckpoint, savePipelineState, stopVerdict, unverifiedNag, } from "../core/pipeline.js";
88
89
  import { draftDuplicateOf, isAcceptedDuplicateAnchor } from "../core/dupdetect.js";
@@ -4471,6 +4472,9 @@ program
4471
4472
  if (!evt)
4472
4473
  return;
4473
4474
  const root = findRoot();
4475
+ // The host delivered this event: runtime evidence for `hunch integrations check`,
4476
+ // recorded before any policy decision so firmness never hides delivery itself.
4477
+ recordHookObservation(root, provider, evt.hook_event_name);
4474
4478
  const paths = hunchPaths(root);
4475
4479
  const firmness = readConfig(paths).firmness;
4476
4480
  if (firmness === "off")
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { findRoot } from "../core/paths.js";
3
3
  import { writeFileAtomic } from "../core/io.js";
4
4
  import { finishReportTask, forgetReportTask, listReportTasks, pruneReportHistory, readTaskReport, readLessonHistory, startReportTask } from "../core/taskReport.js";
5
- import { reportSourceSnapshot, runReportCheck, runReportConformance } from "../core/taskReportEvidence.js";
5
+ import { DEFAULT_CHECK_TIMEOUT_MS, MAX_CHECK_TIMEOUT_MS, reportSourceSnapshot, runReportCheck, runReportConformance } from "../core/taskReportEvidence.js";
6
6
  import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.js";
7
7
  import { assertReportPath } from "../core/taskReportPaths.js";
8
8
  import { publicTaskReport } from "../core/taskReportPublic.js";
@@ -59,8 +59,12 @@ export function registerTaskReportCommands(program, openStore) {
59
59
  });
60
60
  task.command("verify <id> <command...>").description("Explicitly run a verification command and retain its result/hash, never raw output; use -- before the command")
61
61
  .option("--label <label>", "short name of the check", "Verification command")
62
+ .option("--timeout <seconds>", `seconds before the command tree is stopped and recorded as timed out (max ${MAX_CHECK_TIMEOUT_MS / 1000})`, String(DEFAULT_CHECK_TIMEOUT_MS / 1000))
62
63
  .option("--json", "emit only the result JSON, suppressing live command output")
63
64
  .action(async (id, command, opts) => {
65
+ const seconds = Number(opts.timeout);
66
+ if (!Number.isInteger(seconds) || seconds < 1 || seconds * 1000 > MAX_CHECK_TIMEOUT_MS)
67
+ throw new Error(`--timeout must be a whole number of seconds between 1 and ${MAX_CHECK_TIMEOUT_MS / 1000}`);
64
68
  const controller = new AbortController();
65
69
  let signalExit = 0;
66
70
  const interrupt = () => { signalExit = 130; controller.abort(); };
@@ -68,7 +72,7 @@ export function registerTaskReportCommands(program, openStore) {
68
72
  process.on("SIGINT", interrupt);
69
73
  process.on("SIGTERM", terminate);
70
74
  try {
71
- const result = await runReportCheck(findRoot(), id, command, opts.label, 120_000, {
75
+ const result = await runReportCheck(findRoot(), id, command, opts.label, seconds * 1000, {
72
76
  signal: controller.signal,
73
77
  onStdout: opts.json ? undefined : chunk => { process.stdout.write(chunk); },
74
78
  onStderr: opts.json ? undefined : chunk => { process.stderr.write(chunk); },
@@ -0,0 +1,9 @@
1
+ export interface HookObservation {
2
+ provider: string;
3
+ event: string;
4
+ at: string;
5
+ version: string;
6
+ }
7
+ /** Never throws (con_03a0b94b2e): a missing ledger costs evidence, not the edit. */
8
+ export declare function recordHookObservation(root: string, provider: string, event: string): void;
9
+ export declare function readHookObservations(root: string): HookObservation[];
@@ -0,0 +1,33 @@
1
+ /** Runtime evidence that a host actually delivered a lifecycle event to Hunch's
2
+ * hook. Configuration proves wiring; only an observed event proves delivery.
3
+ * One row per (provider, normalized event), machine-local, outside the
4
+ * rebuildable index. Recording is best-effort: a hook must never fail on it. */
5
+ import { existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { withServedDatabase } from "./served.js";
8
+ import { HUNCH_VERSION } from "./version.js";
9
+ function ensureTable(db) {
10
+ db.exec(`CREATE TABLE IF NOT EXISTS hook_observations (
11
+ provider TEXT NOT NULL, event TEXT NOT NULL, at TEXT NOT NULL, version TEXT NOT NULL,
12
+ PRIMARY KEY (provider, event)
13
+ )`);
14
+ }
15
+ /** Never throws (con_03a0b94b2e): a missing ledger costs evidence, not the edit. */
16
+ export function recordHookObservation(root, provider, event) {
17
+ try {
18
+ withServedDatabase(root, db => {
19
+ ensureTable(db);
20
+ db.prepare("INSERT OR REPLACE INTO hook_observations VALUES (?, ?, ?, ?)").run(provider, event, new Date().toISOString(), HUNCH_VERSION);
21
+ });
22
+ }
23
+ catch { /* evidence is optional; the hook response is not */ }
24
+ }
25
+ export function readHookObservations(root) {
26
+ if (!existsSync(join(root, ".hunch-cache", "served.db")))
27
+ return [];
28
+ return withServedDatabase(root, db => {
29
+ ensureTable(db);
30
+ return db.prepare("SELECT provider, event, at, version FROM hook_observations ORDER BY at DESC").all();
31
+ });
32
+ }
33
+ //# sourceMappingURL=hookObservations.js.map
@@ -17,6 +17,8 @@ export declare function reportSourceSnapshot(root: string): ReportSnapshot;
17
17
  * predicate's subject lives in a changed file. Everything else stays
18
18
  * `not-exercised` or `unavailable` — never "satisfied" by file overlap. */
19
19
  export declare function runReportConformance(root: string, store: HunchStore, taskId: string): ReportConformance[];
20
+ export declare const DEFAULT_CHECK_TIMEOUT_MS = 120000;
21
+ export declare const MAX_CHECK_TIMEOUT_MS: number;
20
22
  /** A deliberately explicit command wrapper. The caller chooses the command;
21
23
  * reports never execute commands automatically to validate submitted claims. */
22
24
  export declare function runReportCheck(root: string, taskId: string, command: string[], label: string, timeoutMs?: number, options?: {
@@ -190,11 +190,15 @@ export function runReportConformance(root, store, taskId) {
190
190
  return value;
191
191
  });
192
192
  }
193
+ export const DEFAULT_CHECK_TIMEOUT_MS = 120_000;
194
+ export const MAX_CHECK_TIMEOUT_MS = 6 * 60 * 60_000;
193
195
  /** A deliberately explicit command wrapper. The caller chooses the command;
194
196
  * reports never execute commands automatically to validate submitted claims. */
195
197
  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
+ // A full suite can legitimately run for half an hour (fnd_70dd5c4034); the
199
+ // bound exists so an abandoned runner cannot hold a task open indefinitely.
200
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_CHECK_TIMEOUT_MS)
201
+ throw new Error(`verification timeout must be between 1 and ${MAX_CHECK_TIMEOUT_MS} ms`);
198
202
  options.signal?.throwIfAborted();
199
203
  const task = readTaskReport(root, taskId).task;
200
204
  if (task.state !== "open")
@@ -8,6 +8,15 @@ import { writeFileAtomic } from "../core/io.js";
8
8
  import { HUNCH_VERSION } from "../core/version.js";
9
9
  import { readConfig } from "../core/config.js";
10
10
  import { hunchPaths } from "../core/paths.js";
11
+ import { readHookObservations } from "../core/hookObservations.js";
12
+ /** Normalized hook events that prove each capability was delivered by the host. */
13
+ const CAPABILITY_EVIDENCE = {
14
+ context: ["SessionStart", "UserPromptSubmit"],
15
+ "edit-blocking": ["PreToolUse"],
16
+ "failure-capture": ["PostToolUseFailure", "PostToolUse"],
17
+ compaction: ["PreCompact"],
18
+ };
19
+ const OBSERVATION_FRESH_MS = 30 * 86_400_000;
11
20
  export const CAPABILITIES = ["mcp", "context", "edit-blocking", "failure-capture", "compaction"];
12
21
  export const HARNESSES = {
13
22
  claude: { mcp: ".mcp.json", hooks: ".claude/settings.json", key: "mcpServers", events: ["SessionStart", "PreToolUse", "PostToolUseFailure", "PreCompact"] },
@@ -112,6 +121,14 @@ export function inspectIntegrations(root, selected) {
112
121
  }
113
122
  }
114
123
  };
124
+ // Machine-local runtime evidence; an unreadable ledger simply leaves hooks untested.
125
+ let observed = [];
126
+ try {
127
+ observed = readHookObservations(root);
128
+ }
129
+ catch {
130
+ observed = [];
131
+ }
115
132
  for (const harness of selected ? [selected] : Object.keys(HARNESSES)) {
116
133
  const spec = HARNESSES[harness];
117
134
  if (!selected && !existsSync(join(root, spec.mcp)) && (!spec.hooks || !existsSync(join(root, spec.hooks))))
@@ -163,7 +180,20 @@ export function inspectIntegrations(root, selected) {
163
180
  status.detail = `firmness=${firmness}; edits are not blocked`;
164
181
  }
165
182
  else {
166
- status.detail = `${event} configured; host delivery, matchers, and tool coverage are not verified`;
183
+ // Verified only by an event the host actually delivered, on the expected
184
+ // version, recently. Matchers and tool coverage beyond that event stay unproven.
185
+ const hit = observed.find(o => o.provider === harness && CAPABILITY_EVIDENCE[capability].includes(o.event));
186
+ const fresh = hit !== undefined && Date.now() - Date.parse(hit.at) <= OBSERVATION_FRESH_MS;
187
+ if (hit && fresh && hit.version === report.expectedVersion) {
188
+ status.status = "verified";
189
+ status.detail = `${hit.event} observed from the ${harness} host at ${hit.at} on Hunch ${hit.version}; matchers and tool coverage beyond that event are not verified`;
190
+ }
191
+ else if (hit) {
192
+ status.detail = `${event} configured; last observed ${hit.at} on Hunch ${hit.version}${hit.version === report.expectedVersion ? " (stale)" : `, not the expected ${report.expectedVersion}`}`;
193
+ }
194
+ else {
195
+ status.detail = `${event} configured; host delivery, matchers, and tool coverage are not verified`;
196
+ }
167
197
  }
168
198
  }
169
199
  }
@@ -250,7 +280,7 @@ export function formatIntegrationHealth(report) {
250
280
  `Hunch integrations — expected ${report.expectedVersion} (repository configuration only)`,
251
281
  ...report.harnesses.map(h => `${h.harness}:\n${CAPABILITIES.map(c => ` ${c}: ${h.capabilities[c].status} — ${h.capabilities[c].detail}`).join("\n")}`),
252
282
  ...report.issues.map(i => `ERROR ${i.file}: ${i.detail}`),
253
- "Configured hooks are untested until exercised inside the host. Global settings, active sessions, and model compliance are not verified.",
283
+ "Hooks become verified only from lifecycle events observed inside the host on the expected version within 30 days. Global settings, active sessions, and model compliance are not verified.",
254
284
  ].join("\n");
255
285
  }
256
286
  /** Bounded session warning; diagnostics must never break hook execution. */
@@ -1,3 +1,188 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { readTaskReport } from "../core/taskReport.js";
2
3
  import type { HunchStore } from "../store/hunchStore.js";
4
+ /** A tool result must fit the host's round-trip. The full report (every
5
+ * envelope's context text, every lesson) belongs to `hunch report --json` and
6
+ * the HTML view; MCP returns exact identities, verdicts and the card. Bounded
7
+ * by caps first, then by byte size (fnd_53991b877b: an 18-delivery task
8
+ * produced a 101 KB result the host refused). */
9
+ export declare const MCP_REPORT_BYTE_BUDGET = 48000;
10
+ export declare function boundedTaskReport(report: ReturnType<typeof readTaskReport>, deliveries?: number, recordsPerDelivery?: number): {
11
+ schema: "hunch.task-report-summary/1";
12
+ task: {
13
+ task_id: string;
14
+ scope: string;
15
+ title: string;
16
+ started_at: string;
17
+ finished_at: string | null;
18
+ state: "open" | "completed" | "interrupted";
19
+ };
20
+ coverage: "delivered" | "no-delivery-observed" | "no-relevant-memory";
21
+ content_hash: string;
22
+ unknowns: string[];
23
+ deliveries: {
24
+ occurrence_id: string;
25
+ receipt_id: string;
26
+ at: string;
27
+ envelope_hash: string;
28
+ delivered: number;
29
+ abstention: boolean;
30
+ records: {
31
+ record_id: string;
32
+ kind: string;
33
+ content_hash: string;
34
+ title: string;
35
+ }[];
36
+ more_records: number;
37
+ }[];
38
+ claims: {
39
+ action: string;
40
+ occurrence_id: string;
41
+ record_id: string;
42
+ content_hash: string;
43
+ at: string;
44
+ attribution: "agent-reported";
45
+ supported_by: string | null;
46
+ }[];
47
+ checks: {
48
+ check_id: string | undefined;
49
+ label: string;
50
+ command: string[];
51
+ exit_code: number | null;
52
+ timed_out: boolean;
53
+ cancelled: boolean;
54
+ current: boolean;
55
+ at: string;
56
+ }[];
57
+ conformance: {
58
+ record_id: string;
59
+ kind: "decisions" | "constraints";
60
+ content_hash: string;
61
+ rule: "constraint-forbids" | "decision-conformance";
62
+ outcome: "satisfied" | "violated" | "not-exercised" | "unavailable";
63
+ current: boolean;
64
+ files: string[];
65
+ detail: string;
66
+ }[];
67
+ saves: {
68
+ event_id: string;
69
+ at: string;
70
+ record_id: string;
71
+ kind: string;
72
+ content_hash: string;
73
+ title: string;
74
+ home: "public" | "private";
75
+ operation: "updated" | "created";
76
+ durability: "committed" | "pushed" | "local";
77
+ }[];
78
+ refusals: ({
79
+ source: "native-edit-gate";
80
+ outcome: "denial-emitted";
81
+ kind: "constraint" | "veto";
82
+ record_id: string;
83
+ target: string;
84
+ reason_hash: string;
85
+ } & {
86
+ event_id: string;
87
+ at: string;
88
+ })[];
89
+ omitted: {
90
+ deliveries: number;
91
+ claims: number;
92
+ checks: number;
93
+ conformance: number;
94
+ saves: number;
95
+ refusals: number;
96
+ };
97
+ full_report: string;
98
+ };
99
+ export declare function boundedTaskReportForHost(report: ReturnType<typeof readTaskReport>): {
100
+ schema: "hunch.task-report-summary/1";
101
+ task: {
102
+ task_id: string;
103
+ scope: string;
104
+ title: string;
105
+ started_at: string;
106
+ finished_at: string | null;
107
+ state: "open" | "completed" | "interrupted";
108
+ };
109
+ coverage: "delivered" | "no-delivery-observed" | "no-relevant-memory";
110
+ content_hash: string;
111
+ unknowns: string[];
112
+ deliveries: {
113
+ occurrence_id: string;
114
+ receipt_id: string;
115
+ at: string;
116
+ envelope_hash: string;
117
+ delivered: number;
118
+ abstention: boolean;
119
+ records: {
120
+ record_id: string;
121
+ kind: string;
122
+ content_hash: string;
123
+ title: string;
124
+ }[];
125
+ more_records: number;
126
+ }[];
127
+ claims: {
128
+ action: string;
129
+ occurrence_id: string;
130
+ record_id: string;
131
+ content_hash: string;
132
+ at: string;
133
+ attribution: "agent-reported";
134
+ supported_by: string | null;
135
+ }[];
136
+ checks: {
137
+ check_id: string | undefined;
138
+ label: string;
139
+ command: string[];
140
+ exit_code: number | null;
141
+ timed_out: boolean;
142
+ cancelled: boolean;
143
+ current: boolean;
144
+ at: string;
145
+ }[];
146
+ conformance: {
147
+ record_id: string;
148
+ kind: "decisions" | "constraints";
149
+ content_hash: string;
150
+ rule: "constraint-forbids" | "decision-conformance";
151
+ outcome: "satisfied" | "violated" | "not-exercised" | "unavailable";
152
+ current: boolean;
153
+ files: string[];
154
+ detail: string;
155
+ }[];
156
+ saves: {
157
+ event_id: string;
158
+ at: string;
159
+ record_id: string;
160
+ kind: string;
161
+ content_hash: string;
162
+ title: string;
163
+ home: "public" | "private";
164
+ operation: "updated" | "created";
165
+ durability: "committed" | "pushed" | "local";
166
+ }[];
167
+ refusals: ({
168
+ source: "native-edit-gate";
169
+ outcome: "denial-emitted";
170
+ kind: "constraint" | "veto";
171
+ record_id: string;
172
+ target: string;
173
+ reason_hash: string;
174
+ } & {
175
+ event_id: string;
176
+ at: string;
177
+ })[];
178
+ omitted: {
179
+ deliveries: number;
180
+ claims: number;
181
+ checks: number;
182
+ conformance: number;
183
+ saves: number;
184
+ refusals: number;
185
+ };
186
+ full_report: string;
187
+ };
3
188
  export declare function registerTaskReportTools(server: McpServer, getRoot: () => string, getStore: () => HunchStore): void;
@@ -6,6 +6,47 @@ import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.
6
6
  function applicationReferences(report) {
7
7
  return report.deliveries.flatMap(d => d.records.map(r => ({ occurrence_id: d.occurrence_id, record_id: r.record_id, content_hash: r.content_hash, title: r.title }))).slice(-100);
8
8
  }
9
+ /** A tool result must fit the host's round-trip. The full report (every
10
+ * envelope's context text, every lesson) belongs to `hunch report --json` and
11
+ * the HTML view; MCP returns exact identities, verdicts and the card. Bounded
12
+ * by caps first, then by byte size (fnd_53991b877b: an 18-delivery task
13
+ * produced a 101 KB result the host refused). */
14
+ export const MCP_REPORT_BYTE_BUDGET = 48_000;
15
+ export function boundedTaskReport(report, deliveries = 30, recordsPerDelivery = 25) {
16
+ const clip = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
17
+ const tail = (items, n) => items.slice(Math.max(0, items.length - n));
18
+ return {
19
+ schema: "hunch.task-report-summary/1",
20
+ task: report.task, coverage: report.coverage, content_hash: report.content_hash, unknowns: report.unknowns,
21
+ deliveries: tail(report.deliveries, deliveries).map(d => ({
22
+ occurrence_id: d.occurrence_id, receipt_id: d.receipt_id, at: d.at, envelope_hash: d.envelope_hash,
23
+ delivered: d.envelope.delivered.length, abstention: d.envelope.abstention.active,
24
+ records: d.records.slice(0, recordsPerDelivery).map(r => ({ record_id: r.record_id, kind: r.kind, content_hash: r.content_hash, title: clip(r.title, 160) })),
25
+ more_records: Math.max(0, d.records.length - recordsPerDelivery),
26
+ })),
27
+ claims: tail(report.claims, 20).map(c => ({ ...c, action: clip(c.action, 300) })),
28
+ checks: tail(report.checks, 30).map(c => ({ check_id: c.check_id, label: c.label, command: c.command.map(x => clip(x, 120)), exit_code: c.exit_code, timed_out: c.timed_out, cancelled: c.cancelled ?? false, current: c.current, at: c.at })),
29
+ conformance: tail(report.conformance, 50).map(r => ({ record_id: r.record_id, kind: r.kind, content_hash: r.content_hash, rule: r.rule, outcome: r.outcome, current: r.current, files: r.files.slice(0, 8), detail: clip(r.detail, 240) })),
30
+ saves: tail(report.saves, 30).map(s => ({ event_id: s.event_id, at: s.at, record_id: s.record.record_id, kind: s.record.kind, content_hash: s.record.content_hash, title: clip(s.record.title, 160), home: s.home, operation: s.operation, durability: s.durability })),
31
+ refusals: tail(report.refusals, 30),
32
+ omitted: {
33
+ deliveries: Math.max(0, report.deliveries.length - deliveries), claims: Math.max(0, report.claims.length - 20), checks: Math.max(0, report.checks.length - 30),
34
+ conformance: Math.max(0, report.conformance.length - 50), saves: Math.max(0, report.saves.length - 30), refusals: Math.max(0, report.refusals.length - 30),
35
+ },
36
+ full_report: `hunch report ${report.task.task_id} --json`,
37
+ };
38
+ }
39
+ export function boundedTaskReportForHost(report) {
40
+ // Shrink deterministically until the summary fits; identities are never dropped
41
+ // from what remains, and `omitted` says exactly how much fell off.
42
+ for (const [deliveries, records] of [[30, 25], [10, 10], [5, 5], [1, 5], [1, 1]]) {
43
+ const summary = boundedTaskReport(report, deliveries, records);
44
+ if (JSON.stringify(summary).length <= MCP_REPORT_BYTE_BUDGET)
45
+ return summary;
46
+ }
47
+ const minimal = boundedTaskReport(report, 1, 1);
48
+ return { ...minimal, deliveries: [], omitted: { ...minimal.omitted, deliveries: report.deliveries.length } };
49
+ }
9
50
  /** Reuse the MCP server's installation, not a potentially stale global binary.
10
51
  * Structured argv is authoritative; the shell hint uses literal quoting. */
11
52
  function verificationLauncher() {
@@ -34,7 +75,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
34
75
  throw new Error("start requires a short task title and no completion evidence");
35
76
  const task = startReportTask(root, title, task_id);
36
77
  const launcher = verificationLauncher();
37
- return { content: [{ type: "text", text: `Task ${task.task_id} · ${task.state}. Pass task_id to every hunch_context and decision/correction/finding capture call. Before the final response, finish with hunch_task and include its contribution card. For checks use this exact installation (the global hunch binary may be stale): ${launcher.shell} task verify ${task.task_id} -- <command> [arguments].` }], structuredContent: { task, verification_argv: [...launcher.argv, "task", "verify", task.task_id, "--"] } };
78
+ return { content: [{ type: "text", text: `Task ${task.task_id} · ${task.state}. Pass task_id to every hunch_context and decision/correction/finding capture call. Before the final response, finish with hunch_task and include its contribution card. For checks use this exact installation (the global hunch binary may be stale): ${launcher.shell} task verify ${task.task_id} -- <command> [arguments]. The default budget is 2 minutes; add --timeout <seconds> before -- for a long suite.` }], structuredContent: { task, verification_argv: [...launcher.argv, "task", "verify", task.task_id, "--"] } };
38
79
  }
39
80
  if (!task_id)
40
81
  throw new Error("finish requires the exact task_id");
@@ -57,7 +98,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
57
98
  }
58
99
  catch { /* retained report remains inspectable through MCP */ }
59
100
  const card = file ? renderTaskReport(report).replace(/^Evidence .*$/m, `Evidence [Open local report](<${file}>)`) : renderTaskReport(report);
60
- return { content: [{ type: "text", text: show ? card : "Task report retained. Automatic presentation is disabled; omit the contribution card from the final response." }], structuredContent: { ...report, presentation_enabled: show, contribution_card: show ? card : null, report_path: file } };
101
+ return { content: [{ type: "text", text: show ? card : "Task report retained. Automatic presentation is disabled; omit the contribution card from the final response." }], structuredContent: { ...boundedTaskReportForHost(report), presentation_enabled: show, contribution_card: show ? card : null, report_path: file } };
61
102
  }
62
103
  catch (error) {
63
104
  const message = `Task report unavailable: ${error.message}`;
@@ -75,7 +116,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
75
116
  });
76
117
  server.registerTool("hunch_report", {
77
118
  title: "Inspect the evidence for Hunch's contribution to a task",
78
- description: "Read task reports: exact delivered memory, agent-reported applications, observed command results and explicit unknowns. Supply lesson for exact revision history across tasks. With neither task_id nor lesson, lists recent tasks without guessing which is yours. html writes a local private evidence view. Not a causal impact score, public export, or authority to execute verification commands.",
119
+ description: "Read task reports: exact delivered memory, agent-reported applications, observed command results and explicit unknowns, as a bounded summary (identities and verdicts, not envelope text; the full report is `hunch report <id> --json`). Supply lesson for exact revision history across tasks. With neither task_id nor lesson, lists recent tasks without guessing which is yours. html writes a local private evidence view. Not a causal impact score, public export, or authority to execute verification commands.",
79
120
  inputSchema: { task_id: TaskIdSchema.optional(), lesson: LessonReferenceSchema.optional().describe("Exact kind and record_id, optionally content_hash, to inspect retained appearances across tasks. Partial indexing requires refreshing before pagination."), before: z.number().int().positive().optional(), html: z.boolean().optional(), cwd: z.string().optional() },
80
121
  }, async ({ task_id, lesson, before, html }) => {
81
122
  try {
@@ -96,7 +137,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
96
137
  }
97
138
  const report = readTaskReport(root, task_id, reportSourceSnapshot(root).hash);
98
139
  const file = html ? writeTaskReportHtml(root, task_id) : null;
99
- return { content: [{ type: "text", text: `${renderTaskReport(report)}${file ? `\nLocal evidence view: ${file}` : ""}` }], structuredContent: { ...report, application_references: applicationReferences(report), contribution_card: renderTaskReport(report), report_path: file } };
140
+ return { content: [{ type: "text", text: `${renderTaskReport(report)}${file ? `\nLocal evidence view: ${file}` : ""}` }], structuredContent: { ...boundedTaskReportForHost(report), application_references: applicationReferences(report), contribution_card: renderTaskReport(report), report_path: file } };
100
141
  }
101
142
  catch (error) {
102
143
  return { isError: true, content: [{ type: "text", text: `Invalid: ${error.message}` }] };
@@ -1,5 +1,6 @@
1
1
  import { type ReportClaim, type ReportRecord, type LessonReference } from "./core/taskReport.js";
2
2
  import { runReportCheck } from "./core/taskReportEvidence.js";
3
+ export { DEFAULT_CHECK_TIMEOUT_MS, MAX_CHECK_TIMEOUT_MS } from "./core/taskReportEvidence.js";
3
4
  import type { DeliveryEnvelope } from "./core/delivery.js";
4
5
  export type { TaskReport, ReportTask, ReportClaim, ReportCheck, ReportConformance, ReportSave, ReportDurability, ReportRefusal, ReportRecord, TaskDelivery, LessonReference, LessonHistory } from "./core/taskReport.js";
5
6
  export { TASK_REPORT_SCHEMA, TaskIdSchema } from "./core/taskReport.js";
@@ -28,7 +29,9 @@ export declare function createTaskReporter(root: string): {
28
29
  applied(taskId: string, claim: ReportClaim): string;
29
30
  /** Runs locally as argv, without a shell. Only use commands authorized by
30
31
  * the task owner. This API does not accept remote claimed-success receipts. */
31
- verify(taskId: string, command: string[], label: string, options?: Parameters<typeof runReportCheck>[5]): Promise<{
32
+ verify(taskId: string, command: string[], label: string, options?: Parameters<typeof runReportCheck>[5] & {
33
+ timeoutMs?: number;
34
+ }): Promise<{
32
35
  label: string;
33
36
  command: string[];
34
37
  exit_code: number | null;
@@ -3,7 +3,8 @@
3
3
  import { realpathSync } from "node:fs";
4
4
  import { z } from "zod";
5
5
  import { finishReportTask, listReportTasks, readTaskReport, readLessonHistory, recordReportClaim, recordTaskDelivery, reportHash, reportPresentationEnabled, startReportTask } from "./core/taskReport.js";
6
- import { reportSourceSnapshot, runReportCheck, runReportConformance } from "./core/taskReportEvidence.js";
6
+ import { DEFAULT_CHECK_TIMEOUT_MS, reportSourceSnapshot, runReportCheck, runReportConformance } from "./core/taskReportEvidence.js";
7
+ export { DEFAULT_CHECK_TIMEOUT_MS, MAX_CHECK_TIMEOUT_MS } from "./core/taskReportEvidence.js";
7
8
  import { renderTaskReport, writeTaskReportHtml } from "./core/taskReportRender.js";
8
9
  import { hunchPaths } from "./core/paths.js";
9
10
  import { HunchStore } from "./store/hunchStore.js";
@@ -34,7 +35,8 @@ export function createTaskReporter(root) {
34
35
  /** Runs locally as argv, without a shell. Only use commands authorized by
35
36
  * the task owner. This API does not accept remote claimed-success receipts. */
36
37
  verify(taskId, command, label, options) {
37
- return runReportCheck(scope, taskId, command, label, 120_000, options);
38
+ const { timeoutMs = DEFAULT_CHECK_TIMEOUT_MS, ...rest } = options ?? {};
39
+ return runReportCheck(scope, taskId, command, label, timeoutMs, rest);
38
40
  },
39
41
  /** Hunch evaluates each delivered lesson's declared rule against the changed
40
42
  * files. Deterministic and local; the harness supplies no verdict. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.32.0",
3
+ "version": "1.32.1",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://www.hunchmemory.com",
10
- "version": "1.32.0",
10
+ "version": "1.32.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.32.0",
16
+ "version": "1.32.1",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {