@davesheffer/hunch 1.18.1 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,83 +23,7 @@ import { homedir } from "node:os";
23
23
  import { join, dirname } from "node:path";
24
24
  import { renderHunchSection, stripManagedSection, upsertSection, updateClaudeMd } from "./claudemd.js";
25
25
  import { headFileContent, isGitCleanPath } from "../extractors/git.js";
26
- /** Strip // line and block comments + trailing commas (JSONC → JSON). String-aware
27
- * (double-quoted, with escapes) so a // inside a value isn't mangled. VS Code's
28
- * .vscode/mcp.json is JSONC, so we must tolerate comments. */
29
- function stripJsonc(s) {
30
- let out = "";
31
- let inStr = false;
32
- for (let i = 0; i < s.length; i++) {
33
- const c = s[i];
34
- const n = s[i + 1];
35
- if (inStr) {
36
- out += c;
37
- if (c === "\\") {
38
- out += n ?? "";
39
- i++;
40
- continue;
41
- }
42
- if (c === '"')
43
- inStr = false;
44
- continue;
45
- }
46
- if (c === '"') {
47
- inStr = true;
48
- out += c;
49
- continue;
50
- }
51
- if (c === "/" && n === "/") {
52
- while (i < s.length && s[i] !== "\n")
53
- i++;
54
- continue;
55
- }
56
- if (c === "/" && n === "*") {
57
- i += 2;
58
- while (i < s.length && !(s[i] === "*" && s[i + 1] === "/"))
59
- i++;
60
- i++;
61
- continue;
62
- }
63
- out += c;
64
- }
65
- return dropTrailingCommas(out);
66
- }
67
- /** Remove trailing commas (`,` before `}`/`]`) — string-aware, so a comma inside
68
- * a string value (e.g. "a,]") is never touched. A blanket regex would corrupt it
69
- * (the same trap test/migrate.test.ts guards against). Runs on comment-free text,
70
- * so lookahead need only skip whitespace. */
71
- function dropTrailingCommas(s) {
72
- let out = "";
73
- let inStr = false;
74
- let esc = false;
75
- for (let i = 0; i < s.length; i++) {
76
- const c = s[i];
77
- if (inStr) {
78
- out += c;
79
- if (esc)
80
- esc = false;
81
- else if (c === "\\")
82
- esc = true;
83
- else if (c === '"')
84
- inStr = false;
85
- continue;
86
- }
87
- if (c === '"') {
88
- inStr = true;
89
- out += c;
90
- continue;
91
- }
92
- if (c === ",") {
93
- let j = i + 1;
94
- while (j < s.length && /\s/.test(s[j]))
95
- j++;
96
- if (s[j] === "}" || s[j] === "]")
97
- continue; // trailing comma → drop
98
- }
99
- out += c;
100
- }
101
- return out;
102
- }
26
+ import { parseJsonc } from "../core/jsonc.js";
103
27
  /** Read a JSON/JSONC object. Returns {} only for an ABSENT or empty file. A
104
28
  * non-empty file we cannot parse THROWS — overwriting it would silently wipe the
105
29
  * user's other MCP servers. */
@@ -110,7 +34,7 @@ function readJsonObj(file) {
110
34
  if (!raw.trim())
111
35
  return {};
112
36
  try {
113
- const v = JSON.parse(stripJsonc(raw));
37
+ const v = parseJsonc(raw);
114
38
  if (v && typeof v === "object" && !Array.isArray(v))
115
39
  return v;
116
40
  throw new Error("not a JSON object");
@@ -175,6 +175,12 @@ export function installClaudeHooks(root, hookCmd) {
175
175
  ...keep(json.hooks.PostToolUse),
176
176
  { matcher: "Edit|Write|MultiEdit|Bash|PowerShell|Skill", hooks: [{ type: "command", command: hookCmd }] },
177
177
  ];
178
+ // Modern Claude Code separates failed tools from PostToolUse. Observe that
179
+ // event too so a failed test cannot be mistaken for a completed proof.
180
+ json.hooks.PostToolUseFailure = [
181
+ ...keep(json.hooks.PostToolUseFailure),
182
+ { matcher: "Bash|PowerShell", hooks: [{ type: "command", command: hookCmd }] },
183
+ ];
178
184
  json.hooks.Stop = [
179
185
  ...keep(json.hooks.Stop),
180
186
  { hooks: [{ type: "command", command: hookCmd }] },
@@ -22,7 +22,11 @@ import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, c
22
22
  import { flushCapture, flushMemoryHome, pinSharedRemote } from "../integrations/sync.js";
23
23
  import { advertisedTeamRemoteContract, ensureTeamOverlay, overlayMatchesTeamRemote, readTeamConfig, teamRemoteContract, teamSharedRef } from "../integrations/team.js";
24
24
  import { formatStructure } from "../core/format.js";
25
+ import { diagnoseIssueCorrectionStage, formatCorrectionStageDiagnostic } from "../core/correctionStage.js";
26
+ import { compileVerifiedEvidenceMap, EvidenceExecutionSchema, EvidenceInterventionSchema, EvidenceProbeSchema, formatVerifiedEvidenceMap, VerifiedEvidenceReceiptSchema, } from "../core/evidenceMap.js";
27
+ import { collectCorrectionStageSources } from "../extractors/correctionSources.js";
25
28
  import { buildDeliveryEnvelope } from "../core/delivery.js";
29
+ import { armExecutionObligations, loadPipelineState, savePipelineState } from "../core/pipeline.js";
26
30
  import { recordServed } from "../core/served.js";
27
31
  import { compareCandidates } from "../core/compare.js";
28
32
  import { checkConformance } from "../core/conformance.js";
@@ -127,6 +131,19 @@ const FINDINGS_CAP = 12; // hunch_findings listing
127
131
  const SEV_CONSTRAINT = { blocking: 3, warning: 2, advisory: 1 };
128
132
  const SEV_BUG = { critical: 4, high: 3, medium: 2, low: 1 };
129
133
  const more = (total, cap, hint = "") => total > cap ? `\n …(+${total - cap} more${hint ? ` — ${hint}` : ""})` : "";
134
+ const EXECUTION_OBLIGATION_SCHEMA = z.object({
135
+ id: z.string(),
136
+ origin: z.enum(["memory", "episode", "manual"]),
137
+ category: z.enum(["evidence", "behavior", "types", "serialization", "compatibility", "other"]),
138
+ phase: z.enum(["session", "after-edit"]),
139
+ description: z.string(),
140
+ command_alternatives: z.array(z.array(z.string())),
141
+ expected: z.object({
142
+ success: z.boolean(),
143
+ output_includes: z.array(z.string()).optional(),
144
+ output_excludes: z.array(z.string()).optional(),
145
+ }),
146
+ });
130
147
  /** Public MCP shape for the canonical delivery envelope. Keeping the schema on
131
148
  * the tool means orchestrators can consume receipt facts without scraping the
132
149
  * backward-compatible text block. */
@@ -140,23 +157,45 @@ const DELIVERY_OUTPUT_SCHEMA = z.object({
140
157
  provenance_status: z.enum(["current", "unverified", "stale"]),
141
158
  token_cost: z.number().int().nonnegative(),
142
159
  })),
160
+ hypotheses: z.array(z.object({
161
+ kind: z.literal("decision"),
162
+ record_id: z.string(),
163
+ rank: z.number().int().positive(),
164
+ why: z.string(),
165
+ where: z.array(z.string()),
166
+ historical_pattern: z.string(),
167
+ verify: z.string(),
168
+ disprove: z.string(),
169
+ obligations: z.array(EXECUTION_OBLIGATION_SCHEMA),
170
+ })),
171
+ obligations: z.array(EXECUTION_OBLIGATION_SCHEMA),
143
172
  supplements: z.array(z.object({
144
173
  id: z.string(),
145
174
  kind: z.string(),
146
175
  delivered: z.boolean(),
147
- reason: z.enum(["supplemental", "budget", "empty"]),
176
+ reason: z.enum(["supplemental", "budget", "empty", "abstained"]),
148
177
  rank: z.number().int().positive(),
149
178
  token_cost: z.number().int().nonnegative(),
150
179
  })),
151
180
  omitted: z.array(z.object({
152
181
  kind: z.enum(["constraints", "decisions", "bugs", "findings"]),
153
182
  record_id: z.string(),
154
- reason: z.enum(["budget", "stale-provenance", "retired"]),
183
+ reason: z.enum(["budget", "stale-provenance", "retired", "actionability-cap", "low-confidence", "insufficient-context", "low-relevance"]),
155
184
  detail: z.string(),
156
185
  })),
157
186
  budget_tokens: z.number().int().nonnegative(),
158
187
  used_chars: z.number().int().nonnegative(),
159
188
  blocking_overflow: z.boolean(),
189
+ abstention: z.object({
190
+ active: z.boolean(),
191
+ withheld: z.number().int().nonnegative(),
192
+ reasons: z.object({
193
+ "low-confidence": z.number().int().nonnegative(),
194
+ "insufficient-context": z.number().int().nonnegative(),
195
+ "low-relevance": z.number().int().nonnegative(),
196
+ }),
197
+ retry_hint: z.string().nullable(),
198
+ }),
160
199
  });
161
200
  /** Return the same human-readable brief older clients consume plus the exact
162
201
  * machine-readable envelope. Receipt recording is deliberately best-effort:
@@ -166,6 +205,10 @@ function deliveredContext(root, target, envelope, sessionId) {
166
205
  // advertised MCP contract, the SDK will reject the call and the local ledger
167
206
  // must not claim that response was served.
168
207
  const structuredContent = DELIVERY_OUTPUT_SCHEMA.parse(envelope);
208
+ if (sessionId) {
209
+ const state = armExecutionObligations(loadPipelineState(sessionId), structuredContent.obligations, { replaceOrigin: "memory" });
210
+ savePipelineState(sessionId, state);
211
+ }
169
212
  recordServed(root, structuredContent.delivered.map((item) => ({
170
213
  event: "served",
171
214
  kind: item.kind,
@@ -761,6 +804,45 @@ export function buildServerWithRootControl(initialRoot) {
761
804
  const envelope = buildDeliveryEnvelope(ctx, options);
762
805
  return deliveredContext(root, as_of ? `${target} (as_of:${as_of})` : target, envelope, extra.sessionId);
763
806
  });
807
+ // -- hunch_shortlist (bounded correction-stage diagnostic) ----------------
808
+ server.registerTool("hunch_shortlist", {
809
+ title: "Shortlist the likely correction stage and declarations",
810
+ description: "Experimental, deterministic, read-only repository-adaptive diagnostic for a schema/validation issue or reproduction. Preserves a flat top five, adds a transfer-tested hierarchical inspection view, and emits an efficiency-tested advisory progressive inspection queue capped at eleven declarations with deterministic receipts. Optional authenticated same-claim evidence is annotated but cannot reorder candidates because fresh transfer rejected that mechanism. It never claims an exact implementation owner or per-case confidence and does not edit, gate, or capture memory.",
811
+ inputSchema: {
812
+ issue: z.string().min(1).max(100_000).describe("Issue report or reproduction prose, including observed and expected behavior when available."),
813
+ limit: z.number().int().min(1).max(5).optional().describe("Candidate count, capped at five (default 5)."),
814
+ evidence: VerifiedEvidenceReceiptSchema.optional().describe("Optional verified evidence for this exact issue claim. It is reported and attached to candidates but cannot change ranking."),
815
+ cwd: cwdHintField,
816
+ },
817
+ }, async ({ issue, limit, evidence }) => {
818
+ try {
819
+ const collection = collectCorrectionStageSources(root, issue);
820
+ const diagnostic = diagnoseIssueCorrectionStage(issue, collection.sources, limit ?? 5, evidence);
821
+ return ok(`${formatCorrectionStageDiagnostic(diagnostic)}\nScan: ${collection.files_read} source file(s), ${collection.files_skipped} skipped by safety/budget limits.`);
822
+ }
823
+ catch (error) {
824
+ return err(`Could not build the correction-stage shortlist: ${error.message}`);
825
+ }
826
+ });
827
+ // -- hunch_evidence_map (verified behavioral receipt compiler) ------------
828
+ server.registerTool("hunch_evidence_map", {
829
+ title: "Compile a verified behavioral evidence map",
830
+ description: "Compile supplied red-target/green-control, execution, and intervention observations into a bounded read-only evidence map. Useful for bugs, regressions, design invariants, and any other testable behavior. This tool executes no code, mutates nothing, and never converts behavioral influence into an exact correction-owner claim.",
831
+ inputSchema: {
832
+ version: z.literal(1).describe("Receipt schema version; currently 1."),
833
+ claim: z.string().trim().min(1).max(100_000).describe("The behavior or invariant being tested."),
834
+ probe: EvidenceProbeSchema,
835
+ execution: z.array(EvidenceExecutionSchema).max(500).optional(),
836
+ interventions: z.array(EvidenceInterventionSchema).max(500).optional(),
837
+ },
838
+ }, async (receipt) => {
839
+ try {
840
+ return ok(formatVerifiedEvidenceMap(compileVerifiedEvidenceMap(receipt)));
841
+ }
842
+ catch (error) {
843
+ return err(`Could not compile the verified evidence map: ${error.message}`);
844
+ }
845
+ });
764
846
  // -- hunch_now (the hot view: recent activity + roadmap) --------------------
765
847
  // PUBLIC store only, per dec_29eff08c69's jurisdiction rule: an assistant may
766
848
  // paste this anywhere, so it must be publishable by construction. Union view
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
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://hunch-pi.vercel.app",
10
- "version": "1.18.1",
10
+ "version": "1.19.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.18.1",
16
+ "version": "1.19.0",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {