@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 +2 -2
- package/dist/cli/index.js +1 -1
- package/dist/core/agenthook.js +33 -5
- package/dist/core/hookObservations.d.ts +4 -1
- package/dist/core/hookObservations.js +16 -3
- package/dist/extractors/git.js +5 -1
- package/dist/integrations/health.js +15 -1
- package/dist/integrations/providers.js +3 -1
- package/dist/mcp/server.js +5 -1
- package/package.json +1 -1
- package/server.json +2 -2
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")
|
package/dist/core/agenthook.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/extractors/git.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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()],
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
-
|
|
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
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.
|
|
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.
|
|
16
|
+
"version": "1.32.7",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|