@davesheffer/hunch 1.32.0 → 1.32.2
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 +1 -1
- package/dist/cli/index.js +33 -25
- package/dist/cli/taskReport.js +6 -2
- package/dist/constitution/behaviorEvaluator.js +9 -2
- package/dist/constitution/renderEvaluations.d.ts +8 -0
- package/dist/constitution/renderEvaluations.js +48 -0
- package/dist/core/drift.d.ts +2 -1
- package/dist/core/drift.js +1 -0
- package/dist/core/hookObservations.d.ts +9 -0
- package/dist/core/hookObservations.js +33 -0
- package/dist/core/taskReport.d.ts +6 -0
- package/dist/core/taskReport.js +15 -0
- package/dist/core/taskReportEvidence.d.ts +2 -0
- package/dist/core/taskReportEvidence.js +6 -2
- package/dist/core/taskReportRender.d.ts +5 -0
- package/dist/core/taskReportRender.js +8 -0
- package/dist/integrations/health.js +32 -2
- package/dist/mcp/server.js +5 -2
- package/dist/mcp/taskReportTools.d.ts +185 -0
- package/dist/mcp/taskReportTools.js +45 -4
- package/dist/taskReports.d.ts +4 -1
- package/dist/taskReports.js +4 -2
- package/package.json +1 -1
- package/server.json +2 -2
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
|
@@ -80,16 +80,18 @@ import { appendEvent, readEvents } from "../core/events.js";
|
|
|
80
80
|
import { computeStats, formatStats } from "../core/stats.js";
|
|
81
81
|
import { injectionMode, resetSessionInjections } from "../core/hookcache.js";
|
|
82
82
|
import { recordServed, servedSummary } from "../core/served.js";
|
|
83
|
-
import { recordTaskDelivery, reportActivity } from "../core/taskReport.js";
|
|
83
|
+
import { recordTaskDelivery, reportActivity, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
|
|
84
84
|
import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
|
|
85
|
+
import { renderRecalledLine } from "../core/taskReportRender.js";
|
|
85
86
|
import { hookReportTaskId, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
87
|
+
import { recordHookObservation } from "../core/hookObservations.js";
|
|
86
88
|
import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
|
|
87
89
|
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
90
|
import { draftDuplicateOf, isAcceptedDuplicateAnchor } from "../core/dupdetect.js";
|
|
89
91
|
import { planAutoReview, planMutations } from "../core/autoreview.js";
|
|
90
92
|
import { loadGoldenSet, evaluateRetrieval, evaluateTraversalLift } from "../eval/harness.js";
|
|
91
93
|
import { loadGuardCases, evalGuards, generateGuardCases } from "../eval/guards.js";
|
|
92
|
-
import { computeDrift } from "../core/drift.js";
|
|
94
|
+
import { DRIFT_KINDS, computeDrift } from "../core/drift.js";
|
|
93
95
|
import { renderCompilerScorecard, scoreCompilerCaseBank } from "../constitution/scorecard.js";
|
|
94
96
|
import { generateWiki, wikiStatus, wikiPrompt, publicHome, privateHome, readWikiManifestAt, nowData } from "../wiki/wiki.js";
|
|
95
97
|
import { adoptProsePrompt } from "../wiki/adopt.js";
|
|
@@ -107,6 +109,7 @@ import { MAX_LANDSCAPE_REFRESH_REVISIONS, planLandscapeAdoption, } from "../core
|
|
|
107
109
|
import { discoverRepositoryLandscape } from "../extractors/landscapeDiscovery.js";
|
|
108
110
|
import { checkConformance } from "../core/conformance.js";
|
|
109
111
|
import { ConstitutionService, policyEvaluationEnvelope } from "../constitution/service.js";
|
|
112
|
+
import { renderPolicyEvaluations } from "../constitution/renderEvaluations.js";
|
|
110
113
|
import { sourceGraphSnapshot } from "../constitution/evaluator.js";
|
|
111
114
|
import { renderProofCard } from "../constitution/card.js";
|
|
112
115
|
import { movePolicyArtifactsToPrivate } from "../constitution/repository.js";
|
|
@@ -2294,20 +2297,6 @@ policyCmd
|
|
|
2294
2297
|
store.close();
|
|
2295
2298
|
}
|
|
2296
2299
|
});
|
|
2297
|
-
function renderPolicyEvaluations(results) {
|
|
2298
|
-
if (!results.length)
|
|
2299
|
-
return ["No Constitution policies matched."];
|
|
2300
|
-
const icon = { satisfied: "✅", violated: "⛔", not_applicable: "·", unknown: "?", error: "‼" };
|
|
2301
|
-
const out = [`Constitution policy evaluation: ${results.length} canonical receipt(s)`];
|
|
2302
|
-
for (const r of results) {
|
|
2303
|
-
out.push(` ${icon[r.evaluation.result] ?? "·"} ${r.policy.id} [${r.policy.state}] ${r.evaluation.result}${r.blocks ? " — BLOCK" : ""}`);
|
|
2304
|
-
out.push(` ${r.evaluation.explanation}`);
|
|
2305
|
-
if (r.gate_error)
|
|
2306
|
-
out.push(` gate error: ${r.gate_error}`);
|
|
2307
|
-
out.push(` receipt: ${r.evaluation.deterministic_hash}`);
|
|
2308
|
-
}
|
|
2309
|
-
return out;
|
|
2310
|
-
}
|
|
2311
2300
|
// ---- constitution (deterministic evidence -> candidate bootstrap) --------
|
|
2312
2301
|
const constitutionCmd = program
|
|
2313
2302
|
.command("constitution")
|
|
@@ -4200,8 +4189,9 @@ program
|
|
|
4200
4189
|
if (opts.task) {
|
|
4201
4190
|
try {
|
|
4202
4191
|
const records = asOf ? [] : snapshotDeliveredRecords(store, envelope);
|
|
4192
|
+
const recalled = renderRecalledLine(unseenLessons(root, opts.task, records));
|
|
4203
4193
|
const occurrence = recordTaskDelivery(root, opts.task, envelope, records);
|
|
4204
|
-
console.log(`\
|
|
4194
|
+
console.log(`\n${recalled ? `${recalled}\n` : ""}Task evidence: ${opts.task} · occurrence ${occurrence}`);
|
|
4205
4195
|
}
|
|
4206
4196
|
catch {
|
|
4207
4197
|
console.error(`Task evidence could not be recorded for ${opts.task}; context remains available but report attribution is unverified.`);
|
|
@@ -4471,6 +4461,9 @@ program
|
|
|
4471
4461
|
if (!evt)
|
|
4472
4462
|
return;
|
|
4473
4463
|
const root = findRoot();
|
|
4464
|
+
// The host delivered this event: runtime evidence for `hunch integrations check`,
|
|
4465
|
+
// recorded before any policy decision so firmness never hides delivery itself.
|
|
4466
|
+
recordHookObservation(root, provider, evt.hook_event_name);
|
|
4474
4467
|
const paths = hunchPaths(root);
|
|
4475
4468
|
const firmness = readConfig(paths).firmness;
|
|
4476
4469
|
if (firmness === "off")
|
|
@@ -4890,16 +4883,22 @@ program
|
|
|
4890
4883
|
}
|
|
4891
4884
|
receipts("served");
|
|
4892
4885
|
let reportNotice = "";
|
|
4886
|
+
let recalled = null;
|
|
4893
4887
|
if (reportTaskId) {
|
|
4894
4888
|
try {
|
|
4895
|
-
const
|
|
4889
|
+
const snapshots = snapshotDeliveredRecords(store, envelope);
|
|
4890
|
+
// The first time a lesson reaches this prompt's task, tell the USER in one
|
|
4891
|
+
// line (systemMessage); repeats of the same revision stay silent.
|
|
4892
|
+
recalled = reportPresentationEnabled(root) ? renderRecalledLine(unseenLessons(root, reportTaskId, snapshots)) : null;
|
|
4893
|
+
const occurrence = recordTaskDelivery(root, reportTaskId, envelope, snapshots);
|
|
4896
4894
|
reportNotice = `\n\nHunch task ${reportTaskId} · delivery ${occurrence}. Inspect exact application references with hunch_report(task_id).`;
|
|
4897
4895
|
}
|
|
4898
4896
|
catch {
|
|
4899
4897
|
reportNotice = "\n\nTask report observation unavailable; this delivery's task contribution remains unverified.";
|
|
4898
|
+
recalled = null;
|
|
4900
4899
|
}
|
|
4901
4900
|
}
|
|
4902
|
-
emitContext(provider, "PreToolUse", text + reportNotice);
|
|
4901
|
+
emitContext(provider, "PreToolUse", text + reportNotice, recalled ?? undefined);
|
|
4903
4902
|
}
|
|
4904
4903
|
catch {
|
|
4905
4904
|
// swallow — never block an edit on a hook failure
|
|
@@ -6027,8 +6026,13 @@ program
|
|
|
6027
6026
|
// ---- drift (doc≠graph detector; advisory + CI-gateable) -------------------
|
|
6028
6027
|
program
|
|
6029
6028
|
.command("drift")
|
|
6030
|
-
.description("Detect memory drift: dead refs, dangling supersedes, stale 'proposed' docs, commit-unresolvable (a decision cites a commit that no longer resolves in this repository), doc≠graph anchor-stale (a file still anchored to a superseded decision), markdown sections whose <!-- hunch:topic … dec_id --> pin points at a superseded or missing decision (AGENTS.md/CLAUDE.md as a drift surface), and ledger≠records replay divergence when this partition has a change ledger. Exits non-zero on any anchor-stale drift, topic collision or replay divergence — the doc≠graph and ledger≠records gate.")
|
|
6031
|
-
.
|
|
6029
|
+
.description("Detect memory drift: dead refs, dangling supersedes, stale 'proposed' docs, commit-unresolvable (a decision cites a commit that no longer resolves in this repository), doc≠graph anchor-stale (a file still anchored to a superseded decision), markdown sections whose <!-- hunch:topic … dec_id --> pin points at a superseded or missing decision (AGENTS.md/CLAUDE.md as a drift surface), and ledger≠records replay divergence when this partition has a change ledger. Exits non-zero on any anchor-stale drift, topic collision or replay divergence — the doc≠graph and ledger≠records gate. --fail-on adds further kinds to the gate (the release gate passes finding-stale).")
|
|
6030
|
+
.option("--fail-on <kinds>", `comma-separated drift kinds that also fail the gate (${DRIFT_KINDS.join(", ")})`)
|
|
6031
|
+
.action((opts) => {
|
|
6032
|
+
const failOn = new Set((opts.failOn ?? "").split(",").map((k) => k.trim()).filter(Boolean));
|
|
6033
|
+
for (const kind of failOn)
|
|
6034
|
+
if (!DRIFT_KINDS.includes(kind))
|
|
6035
|
+
return fail(`--fail-on: unknown drift kind "${kind}" (known: ${DRIFT_KINDS.join(", ")})`);
|
|
6032
6036
|
const { store, root } = storeFor();
|
|
6033
6037
|
try {
|
|
6034
6038
|
const { findings } = computeDrift(store, root);
|
|
@@ -6052,8 +6056,9 @@ program
|
|
|
6052
6056
|
if (replayCount && !replayFailing.length)
|
|
6053
6057
|
console.log(`· [replay-fingerprint] ${scopePath(own)}: ledger fold ${replay.replay_hash} ≠ stored ${replay.stored_hash}`);
|
|
6054
6058
|
const anchor = findings.filter((f) => f.kind === "anchor-stale" || f.kind === "doc-anchor-stale").length;
|
|
6055
|
-
|
|
6056
|
-
|
|
6059
|
+
const failing = findings.filter((f) => failOn.has(f.kind)).length;
|
|
6060
|
+
console.log(`\n${findings.length + replayCount} finding(s)${anchor ? `, ${anchor} doc≠graph (anchor-stale)` : ""}${collisions.size ? `, ${collisions.size} topic-collision(s)` : ""}${replayCount ? `, ${replayCount} ledger≠records (replay: hunch serve replay --root .)` : ""}${failing ? `, ${failing} failing by --fail-on (${[...failOn].join(", ")})` : ""}.`);
|
|
6061
|
+
if (anchor || collisions.size || replayCount || failing)
|
|
6057
6062
|
process.exitCode = 1;
|
|
6058
6063
|
}
|
|
6059
6064
|
finally {
|
|
@@ -6809,7 +6814,10 @@ function realpathNorm(p) {
|
|
|
6809
6814
|
function toRepoRel(root, abs) {
|
|
6810
6815
|
return relative(realpathNorm(root), realpathNorm(abs)).split("\\").join("/");
|
|
6811
6816
|
}
|
|
6812
|
-
function emitContext(provider, event, text
|
|
6817
|
+
function emitContext(provider, event, text,
|
|
6818
|
+
/** One user-facing line where the host shows hook messages (Claude Code's
|
|
6819
|
+
* `systemMessage`); never a block, never a second model turn. */
|
|
6820
|
+
systemMessage) {
|
|
6813
6821
|
if (event === "SessionStart") {
|
|
6814
6822
|
const warning = integrationSessionWarning(findRoot(), provider);
|
|
6815
6823
|
if (warning)
|
|
@@ -6817,7 +6825,7 @@ function emitContext(provider, event, text) {
|
|
|
6817
6825
|
}
|
|
6818
6826
|
const output = contextHookOutput(provider, event, text);
|
|
6819
6827
|
if (output)
|
|
6820
|
-
process.stdout.write(JSON.stringify(output));
|
|
6828
|
+
process.stdout.write(JSON.stringify(provider === "claude" && systemMessage ? { ...output, systemMessage } : output));
|
|
6821
6829
|
}
|
|
6822
6830
|
function emitDeny(provider, reason) {
|
|
6823
6831
|
const result = denyHookOutput(provider, reason);
|
package/dist/cli/taskReport.js
CHANGED
|
@@ -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,
|
|
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); },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { headSha } from "../extractors/git.js";
|
|
7
7
|
import { canonicalHash } from "./canonical.js";
|
|
@@ -125,7 +125,14 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
|
|
|
125
125
|
}
|
|
126
126
|
const dependency = dependencySnapshotForCommit(root, commit, assertion.dependency_snapshot_ids);
|
|
127
127
|
if (!dependency) {
|
|
128
|
-
|
|
128
|
+
// Two different situations hid behind one message (fnd_b421b3f7ab): a machine
|
|
129
|
+
// that never built the snapshot cache, and a policy whose pinned snapshots no
|
|
130
|
+
// longer match the commit's dependency inputs. Name each with its recovery;
|
|
131
|
+
// both stay `error`, never a coerced pass.
|
|
132
|
+
if (!existsSync(join(root, ".hunch-cache", "behavior-deps"))) {
|
|
133
|
+
return evaluation(policy, commit, { ...baseExecution, commit, error_code: "dependency-snapshot-cache-absent" }, "error", "no dependency snapshot cache exists on this machine (.hunch-cache/behavior-deps); executable behavior is unevaluated here, not failed — provision the policy's snapshots (hunch constitution bootstrap --behavior-deps <candidate>) or evaluate where they were built");
|
|
134
|
+
}
|
|
135
|
+
return evaluation(policy, commit, { ...baseExecution, commit, error_code: "dependency-snapshot-unavailable" }, "error", `no unique exact dependency snapshot matches this commit's package.json/package-lock.json among the policy's pinned ids (${assertion.dependency_snapshot_ids.join(", ")}); dependency inputs changed since compilation — re-plan and re-prove the policy (rb_g2_stale_policy_01)`);
|
|
129
136
|
}
|
|
130
137
|
const session = mkdtempSync(join(tmpdir(), "hunch-behavior-policy-"));
|
|
131
138
|
const hooks = join(session, "hooks-disabled");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Terminal rendering of canonical policy receipts, shared by `hunch policy
|
|
2
|
+
* evaluate` and the pre-commit `hunch check`. Rendering never alters a receipt.
|
|
3
|
+
* Receipts that did not evaluate (error / unknown / not_applicable) and share
|
|
4
|
+
* one explanation are grouped, so ten policies failing for the same
|
|
5
|
+
* environmental reason read as one actionable block instead of ten
|
|
6
|
+
* (fnd_b421b3f7ab); satisfied and violated policies always stay one per line. */
|
|
7
|
+
import type { PolicyEvaluationSet } from "./service.js";
|
|
8
|
+
export declare function renderPolicyEvaluations(results: PolicyEvaluationSet[]): string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const ICON = { satisfied: "✅", violated: "⛔", not_applicable: "·", unknown: "?", error: "‼" };
|
|
2
|
+
const GROUP_AT = 3;
|
|
3
|
+
function groupKey(r) {
|
|
4
|
+
const result = r.evaluation.result;
|
|
5
|
+
const groupable = (result === "error" || result === "unknown" || result === "not_applicable") && !r.blocks && !r.gate_error;
|
|
6
|
+
if (!groupable)
|
|
7
|
+
return `one ${r.policy.id}`;
|
|
8
|
+
return `${r.policy.state} ${result} ${r.evaluation.explanation}`;
|
|
9
|
+
}
|
|
10
|
+
export function renderPolicyEvaluations(results) {
|
|
11
|
+
if (!results.length)
|
|
12
|
+
return ["No Constitution policies matched."];
|
|
13
|
+
const out = [`Constitution policy evaluation: ${results.length} canonical receipt(s)`];
|
|
14
|
+
const groups = new Map();
|
|
15
|
+
for (const r of results) {
|
|
16
|
+
const key = groupKey(r);
|
|
17
|
+
const members = groups.get(key) ?? [];
|
|
18
|
+
members.push(r);
|
|
19
|
+
groups.set(key, members);
|
|
20
|
+
}
|
|
21
|
+
const rendered = new Set();
|
|
22
|
+
for (const r of results) {
|
|
23
|
+
if (rendered.has(r))
|
|
24
|
+
continue;
|
|
25
|
+
const members = groups.get(groupKey(r)) ?? [r];
|
|
26
|
+
const icon = ICON[r.evaluation.result] ?? "·";
|
|
27
|
+
if (members.length >= GROUP_AT) {
|
|
28
|
+
for (const member of members)
|
|
29
|
+
rendered.add(member);
|
|
30
|
+
const ids = members.map((m) => m.policy.id);
|
|
31
|
+
const receipts = members.map((m) => `${m.policy.id}=${m.evaluation.deterministic_hash.slice(0, 17)}`);
|
|
32
|
+
out.push(` ${icon} ${members.length} policies [${r.policy.state}] ${r.evaluation.result} — same cause`);
|
|
33
|
+
out.push(` ${r.evaluation.explanation}`);
|
|
34
|
+
out.push(` policies: ${ids.join(", ")}`);
|
|
35
|
+
out.push(` receipts: ${receipts.join(" ")}`);
|
|
36
|
+
out.push(" full receipts: hunch policy evaluate --json");
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
rendered.add(r);
|
|
40
|
+
out.push(` ${icon} ${r.policy.id} [${r.policy.state}] ${r.evaluation.result}${r.blocks ? " — BLOCK" : ""}`);
|
|
41
|
+
out.push(` ${r.evaluation.explanation}`);
|
|
42
|
+
if (r.gate_error)
|
|
43
|
+
out.push(` gate error: ${r.gate_error}`);
|
|
44
|
+
out.push(` receipt: ${r.evaluation.deterministic_hash}`);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=renderEvaluations.js.map
|
package/dist/core/drift.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HunchStore } from "../store/hunchStore.js";
|
|
2
|
-
export
|
|
2
|
+
export declare const DRIFT_KINDS: readonly ["dead-ref", "supersede", "doc-stale", "anchor-stale", "doc-anchor-stale", "doc-anchor-dangling", "wiki-stale", "finding-stale", "premise-stale", "commit-unresolvable", "madr-stale", "madr-edited", "madr-orphan"];
|
|
3
|
+
export type DriftKind = typeof DRIFT_KINDS[number];
|
|
3
4
|
export interface DriftFinding {
|
|
4
5
|
kind: DriftKind;
|
|
5
6
|
id: string;
|
package/dist/core/drift.js
CHANGED
|
@@ -27,6 +27,7 @@ import { markdownDocs, STALE_MARKER, SRC_REF } from "./docscan.js";
|
|
|
27
27
|
import { computeWikiDrift } from "../wiki/wiki.js";
|
|
28
28
|
import { computeMadrDrift } from "../integrations/madrManifest.js";
|
|
29
29
|
import { commitsExist, isGitRepo } from "../extractors/git.js";
|
|
30
|
+
export const DRIFT_KINDS = ["dead-ref", "supersede", "doc-stale", "anchor-stale", "doc-anchor-stale", "doc-anchor-dangling", "wiki-stale", "finding-stale", "premise-stale", "commit-unresolvable", "madr-stale", "madr-edited", "madr-orphan"];
|
|
30
31
|
export function computeDrift(store, root, deps = {}) {
|
|
31
32
|
const findings = [];
|
|
32
33
|
const decisions = store.recs("decisions");
|
|
@@ -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
|
|
@@ -198,6 +198,12 @@ export declare function readLessonHistory(root: string, reference: LessonReferen
|
|
|
198
198
|
before?: number;
|
|
199
199
|
}): LessonHistory;
|
|
200
200
|
export declare function startReportTask(root: string, title: string, taskId?: string): ReportTask;
|
|
201
|
+
/** The record revisions among `records` that this task has not received before.
|
|
202
|
+
* Powers the one-line "Hunch recalled …" indication on a lesson's FIRST delivery
|
|
203
|
+
* in a task; repeats of the same revision stay silent (deduplicated per task and
|
|
204
|
+
* revision, never per session or file). Read-only; never throws for callers
|
|
205
|
+
* that must stay silent on failure — they catch. */
|
|
206
|
+
export declare function unseenLessons(root: string, taskId: string, records: readonly ReportRecord[]): ReportRecord[];
|
|
201
207
|
/** Strict operation for explicit callers. Passive integrations catch failure
|
|
202
208
|
* and disclose it without blocking context delivery. Empty envelopes count. */
|
|
203
209
|
export declare function recordTaskDelivery(root: string, taskId: string, envelope: DeliveryEnvelope, records: ReportRecord[], occurrenceId?: string): string;
|
package/dist/core/taskReport.js
CHANGED
|
@@ -281,6 +281,21 @@ function appendEvent(root, taskId, kind, body, eventId) {
|
|
|
281
281
|
return id;
|
|
282
282
|
}));
|
|
283
283
|
}
|
|
284
|
+
/** The record revisions among `records` that this task has not received before.
|
|
285
|
+
* Powers the one-line "Hunch recalled …" indication on a lesson's FIRST delivery
|
|
286
|
+
* in a task; repeats of the same revision stay silent (deduplicated per task and
|
|
287
|
+
* revision, never per session or file). Read-only; never throws for callers
|
|
288
|
+
* that must stay silent on failure — they catch. */
|
|
289
|
+
export function unseenLessons(root, taskId, records) {
|
|
290
|
+
if (!records.length)
|
|
291
|
+
return [];
|
|
292
|
+
return taskDb(root, db => {
|
|
293
|
+
readTask(db, root, taskId);
|
|
294
|
+
const seen = db.prepare(`SELECT 1 FROM report_record_links l JOIN report_events e ON e.event_id = l.event_id
|
|
295
|
+
WHERE e.task_id = ? AND e.kind = 'delivery' AND l.kind = ? AND l.record_id = ? AND l.content_hash = ? LIMIT 1`);
|
|
296
|
+
return records.filter(r => !seen.get(taskId, r.kind, r.record_id, r.content_hash));
|
|
297
|
+
});
|
|
298
|
+
}
|
|
284
299
|
/** Strict operation for explicit callers. Passive integrations catch failure
|
|
285
300
|
* and disclose it without blocking context delivery. Empty envelopes count. */
|
|
286
301
|
export function recordTaskDelivery(root, taskId, envelope, records, occurrenceId = `hocc_${randomBytes(12).toString("hex")}`) {
|
|
@@ -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
|
-
|
|
197
|
-
|
|
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")
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { type LessonHistory, type TaskReport } from "./taskReport.js";
|
|
2
2
|
export declare function writeTaskReportHtml(root: string, taskId: string, publicOnly?: boolean): string;
|
|
3
|
+
/** One short line for the first time a lesson reaches a task; null when every
|
|
4
|
+
* delivered revision was already seen in this task. Never a banner per delivery. */
|
|
5
|
+
export declare function renderRecalledLine(fresh: readonly {
|
|
6
|
+
title: string;
|
|
7
|
+
}[]): string | null;
|
|
3
8
|
export declare function renderTaskReport(report: TaskReport): string;
|
|
4
9
|
/** Standalone, local-only projection. No active content, external assets or
|
|
5
10
|
* untrusted outbound URLs; evidence references are internal anchors. */
|
|
@@ -36,6 +36,14 @@ function ruleStanding(report) {
|
|
|
36
36
|
function recordTitle(report, rule) {
|
|
37
37
|
return uniqueRecords(report).find(r => r.kind === rule.kind && r.record_id === rule.record_id && r.content_hash === rule.content_hash)?.title ?? rule.record_id;
|
|
38
38
|
}
|
|
39
|
+
/** One short line for the first time a lesson reaches a task; null when every
|
|
40
|
+
* delivered revision was already seen in this task. Never a banner per delivery. */
|
|
41
|
+
export function renderRecalledLine(fresh) {
|
|
42
|
+
if (!fresh.length)
|
|
43
|
+
return null;
|
|
44
|
+
const rest = fresh.length - 1;
|
|
45
|
+
return `Hunch recalled: ${clip(fresh[0].title, 90)}${rest ? ` (+${rest} more lesson${rest === 1 ? "" : "s"})` : ""}`;
|
|
46
|
+
}
|
|
39
47
|
export function renderTaskReport(report) {
|
|
40
48
|
const records = uniqueRecords(report);
|
|
41
49
|
const lines = [`Hunch · ${clip(report.task.title)}`, `Task ${report.task.task_id} · ${report.task.state}`];
|
|
@@ -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
|
-
|
|
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
|
-
"
|
|
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. */
|
package/dist/mcp/server.js
CHANGED
|
@@ -40,7 +40,8 @@ import { PROJECT_DNA_DELTA_SCHEMA_VERSION, diffProjectDna } from "../core/projec
|
|
|
40
40
|
import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
|
|
41
41
|
import { armExecutionObligations, loadPipelineState, savePipelineState } from "../core/pipeline.js";
|
|
42
42
|
import { recordServed } from "../core/served.js";
|
|
43
|
-
import { TaskIdSchema, recordTaskDelivery } from "../core/taskReport.js";
|
|
43
|
+
import { TaskIdSchema, recordTaskDelivery, unseenLessons } from "../core/taskReport.js";
|
|
44
|
+
import { renderRecalledLine } from "../core/taskReportRender.js";
|
|
44
45
|
import { observeReportCapture } from "../core/taskReportCapture.js";
|
|
45
46
|
import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
|
|
46
47
|
import { registerTaskReportTools } from "./taskReportTools.js";
|
|
@@ -1049,8 +1050,10 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1049
1050
|
try {
|
|
1050
1051
|
// Historical contexts must not borrow today's record text/revision.
|
|
1051
1052
|
const records = as_of ? [] : snapshotDeliveredRecords(store, envelope);
|
|
1053
|
+
// First delivery of a revision in this task earns one line; repeats stay quiet.
|
|
1054
|
+
const recalled = renderRecalledLine(unseenLessons(root, task_id, records));
|
|
1052
1055
|
const occurrence = recordTaskDelivery(root, task_id, envelope, records);
|
|
1053
|
-
result.content.push({ type: "text", text: `Task evidence: ${task_id} · occurrence ${occurrence}.\n${records.slice(0, 20).map(r => `${r.record_id} @ ${r.content_hash}`).join("\n")}${records.length > 20 ? "\nMore record identities: hunch_report(task_id)." : ""}` });
|
|
1056
|
+
result.content.push({ type: "text", text: `${recalled ? `${recalled}\n` : ""}Task evidence: ${task_id} · occurrence ${occurrence}.\n${records.slice(0, 20).map(r => `${r.record_id} @ ${r.content_hash}`).join("\n")}${records.length > 20 ? "\nMore record identities: hunch_report(task_id)." : ""}` });
|
|
1054
1057
|
}
|
|
1055
1058
|
catch {
|
|
1056
1059
|
result.content.push({ type: "text", text: `Task evidence could not be recorded for ${task_id}. Context remains available; this delivery's report attribution is unverified. Check the task ID, working directory, and local ledger.` });
|
|
@@ -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}` }] };
|
package/dist/taskReports.d.ts
CHANGED
|
@@ -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]
|
|
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;
|
package/dist/taskReports.js
CHANGED
|
@@ -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
|
-
|
|
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
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.2",
|
|
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.2",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|