@davesheffer/hunch 1.32.5 → 1.32.7

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
@@ -61,7 +61,7 @@ hunch integrations check --harness codex --require context,edit-blocking
61
61
 
62
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
- Codex CLI 0.153+ gets a native lifecycle adapter (`.codex/hooks.json`: session orientation, prompt task IDs from `turn_id`, `apply_patch` pre-edit grounding and strict denial, Stop cards); project-layer hooks load only for a trusted project and must be trusted once in Codex with `/hooks`. The opt-in `--probe` verifies a fresh MCP process, not whether an existing host session or model actually followed the memory.
64
+ Codex CLI 0.153+ gets a native lifecycle adapter (`.codex/hooks.json`: session orientation, prompt task IDs from `turn_id`, `apply_patch` pre-edit grounding and strict denial, native `Bash`/`PowerShell` post-tool observation, Stop cards); project-layer hooks load only for a trusted project and must be trusted once in Codex with `/hooks`. Failure capture is certified only by an explicit failed-tool lifecycle event; a successful `PostToolUse` observation does not prove it. The opt-in `--probe` verifies a fresh MCP process, not whether an existing host session or model actually followed the memory.
65
65
 
66
66
  Use `hunch integrations check` in CI to prevent pin drift; add `--require` for capabilities your workflow cannot operate without.
67
67
 
@@ -288,4 +288,4 @@ See the [changelog](CHANGELOG.md) for release detail and the [roadmap](ROADMAP.m
288
288
  - [Architecture benchmark](bench/architectural-conformance.md)
289
289
  - [Contributing](CONTRIBUTING.md)
290
290
 
291
- Apache-2.0
291
+ Apache-2.0
package/dist/cli/index.js CHANGED
@@ -4470,7 +4470,7 @@ program
4470
4470
  const root = findRoot();
4471
4471
  // The host delivered this event: runtime evidence for `hunch integrations check`,
4472
4472
  // recorded before any policy decision so firmness never hides delivery itself.
4473
- recordHookObservation(root, provider, evt.hook_event_name);
4473
+ recordHookObservation(root, provider, evt.hook_event_name, evt.tool_outcome?.status);
4474
4474
  const paths = hunchPaths(root);
4475
4475
  const firmness = readConfig(paths).firmness;
4476
4476
  if (firmness === "off")
@@ -107,6 +107,38 @@ function toolOutput(value) {
107
107
  return "";
108
108
  }
109
109
  }
110
+ function explicitToolOutcome(response) {
111
+ const raw = obj(response);
112
+ if (!raw)
113
+ return typeof response === "string" && response.trim() ? "success" : "unknown";
114
+ if (raw.success === false || raw.is_error === true || raw.isError === true || (raw.error !== undefined && raw.error !== null))
115
+ return "failure";
116
+ const status = raw.status;
117
+ if (typeof status === "string") {
118
+ if (/^(?:failure|failed|error|errored)$/i.test(status.trim()))
119
+ return "failure";
120
+ }
121
+ let explicitSuccess = raw.success === true || raw.is_error === false || raw.isError === false;
122
+ for (const key of ["exit_code", "exitCode", "return_code", "returnCode"]) {
123
+ const value = raw[key];
124
+ const numeric = typeof value === "number" ? value : typeof value === "string" && /^-?\d+$/.test(value.trim()) ? Number(value) : undefined;
125
+ if (numeric === undefined || !Number.isFinite(numeric))
126
+ continue;
127
+ if (numeric !== 0)
128
+ return "failure";
129
+ explicitSuccess = true;
130
+ }
131
+ if (typeof status === "string" && /^(?:success|succeeded|ok|completed)$/i.test(status.trim()))
132
+ explicitSuccess = true;
133
+ if (explicitSuccess)
134
+ return "success";
135
+ // Common successful tool-result shapes carry output fields even when the
136
+ // output is empty. An unstructured empty string (Codex's native failure
137
+ // payload) remains unknown until the host supplies an explicit status.
138
+ if (["stdout", "stderr", "output", "content"].some(key => Object.prototype.hasOwnProperty.call(raw, key)))
139
+ return "success";
140
+ return "unknown";
141
+ }
110
142
  function normalizeToolOutcome(input, event) {
111
143
  if (event !== "PostToolUse" && event !== "PostToolUseFailure")
112
144
  return undefined;
@@ -114,11 +146,7 @@ function normalizeToolOutcome(input, event) {
114
146
  return {
115
147
  // Claude Code splits successful and failed calls into separate lifecycle
116
148
  // events. Providers without that split may expose an explicit result flag.
117
- status: event === "PostToolUseFailure"
118
- ? "failure"
119
- : obj(response)?.success === false || obj(response)?.is_error === true || obj(response)?.isError === true
120
- ? "failure"
121
- : "success",
149
+ status: event === "PostToolUseFailure" ? "failure" : explicitToolOutcome(response),
122
150
  output: event === "PostToolUseFailure"
123
151
  ? toolOutput(input.error ?? response)
124
152
  : toolOutput(response),
@@ -1,9 +1,12 @@
1
+ export type HookOutcomeEvidence = "success" | "failure" | "unknown";
1
2
  export interface HookObservation {
2
3
  provider: string;
3
4
  event: string;
4
5
  at: string;
5
6
  version: string;
7
+ /** Present for observations recorded after outcome tracking was added. */
8
+ outcome?: HookOutcomeEvidence | null;
6
9
  }
7
10
  /** Never throws (con_03a0b94b2e): a missing ledger costs evidence, not the edit. */
8
- export declare function recordHookObservation(root: string, provider: string, event: string): void;
11
+ export declare function recordHookObservation(root: string, provider: string, event: string, outcome?: HookOutcomeEvidence): void;
9
12
  export declare function readHookObservations(root: string): HookObservation[];
@@ -9,15 +9,28 @@ import { HUNCH_VERSION } from "./version.js";
9
9
  function ensureTable(db) {
10
10
  db.exec(`CREATE TABLE IF NOT EXISTS hook_observations (
11
11
  provider TEXT NOT NULL, event TEXT NOT NULL, at TEXT NOT NULL, version TEXT NOT NULL,
12
+ outcome TEXT,
12
13
  PRIMARY KEY (provider, event)
13
14
  )`);
15
+ // Existing machine ledgers predate outcome tracking. Their rows remain
16
+ // intentionally unknown until a later hook delivery supplies a result.
17
+ const columns = db.prepare("PRAGMA table_info(hook_observations)").all();
18
+ if (!columns.some(column => column.name === "outcome"))
19
+ db.exec("ALTER TABLE hook_observations ADD COLUMN outcome TEXT");
14
20
  }
15
21
  /** Never throws (con_03a0b94b2e): a missing ledger costs evidence, not the edit. */
16
- export function recordHookObservation(root, provider, event) {
22
+ export function recordHookObservation(root, provider, event, outcome) {
17
23
  try {
18
24
  withServedDatabase(root, db => {
19
25
  ensureTable(db);
20
- db.prepare("INSERT OR REPLACE INTO hook_observations VALUES (?, ?, ?, ?)").run(provider, event, new Date().toISOString(), HUNCH_VERSION);
26
+ const evidence = outcome === "failure" || outcome === "success" ? outcome : null;
27
+ const at = new Date().toISOString();
28
+ db.prepare(`INSERT INTO hook_observations (provider, event, at, version, outcome)
29
+ VALUES (?, ?, ?, ?, ?)
30
+ ON CONFLICT(provider, event) DO UPDATE SET
31
+ at = CASE WHEN excluded.outcome = 'failure' OR hook_observations.outcome IS NULL OR hook_observations.outcome != 'failure' THEN excluded.at ELSE hook_observations.at END,
32
+ version = CASE WHEN excluded.outcome = 'failure' OR hook_observations.outcome IS NULL OR hook_observations.outcome != 'failure' THEN excluded.version ELSE hook_observations.version END,
33
+ outcome = CASE WHEN excluded.outcome = 'failure' OR hook_observations.outcome = 'failure' THEN 'failure' ELSE excluded.outcome END`).run(provider, event, at, HUNCH_VERSION, evidence);
21
34
  });
22
35
  }
23
36
  catch { /* evidence is optional; the hook response is not */ }
@@ -27,7 +40,7 @@ export function readHookObservations(root) {
27
40
  return [];
28
41
  return withServedDatabase(root, db => {
29
42
  ensureTable(db);
30
- return db.prepare("SELECT provider, event, at, version FROM hook_observations ORDER BY at DESC").all();
43
+ return db.prepare("SELECT provider, event, at, version, outcome FROM hook_observations ORDER BY at DESC").all();
31
44
  });
32
45
  }
33
46
  //# sourceMappingURL=hookObservations.js.map
@@ -1769,7 +1769,11 @@ function waitForCommitLockHandoff(lock, first, timeoutMs) {
1769
1769
  while (Date.now() < deadline) {
1770
1770
  if (attempt.state === "acquired")
1771
1771
  return true;
1772
- if (attempt.state !== "held-live" || attempt.ownerPid === process.pid)
1772
+ // Once the first snapshot proved a live owner, an owner-less snapshot can be
1773
+ // the normal release window: recursive cleanup removes owner-<pid> before
1774
+ // removing the outer lock directory. Keep the bounded handoff wait through
1775
+ // that transient state instead of reporting a false busy/no-op result.
1776
+ if (attempt.state === "held-live" && attempt.ownerPid === process.pid)
1773
1777
  return false;
1774
1778
  Atomics.wait(sleeper, 0, 0, Math.min(25, deadline - Date.now()));
1775
1779
  attempt = acquireCommitLock(lock);
@@ -14,6 +14,8 @@ import { readHookObservations } from "../core/hookObservations.js";
14
14
  const CAPABILITY_EVIDENCE = {
15
15
  context: ["SessionStart", "UserPromptSubmit"],
16
16
  "edit-blocking": ["PreToolUse"],
17
+ // A successful PostToolUse only proves that the post hook ran. A provider
18
+ // may instead include an explicit failure status in that same event.
17
19
  "failure-capture": ["PostToolUseFailure", "PostToolUse"],
18
20
  compaction: ["PreCompact"],
19
21
  };
@@ -34,6 +36,15 @@ const object = (v) => {
34
36
  throw new Error("expected a configuration object");
35
37
  return v;
36
38
  };
39
+ function evidenceFor(capability, harness, observed, expectedVersion) {
40
+ const matches = observed.filter(o => o.provider === harness && (capability !== "failure-capture"
41
+ ? CAPABILITY_EVIDENCE[capability].includes(o.event)
42
+ : o.event === "PostToolUseFailure" || (o.event === "PostToolUse" && o.outcome === "failure")));
43
+ // A stale row must not hide a fresh result recorded by a newer hook. Keep a
44
+ // matching stale row as the fallback so the caller can explain why it is not
45
+ // verified rather than treating the evidence as absent.
46
+ return matches.find(o => o.version === expectedVersion && Date.now() - Date.parse(o.at) <= OBSERVATION_FRESH_MS) ?? matches[0];
47
+ }
37
48
  function strings(value) {
38
49
  if (typeof value === "string")
39
50
  return [value];
@@ -192,7 +203,7 @@ export function inspectIntegrations(root, selected) {
192
203
  else {
193
204
  // Verified only by an event the host actually delivered, on the expected
194
205
  // version, recently. Matchers and tool coverage beyond that event stay unproven.
195
- const hit = observed.find(o => o.provider === harness && CAPABILITY_EVIDENCE[capability].includes(o.event));
206
+ const hit = evidenceFor(capability, harness, observed, report.expectedVersion);
196
207
  const fresh = hit !== undefined && Date.now() - Date.parse(hit.at) <= OBSERVATION_FRESH_MS;
197
208
  if (hit && fresh && hit.version === report.expectedVersion) {
198
209
  status.status = "verified";
@@ -201,6 +212,9 @@ export function inspectIntegrations(root, selected) {
201
212
  else if (hit) {
202
213
  status.detail = `${event} configured; last observed ${hit.at} on Hunch ${hit.version}${hit.version === report.expectedVersion ? " (stale)" : `, not the expected ${report.expectedVersion}`}`;
203
214
  }
215
+ else if (capability === "failure-capture" && observed.some(o => o.provider === harness && o.event === "PostToolUse")) {
216
+ status.detail = `${event} configured; PostToolUse was observed, but no explicit failed-tool event was delivered, so failure capture remains untested`;
217
+ }
204
218
  else {
205
219
  status.detail = `${event} configured; host delivery, matchers, and tool coverage are not verified`;
206
220
  }
@@ -345,7 +345,9 @@ export function writeCodexHooks(root, inv) {
345
345
  SessionStart: [entry()],
346
346
  UserPromptSubmit: [entry()],
347
347
  PreToolUse: [entry("apply_patch")],
348
- PostToolUse: [entry("apply_patch|shell|local_shell")],
348
+ // Codex's native command tool arrives as `Bash` (or `PowerShell` on
349
+ // Windows), while older hosts may expose shell/local_shell names.
350
+ PostToolUse: [entry("apply_patch|Bash|PowerShell|shell|local_shell")],
349
351
  Stop: [entry()],
350
352
  PreCompact: [entry()],
351
353
  SubagentStart: [entry()],
@@ -1753,7 +1753,11 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
1753
1753
  spawned_decision: finding.spawned_decision ?? existing?.spawned_decision ?? null,
1754
1754
  observed_at: existing?.observed_at ?? now, // first observation wins — updates re-verify, not re-date
1755
1755
  resolved_commit: finding.resolved_commit ?? existing?.resolved_commit ?? null,
1756
- provenance: { source: "human_confirmed", confidence: 0.95, evidence: finding.evidence ?? existing?.provenance.evidence ?? [], last_verified: now },
1756
+ // Findings have no authenticated capture front door. Calling this MCP tool is
1757
+ // agent testimony, even when the observation is updating a record that a human
1758
+ // confirmed previously; only an explicit human-authored path may mint the
1759
+ // human_confirmed tier.
1760
+ provenance: { source: "agent_recorded", confidence: 0.75, evidence: finding.evidence ?? existing?.provenance.evidence ?? [], last_verified: now },
1757
1761
  };
1758
1762
  const stored = store.putCapture("findings", rec, !!finding.private);
1759
1763
  const observed = observeReportCapture(root, task_id, "findings", stored, home, !!existing, home === "private" ? store.privateDir ?? undefined : hunchPaths(root).hunch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.32.5",
3
+ "version": "1.32.7",
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.5",
10
+ "version": "1.32.7",
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.5",
16
+ "version": "1.32.7",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {