@davesheffer/hunch 1.41.6 → 1.42.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.
- package/README.md +1 -1
- package/dist/cli/index.js +402 -154
- package/dist/constitution/experiment.d.ts +3 -3
- package/dist/constitution/g3.d.ts +1 -1
- package/dist/core/footprint.d.ts +15 -0
- package/dist/core/footprint.js +167 -0
- package/dist/core/groundingLag.d.ts +15 -0
- package/dist/core/groundingLag.js +27 -0
- package/dist/core/hookText.d.ts +4 -0
- package/dist/core/hookText.js +8 -0
- package/dist/core/pipeline.d.ts +28 -0
- package/dist/core/pipeline.js +50 -0
- package/dist/core/shellwrites.d.ts +7 -0
- package/dist/core/shellwrites.js +131 -0
- package/dist/core/siblingfix.d.ts +124 -0
- package/dist/core/siblingfix.js +814 -0
- package/dist/core/taskReportHook.d.ts +1 -1
- package/dist/core/taskReportHook.js +19 -3
- package/dist/extractors/git.d.ts +31 -0
- package/dist/extractors/git.js +180 -1
- package/dist/extractors/nativeTreeSitter.d.ts +2 -1
- package/dist/extractors/nativeTreeSitter.js +128 -30
- package/dist/integrations/claudemd.d.ts +20 -2
- package/dist/integrations/claudemd.js +79 -53
- package/dist/integrations/providers.d.ts +9 -5
- package/dist/integrations/providers.js +34 -22
- package/dist/integrations/team.d.ts +24 -4
- package/dist/integrations/team.js +154 -16
- package/dist/integrations/worktree.d.ts +3 -2
- package/dist/integrations/worktree.js +7 -4
- package/dist/mcp/server.d.ts +489 -0
- package/dist/mcp/server.js +208 -62
- package/dist/mcp/taskReportTools.js +12 -9
- package/dist/mcp/toolset.d.ts +11 -1
- package/dist/mcp/toolset.js +28 -8
- package/dist/store/hunchStore.d.ts +25 -1
- package/dist/store/hunchStore.js +146 -21
- package/dist/store/jsonStore.js +25 -4
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/mcp/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { conventionSupplements } from '../core/conventionDelivery.js';
|
|
2
2
|
import { fieldCitationText } from "../core/fieldProvenance.js";
|
|
3
|
+
import { siblingGrounding } from "../core/siblingfix.js";
|
|
3
4
|
/**
|
|
4
5
|
* MCP server — the structured two-way API into the Hunch (DESIGN.md §7 / App. A).
|
|
5
6
|
* Exposes read tools (query/why/bug_lineage/check_constraints/get_dependents) and
|
|
@@ -15,6 +16,7 @@ import { z } from "zod";
|
|
|
15
16
|
import { hunchPaths, findRoot, toPosixTarget, repoRelativeTarget } from "../core/paths.js";
|
|
16
17
|
import { matchSymbolsTiered } from "../core/glob.js";
|
|
17
18
|
import { resolveMcpToolset } from "./toolset.js";
|
|
19
|
+
import { PolicyRepository } from "../constitution/repository.js";
|
|
18
20
|
import { readConfig } from "../core/config.js";
|
|
19
21
|
import { canonicalRootPath, resolveActiveRoot } from "./roots.js";
|
|
20
22
|
import { HunchStore } from "../store/hunchStore.js";
|
|
@@ -33,7 +35,7 @@ import { workspacesConfig } from "../core/config.js";
|
|
|
33
35
|
import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeGateDiff, commitFiles, commitGateDiff, stagedFiles, stagedGateDiff, workingFiles, workingGateDiff, pullHunchStatus, sameRemoteUrl, currentBranch, worktreePaths, pathKnownToHistory } from "../extractors/git.js";
|
|
34
36
|
import { flushCapture, flushMemoryHome, pinSharedRemote } from "../integrations/sync.js";
|
|
35
37
|
import { withWriteLock } from "../serve/writelock.js";
|
|
36
|
-
import { advertisedTeamRemoteContract, ensureTeamOverlay, overlayMatchesTeamRemote, readTeamConfig, teamRemoteContract, teamSharedRef } from "../integrations/team.js";
|
|
38
|
+
import { advertisedTeamRemoteContract, ensureTeamOverlay, isTeamStoreTrusted, overlayMatchesTeamRemote, teamWiringConsented, untrustedTeamStoreMessage, readTeamConfig, teamRemoteContract, teamSharedRef } from "../integrations/team.js";
|
|
37
39
|
import { formatSearchHit, formatStructure } from "../core/format.js";
|
|
38
40
|
import { isStateKind, stateSupplements } from "../core/stateDelivery.js";
|
|
39
41
|
import { taskSelectionSupplements } from "../core/taskDelivery.js";
|
|
@@ -90,10 +92,12 @@ const invalid = (text) => err(`Invalid: ${text}`);
|
|
|
90
92
|
* cannot see an agent-driven `cd`/EnterWorktree, so a stdio server's cached root
|
|
91
93
|
* never moves on its own — this is the client-agnostic fallback, resolved fresh on
|
|
92
94
|
* every call by the generic tool wrapper below (see extractCwdHint). */
|
|
93
|
-
const cwdHintField = z.string().optional().describe("Your
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const cwdHintField = z.string().optional().describe("Your current directory, when it differs from where this session started (e.g. a worktree); " +
|
|
96
|
+
"writes then commit there, not to the original root.");
|
|
97
|
+
/** Shared by hunch_record_decision/correction/finding — identical text repeated 3x
|
|
98
|
+
* otherwise. hunch_context's task_id field differs (delivery vs. save) and keeps its
|
|
99
|
+
* own description. */
|
|
100
|
+
const saveTaskIdField = TaskIdSchema.optional().describe("Task ID, to attribute this save to it; never affects capture authority.");
|
|
97
101
|
/** Pull `cwd` out of a tool call's already-parsed input without assuming any one
|
|
98
102
|
* tool's exact input shape — every write tool spreads the same cwdHintField in,
|
|
99
103
|
* but the wrapper below runs for every tool, read or write. */
|
|
@@ -594,6 +598,29 @@ const WHY_CAP = 6; // per record-type in hunch_why
|
|
|
594
598
|
const DEP_CAP = 25; // dependents in hunch_get_dependents
|
|
595
599
|
const QUERY_HITS = 8; // hunch_query matches (was 12)
|
|
596
600
|
const FINDINGS_CAP = 12; // hunch_findings listing
|
|
601
|
+
const FINDINGS_MAX = 50; // hunch_findings limit ceiling
|
|
602
|
+
const FINDING_GIST_CHARS = 200; // list view: first sentence, clipped
|
|
603
|
+
const FINDING_GIST_MIN = 60; // keep reading past a dateline like "Observed 2026-09-14."
|
|
604
|
+
const NOT_A_SENTENCE_END = /(?:\b(?:e\.g|i\.e|vs|etc|cf|approx|incl)|^\d+|\s\d+)\.$/i;
|
|
605
|
+
/** The list view's gist of an observation: whole sentences until the gist says
|
|
606
|
+
* something (≥ FINDING_GIST_MIN chars), clipped to FINDING_GIST_CHARS. A
|
|
607
|
+
* period after an abbreviation or a list number is not a sentence end. */
|
|
608
|
+
export const firstSentence = (text) => {
|
|
609
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
610
|
+
const ends = /[.!?](?=\s|$)/g;
|
|
611
|
+
let cut = flat.length;
|
|
612
|
+
for (let m = ends.exec(flat); m; m = ends.exec(flat)) {
|
|
613
|
+
const end = m.index + 1;
|
|
614
|
+
if (NOT_A_SENTENCE_END.test(flat.slice(0, end)))
|
|
615
|
+
continue;
|
|
616
|
+
if (end >= FINDING_GIST_MIN) {
|
|
617
|
+
cut = end;
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const gist = flat.slice(0, cut);
|
|
622
|
+
return gist.length > FINDING_GIST_CHARS ? `${gist.slice(0, FINDING_GIST_CHARS - 1)}…` : gist;
|
|
623
|
+
};
|
|
597
624
|
const SEV_CONSTRAINT = { blocking: 3, warning: 2, advisory: 1 };
|
|
598
625
|
const SEV_BUG = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
599
626
|
const more = (total, cap, hint = "") => total > cap ? `\n …(+${total - cap} more${hint ? ` — ${hint}` : ""})` : "";
|
|
@@ -683,6 +710,12 @@ const DELIVERY_OUTPUT_SCHEMA = z.object({
|
|
|
683
710
|
reason: z.enum(["budget", "stale-provenance", "retired", "actionability-cap", "endpoint-not-delivered", "landscape-cap", "profile-cap", "low-confidence", "insufficient-context", "low-relevance"]),
|
|
684
711
|
detail: z.string(),
|
|
685
712
|
})),
|
|
713
|
+
// The per-record list above is a SAMPLE on MCP (see compactOmissions); these
|
|
714
|
+
// two carry the whole picture in constant size. Optional: the CLI envelope
|
|
715
|
+
// keeps its full list and omits them.
|
|
716
|
+
omitted_total: z.number().int().nonnegative().optional(),
|
|
717
|
+
omitted_truncated: z.boolean().optional(),
|
|
718
|
+
omitted_by_reason: z.record(z.string(), z.number().int().positive()).optional(),
|
|
686
719
|
landscape: LANDSCAPE_FRAGMENT_SCHEMA.nullable(),
|
|
687
720
|
budget_tokens: z.number().int().nonnegative(),
|
|
688
721
|
used_chars: z.number().int().nonnegative(),
|
|
@@ -699,6 +732,28 @@ const DELIVERY_OUTPUT_SCHEMA = z.object({
|
|
|
699
732
|
retry_hint: z.string().nullable(),
|
|
700
733
|
}),
|
|
701
734
|
});
|
|
735
|
+
/** What tools/list advertises for hunch_context (#368). The landscape fragment embeds
|
|
736
|
+
* the full Resource and Edge record schemas (~7k of the ~13k chars), which every
|
|
737
|
+
* schema-loading host would pay on every session; it is advertised by its identity
|
|
738
|
+
* fields only. The handler still parses the result with the full
|
|
739
|
+
* DELIVERY_OUTPUT_SCHEMA, so the delivered shape is exactly as strict as before. */
|
|
740
|
+
const DELIVERY_ADVERTISED_OUTPUT_SCHEMA = DELIVERY_OUTPUT_SCHEMA.extend({
|
|
741
|
+
// The MCP result carries drill-down ids only (see compactEnvelope, #371): the
|
|
742
|
+
// per-record receipt facts stay in the served ledger and hunch_report. Optional,
|
|
743
|
+
// so a reader of the full CLI envelope and of the MCP result shares one shape.
|
|
744
|
+
delivered: z.array(DELIVERY_OUTPUT_SCHEMA.shape.delivered.element.partial({
|
|
745
|
+
rank: true, delivery_reason: true, provenance_status: true, token_cost: true,
|
|
746
|
+
})),
|
|
747
|
+
supplements: z.array(DELIVERY_OUTPUT_SCHEMA.shape.supplements.element.partial({
|
|
748
|
+
reason: true, rank: true, token_cost: true,
|
|
749
|
+
})),
|
|
750
|
+
omitted: z.array(DELIVERY_OUTPUT_SCHEMA.shape.omitted.element.partial({ detail: true })),
|
|
751
|
+
landscape: z.object({
|
|
752
|
+
schema: z.literal("hunch.landscape-fragment/1"),
|
|
753
|
+
target: z.string(),
|
|
754
|
+
fragmentHash: z.string(),
|
|
755
|
+
}).passthrough().nullable(),
|
|
756
|
+
});
|
|
702
757
|
const CHANGE_IDENTITY_OUTPUT_SCHEMA = z.object({
|
|
703
758
|
schema: z.literal(CHANGE_IDENTITY_SCHEMA_VERSION),
|
|
704
759
|
algorithm: z.literal(CHANGE_IDENTITY_ALGORITHM),
|
|
@@ -815,7 +870,53 @@ function deliveredContext(root, target, envelope, sessionId) {
|
|
|
815
870
|
]);
|
|
816
871
|
return {
|
|
817
872
|
content: [{ type: "text", text: structuredContent.text }],
|
|
818
|
-
structuredContent,
|
|
873
|
+
structuredContent: compactEnvelope(structuredContent),
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
/** What the MCP result names per record: enough to drill down (hunch_why), no
|
|
877
|
+
* more. A host shows the model either `content` or `structuredContent`, and
|
|
878
|
+
* `text` is the only brief a structured-only host sees, so every other field
|
|
879
|
+
* repeats what the brief already says; rank, costs and provenance stay in the
|
|
880
|
+
* receipt. Undelivered supplements are dropped (the brief never showed them). */
|
|
881
|
+
export function compactEnvelope(envelope) {
|
|
882
|
+
return {
|
|
883
|
+
...compactOmissions(envelope),
|
|
884
|
+
delivered: envelope.delivered.map(({ kind, record_id }) => ({ kind, record_id })),
|
|
885
|
+
supplements: envelope.supplements.filter((s) => s.delivered).map(({ id, kind, delivered }) => ({ id, kind, delivered })),
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/** Omitted records the MCP result names individually; the rest are counted. */
|
|
889
|
+
const OMITTED_SAMPLE = 5;
|
|
890
|
+
/** The budget must govern the whole tool result, not only the brief: a host
|
|
891
|
+
* that shows the model structuredContent otherwise pays for one repeated
|
|
892
|
+
* sentence per withheld record (104 of them measured on this repo, issue #371).
|
|
893
|
+
* Keep a few ids — round-robin across reasons so every reason the brief cites
|
|
894
|
+
* keeps an id to pass to hunch_why, budget first — and count all of them by
|
|
895
|
+
* reason. Runs after recordServed and after the full envelope validated, so
|
|
896
|
+
* receipts and receipt_id are unchanged. */
|
|
897
|
+
export function compactOmissions(envelope) {
|
|
898
|
+
const groups = new Map();
|
|
899
|
+
for (const item of envelope.omitted)
|
|
900
|
+
groups.set(item.reason, [...(groups.get(item.reason) ?? []), item]);
|
|
901
|
+
const byReason = {};
|
|
902
|
+
for (const [reason, items] of groups)
|
|
903
|
+
byReason[reason] = items.length;
|
|
904
|
+
const order = [...groups.keys()].sort((left, right) => Number(right === "budget") - Number(left === "budget") || left.localeCompare(right));
|
|
905
|
+
const sample = [];
|
|
906
|
+
for (let depth = 0; sample.length < OMITTED_SAMPLE && sample.length < envelope.omitted.length; depth++) {
|
|
907
|
+
for (const reason of order) {
|
|
908
|
+
const item = groups.get(reason)[depth];
|
|
909
|
+
// The per-record detail sentence is left out: hunch_why(record_id) explains it.
|
|
910
|
+
if (item && sample.length < OMITTED_SAMPLE)
|
|
911
|
+
sample.push({ kind: item.kind, record_id: item.record_id, reason: item.reason });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
...envelope,
|
|
916
|
+
omitted: sample,
|
|
917
|
+
omitted_total: envelope.omitted.length,
|
|
918
|
+
omitted_truncated: sample.length < envelope.omitted.length,
|
|
919
|
+
omitted_by_reason: byReason,
|
|
819
920
|
};
|
|
820
921
|
}
|
|
821
922
|
// Capture-session tokens live in src/core/capturetoken.ts (pure + testable). These
|
|
@@ -957,10 +1058,14 @@ function prepareRoot(root, explicitOverlay, requireIndex) {
|
|
|
957
1058
|
const overlayWarning = store.overlayResolutionWarning(explicitOverlay && existsSync(teamFile));
|
|
958
1059
|
if (overlayWarning)
|
|
959
1060
|
console.error(`[hunch-mcp] ⚠ ${overlayWarning}`);
|
|
960
|
-
if (
|
|
1061
|
+
if (startupTeamConfig && (store.mode !== "shared"
|
|
961
1062
|
|| !store.privateDir
|
|
962
1063
|
|| !existsSync(store.privateDir)
|
|
1064
|
+
|| !teamWiringConsented(root, startupTeamConfig, store.privateDir)
|
|
963
1065
|
|| !overlayMatchesTeamRemote(root, join(store.privateDir, "..")))) {
|
|
1066
|
+
const consented = !!store.privateDir && teamWiringConsented(root, startupTeamConfig, store.privateDir);
|
|
1067
|
+
if (!consented && !isTeamStoreTrusted(root, startupTeamConfig))
|
|
1068
|
+
throw new Error(untrustedTeamStoreMessage(startupTeamConfig));
|
|
964
1069
|
throw new Error("the advertised team memory store is unavailable or tracks a different remote; refusing to start MCP on another graph");
|
|
965
1070
|
}
|
|
966
1071
|
const startupTeamRoute = teamAdvertised && store.privateDir
|
|
@@ -1082,7 +1187,13 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1082
1187
|
// Everyday tools by default; specialist groups by evidence, config, or env
|
|
1083
1188
|
// (src/mcp/toolset.ts). Hidden tools are never registered, so tools/list is
|
|
1084
1189
|
// exactly what the host can call.
|
|
1085
|
-
|
|
1190
|
+
let hasPolicies = false;
|
|
1191
|
+
try {
|
|
1192
|
+
hasPolicies = new PolicyRepository(root, store).listPolicies().length > 0;
|
|
1193
|
+
}
|
|
1194
|
+
catch { /* unreadable policies: keep the group hidden */ }
|
|
1195
|
+
const toolset = resolveMcpToolset(root, { configSpec: readConfig(hunchPaths(root)).mcp_tools ?? null, pinned, hasPolicies });
|
|
1196
|
+
const hiddenTools = new Set(toolset.hidden);
|
|
1086
1197
|
if (toolset.hidden.length)
|
|
1087
1198
|
process.stderr.write(`[hunch-mcp] tool groups: ${toolset.groups.length ? toolset.groups.join(", ") : "core only"} (${toolset.source}); ${toolset.hidden.length} specialist tool(s) hidden — HUNCH_MCP_TOOLS=all or .hunch/config.json mcp_tools to expose\n`);
|
|
1088
1199
|
const server = new McpServer({ name: "hunch", version: HUNCH_VERSION }, { instructions: MCP_INSTRUCTIONS });
|
|
@@ -1167,7 +1278,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1167
1278
|
priorOnClose?.();
|
|
1168
1279
|
};
|
|
1169
1280
|
const registerTool = server.registerTool.bind(server);
|
|
1170
|
-
server.registerTool = ((name, config, callback) => registerTool(name, config, async (...args) => {
|
|
1281
|
+
server.registerTool = ((name, config, callback) => hiddenTools.has(name) ? undefined : registerTool(name, config, async (...args) => {
|
|
1171
1282
|
// Claude Code CLI never advertises `roots`/`roots/list_changed` for an agent-driven
|
|
1172
1283
|
// `cd` or EnterWorktree (issue #20) — the cached `root` above just never moves, so a
|
|
1173
1284
|
// write silently lands wherever the process was spawned. Write tools accept an
|
|
@@ -1529,19 +1640,27 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1529
1640
|
// -- hunch_context (surgical retrieval) -----------------------------------
|
|
1530
1641
|
server.registerTool("hunch_context", {
|
|
1531
1642
|
title: "Assemble the minimal relevant Hunch slice for a task",
|
|
1532
|
-
description: "Given a file, symbol, or task phrase you're about to work on, return the MINIMAL relevant memory — invariants
|
|
1643
|
+
description: "Given a file, symbol, or task phrase you're about to work on, return the MINIMAL relevant memory — invariants, decisions, bug history, blast radius — as a compact brief. Call FIRST when starting work. A task phrase with no file/symbol match falls back to the closest graph matches. Returns a budgeted brief plus a delivery receipt. Not for exhaustive rationale on one file (hunch_why) or keyword search (hunch_query).",
|
|
1533
1644
|
inputSchema: {
|
|
1534
1645
|
target: z.string().describe("A file path, symbol, or task phrase you're about to work on."),
|
|
1535
1646
|
budget_tokens: z.number().optional().describe("Rough token budget for the brief (default 1500)."),
|
|
1536
1647
|
profile: z.enum(DELIVERY_PROFILES).optional().describe("Delivery role: builder (default), reviewer, or architect. Changes non-blocking order only."),
|
|
1537
1648
|
as_of: z.string().optional().describe("Time-travel ref (commit/tag/branch): assemble the slice as it stood then."),
|
|
1538
1649
|
task_id: TaskIdSchema.optional().describe("Exact task ID from hunch_task; records this delivery for the task's contribution report."),
|
|
1650
|
+
include: z.array(z.enum(["recent_tasks", "project_dna"])).optional().describe("Opt-in extras; omitted by default to keep the brief small."),
|
|
1539
1651
|
cwd: cwdHintField,
|
|
1540
1652
|
},
|
|
1541
|
-
outputSchema:
|
|
1542
|
-
}, async ({ target, budget_tokens, profile, as_of, task_id }, extra) => {
|
|
1653
|
+
outputSchema: DELIVERY_ADVERTISED_OUTPUT_SCHEMA,
|
|
1654
|
+
}, async ({ target, budget_tokens, profile, as_of, task_id, include }, extra) => {
|
|
1543
1655
|
const deliver = (envelope) => {
|
|
1544
1656
|
const result = deliveredContext(root, as_of ? `${target} (as_of:${as_of})` : target, envelope, extra.sessionId);
|
|
1657
|
+
// Same sibling-fix lesson the pre-edit hook injects, for a file target.
|
|
1658
|
+
// Sibling lessons need a repo-relative file; a target outside the repo gets none.
|
|
1659
|
+
const rel = relative(root, resolve(root, target)).replace(/\\/g, "/");
|
|
1660
|
+
const siblings = as_of || !rel || rel.startsWith("..") || isAbsolute(rel) ? null : siblingGrounding(root, rel, store.recs("symbols"));
|
|
1661
|
+
// Leads the result: the most specific lesson must not trail the memory slice.
|
|
1662
|
+
if (siblings?.text)
|
|
1663
|
+
result.content.unshift({ type: "text", text: siblings.text });
|
|
1545
1664
|
if (task_id) {
|
|
1546
1665
|
try {
|
|
1547
1666
|
// Historical contexts must not borrow today's record text/revision.
|
|
@@ -1561,14 +1680,18 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1561
1680
|
if (as_of && !asOf)
|
|
1562
1681
|
return invalid(`Could not resolve as_of "${as_of}" to a commit.`);
|
|
1563
1682
|
const ctx = store.assembleContext(target, budget_tokens ?? 1500, { asOf });
|
|
1683
|
+
// Both extras below cost brief tokens (and, for DNA/recent-tasks selection, extra
|
|
1684
|
+
// latency) — opt-in only via `include`, omitted from the default brief.
|
|
1564
1685
|
let dnaSupplement = null;
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1686
|
+
if (include?.includes("project_dna")) {
|
|
1687
|
+
try {
|
|
1688
|
+
dnaSupplement = projectDnaDeliverySupplement(discoverProjectDna(root, as_of ?? "HEAD"));
|
|
1689
|
+
}
|
|
1690
|
+
catch {
|
|
1691
|
+
// Context retrieval must keep its existing graceful behavior when the
|
|
1692
|
+
// Git checkout cannot provide DNA; the dedicated DNA tool reports the
|
|
1693
|
+
// exact derivation error when a caller needs diagnostics.
|
|
1694
|
+
}
|
|
1572
1695
|
}
|
|
1573
1696
|
// The "State" section (nuryel.state/1): current derived, in-force commitments and the
|
|
1574
1697
|
// latest receipts whose subject/text matches the target — bounded, ordered, sharing the
|
|
@@ -1576,7 +1699,9 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1576
1699
|
const stateGrounding = asOf ? [] : stateSupplements(store.stateSlice(target), target);
|
|
1577
1700
|
// Recent finished tasks that touched the target: what earlier agent work did
|
|
1578
1701
|
// here, from graph memory. Advisory history sharing the brief's budget.
|
|
1579
|
-
const recentTasks = asOf
|
|
1702
|
+
const recentTasks = (asOf || !include?.includes("recent_tasks"))
|
|
1703
|
+
? []
|
|
1704
|
+
: taskSelectionSupplements(store.selectTasksAuto(target, buildTaskRankingQuery(root, task_id ?? null, target)), target);
|
|
1580
1705
|
const options = {
|
|
1581
1706
|
root,
|
|
1582
1707
|
symbols: store.recs("symbols"),
|
|
@@ -1633,7 +1758,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1633
1758
|
// -- hunch_shortlist (bounded correction-stage diagnostic) ----------------
|
|
1634
1759
|
server.registerTool("hunch_shortlist", {
|
|
1635
1760
|
title: "Shortlist the likely correction stage and declarations",
|
|
1636
|
-
description: "Experimental, deterministic, read-only
|
|
1761
|
+
description: "Experimental, deterministic, read-only diagnostic for a schema/validation issue or reproduction: a flat top five, a hierarchical inspection view, and an advisory progressive inspection queue (max eleven declarations) with receipts. Same-claim evidence is annotated, never reorders. It never claims an exact implementation owner or per-case confidence and does not edit, gate, or capture memory.",
|
|
1637
1762
|
inputSchema: {
|
|
1638
1763
|
issue: z.string().min(1).max(100_000).describe("Issue report or reproduction prose, including observed and expected behavior when available."),
|
|
1639
1764
|
limit: z.number().int().min(1).max(5).optional().describe("Candidate count, capped at five (default 5)."),
|
|
@@ -1680,18 +1805,27 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1680
1805
|
title: "Recent activity + the roadmap (the hot view)",
|
|
1681
1806
|
description: "What just happened and what's next, straight from the graph: the last N decisions, the ROADMAP, and any inline human question such as an imported ADR awaiting explicit approve/decline. Call at session start to orient, or before planning what to work on. Same data as the wiki's now.md. Public store only, EXCEPT a queued commit-repair's liveness is checked against the full store (so a private-overlay decision's fully-answerable repair doesn't go silently unanswerable); only its id and the commit shas ever surface, never its title.",
|
|
1682
1807
|
inputSchema: {
|
|
1683
|
-
recent_limit: z.number().optional().describe("How many recent decisions to include (default
|
|
1808
|
+
recent_limit: z.number().optional().describe("How many recent decisions to include (default 5)."),
|
|
1809
|
+
roadmap_limit: z.number().optional().describe("How many roadmap entries to include (default 2); the rest are counted."),
|
|
1684
1810
|
},
|
|
1685
|
-
}, async ({ recent_limit }) => {
|
|
1686
|
-
|
|
1811
|
+
}, async ({ recent_limit, roadmap_limit }) => {
|
|
1812
|
+
// Bounded by default (#371): the hot view is read at session start, so its
|
|
1813
|
+
// size is paid on every session; the rest stays one call away.
|
|
1814
|
+
const decisions = store.json.loadAll("decisions");
|
|
1815
|
+
const { recent, roadmap, pendingReview } = nowData(decisions, recent_limit ?? 5);
|
|
1816
|
+
const shownRoadmap = roadmap.slice(0, Math.max(0, roadmap_limit ?? 2));
|
|
1687
1817
|
const L = [`🔥 Recent (${recent.length}):`];
|
|
1688
1818
|
for (const r of recent)
|
|
1689
1819
|
L.push(` ${r.date} [${r.status}] ${r.title} (${r.id}${r.topic ? `, ${r.topic}` : ""})`);
|
|
1820
|
+
if (decisions.length > recent.length)
|
|
1821
|
+
L.push(` +${decisions.length - recent.length} more — hunch_now(recent_limit) or hunch now`);
|
|
1690
1822
|
L.push("", `🗺 Roadmap — live proposed decisions (${roadmap.length}):`);
|
|
1691
1823
|
if (!roadmap.length)
|
|
1692
1824
|
L.push(" (empty — record intent as a PROPOSED decision and it appears here)");
|
|
1693
|
-
for (const r of
|
|
1825
|
+
for (const r of shownRoadmap)
|
|
1694
1826
|
L.push(` • ${r.title} (${r.id}${r.topic ? `, ${r.topic}` : ""}, since ${r.date})\n ${r.note}`);
|
|
1827
|
+
if (roadmap.length > shownRoadmap.length)
|
|
1828
|
+
L.push(` +${roadmap.length - shownRoadmap.length} more — hunch_now(roadmap_limit) or hunch now`);
|
|
1695
1829
|
if (pendingReview > 0)
|
|
1696
1830
|
L.push("", `${pendingReview} legacy un-vouched draft(s) — \`hunch adopt-drafts\` auto-trusts them as advisory (new captures land trusted automatically).`);
|
|
1697
1831
|
// Workspace ledger, from stored PUBLIC records only (same jurisdiction rule as the rest
|
|
@@ -1723,7 +1857,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1723
1857
|
// git; other machines come from stored records, which are display-only.
|
|
1724
1858
|
server.registerTool("hunch_workspaces", {
|
|
1725
1859
|
title: "Worktrees and branches across machines",
|
|
1726
|
-
description: "
|
|
1860
|
+
description: "Worktrees per machine and every local branch with a deterministic verdict — merged (ancestry / squash / rebase), never pushed, upstream gone, dirty — plus a recommended action. Use INSTEAD of git branch / git worktree list to see what is open, stale, or deletable. This machine is read live; others from memory (stale ones marked unverified). Read-only. Not for design rationale (hunch_why) or code structure (hunch_structure).",
|
|
1727
1861
|
inputSchema: {
|
|
1728
1862
|
view: z.enum(["inventory", "branches"]).optional().describe("inventory = one row per worktree (default); branches = one row per branch with its verdict and action."),
|
|
1729
1863
|
machine: z.string().optional().describe("Only this machine label."),
|
|
@@ -1772,7 +1906,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1772
1906
|
// (con_e04226bd05): no Claude-specific behavior.
|
|
1773
1907
|
server.registerTool("hunch_escalations", {
|
|
1774
1908
|
title: "Decisions the human must make now (ask inline, not a queue)",
|
|
1775
|
-
description: "
|
|
1909
|
+
description: "Decisions only the human can make, so you ASK THE USER before acting: topic conflicts (>1 live decision per topic), premise-stale decisions (the recorded reason no longer holds; authority is unchanged until the human re-attests, supersedes, or retires), one imported ADR at a time awaiting approve/decline, a queued commit-provenance repair (`hunch repair-provenance --apply`, never applied unattended), and Constitution policies awaiting review or activation. Normally empty. Never decide an entry for the user — silence is never approval. Reads the public or shared-mode store, never private-mode records — except a queued repair, whose liveness is checked against the full store and which surfaces only its id and commit shas.",
|
|
1776
1910
|
inputSchema: {},
|
|
1777
1911
|
}, async () => {
|
|
1778
1912
|
const items = pendingEscalations(store.advisoryRecs("decisions"));
|
|
@@ -1806,7 +1940,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1806
1940
|
// -- hunch_review_imported_adr (the chat-native countersign) --------------
|
|
1807
1941
|
server.registerTool("hunch_review_imported_adr", {
|
|
1808
1942
|
title: "Apply the human's exact imported-ADR answer",
|
|
1809
|
-
description: "Use ONLY after the human explicitly answers the currently surfaced imported-ADR question with approve or decline in this conversation. Never infer approval from silence, continued work,
|
|
1943
|
+
description: "Use ONLY after the human explicitly answers the currently surfaced imported-ADR question with approve or decline, in this conversation. Never infer approval from silence, continued work, an earlier generic sign-off, or the ADR file's own status. The source and review hashes bind the answer to the exact bytes and mapped meaning shown. Approve grants human-confirmed authority; decline records review but keeps the ADR advisory.",
|
|
1810
1944
|
inputSchema: {
|
|
1811
1945
|
decision_id: z.string().regex(/^dec_[A-Za-z0-9_-]+$/),
|
|
1812
1946
|
expected_source_hash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
@@ -1887,7 +2021,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1887
2021
|
// -- hunch_capture_decision (decision-grounding: the grilling front door) --
|
|
1888
2022
|
server.registerTool("hunch_capture_decision", {
|
|
1889
2023
|
title: "Capture a decision (grilling interview)",
|
|
1890
|
-
description: "Start a decision-capture interview: returns the
|
|
2024
|
+
description: "Start a decision-capture interview ('/capture', 'grill me'): returns the protocol — ONE question at a time until resolved — and a capture-session token; writes nothing. Then commit via hunch_record_decision with the token + confirmed topic. The token is NOT a human signature: human-confirmed authority needs the human's own confirmation (client prompt, or `hunch review --confirm <id>`). Not for corrections (hunch_record_correction) or observations (hunch_record_finding).",
|
|
1891
2025
|
inputSchema: {
|
|
1892
2026
|
topic: z.string().optional().describe("proposed topic anchor (confirm with the human before committing)"),
|
|
1893
2027
|
seed: z.string().optional().describe("what the decision is about, to focus the first question"),
|
|
@@ -1941,7 +2075,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1941
2075
|
// -- hunch_record_decision (write-back) -----------------------------------
|
|
1942
2076
|
server.registerTool("hunch_record_decision", {
|
|
1943
2077
|
title: "Record a decision (write-back)",
|
|
1944
|
-
description: "Persist a
|
|
2078
|
+
description: "Persist a Decision (ADR) with provenance after a non-trivial design choice. private:true keeps it in the HUNCH_PRIVATE_DIR overlay, never committed here. Returns id, home, status. Not for an agent-obeyed rule (hunch_record_correction) or an observation (hunch_record_finding). Errors: 'Refused:' a gate held (don't retry); 'Invalid:' fix arguments; 'Failed to' internal.",
|
|
1945
2079
|
inputSchema: {
|
|
1946
2080
|
decision: z.object({
|
|
1947
2081
|
title: z.string(),
|
|
@@ -1951,7 +2085,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1951
2085
|
alternatives_rejected: z.array(z.string()).optional(),
|
|
1952
2086
|
related_files: z.array(z.string()).optional(),
|
|
1953
2087
|
related_components: z.array(z.string()).optional(),
|
|
1954
|
-
topic: z.string().optional().describe("decision-grounding anchor — one topic per decision; enables doc≠graph drift detection
|
|
2088
|
+
topic: z.string().optional().describe("decision-grounding anchor — one topic per decision; enables doc≠graph drift detection. Omit to leave un-anchored."),
|
|
1955
2089
|
// FLAT, matching PremiseSchema exactly. A nested { check: {...} } shape is
|
|
1956
2090
|
// silently STRIPPED by Zod, leaving a claim-only premise — and a claim-only
|
|
1957
2091
|
// premise is "documented only (no check attached)", which ALWAYS HOLDS. An
|
|
@@ -1959,20 +2093,20 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1959
2093
|
// the exact fail-open this feature exists to prevent. Keep in lockstep with
|
|
1960
2094
|
// PremiseSchema in src/core/types.ts.
|
|
1961
2095
|
premises: z.array(z.object({
|
|
1962
|
-
claim: z.string().min(1).describe("
|
|
1963
|
-
path_absent: z.string().optional().describe("
|
|
1964
|
-
under: z.string().optional().describe("required with path_absent: an EXISTING repo-relative
|
|
1965
|
-
path_exists: z.string().optional().describe("
|
|
1966
|
-
review_by: z.string().optional().describe("
|
|
1967
|
-
attested: z.string().optional().describe("ISO date a human last attested
|
|
1968
|
-
})).optional().describe("
|
|
2096
|
+
claim: z.string().min(1).describe("human-readable reason"),
|
|
2097
|
+
path_absent: z.string().optional().describe("holds while this repo-relative path does NOT exist; requires `under`. Prefer path_exists — negative probes fail OPEN, positive fail closed."),
|
|
2098
|
+
under: z.string().optional().describe("required with path_absent: an EXISTING repo-relative ancestor ('src/gateway' -> under 'src'). If it disappears, reads unevaluable, not silently absent."),
|
|
2099
|
+
path_exists: z.string().optional().describe("holds while this repo-relative path exists"),
|
|
2100
|
+
review_by: z.string().optional().describe("holds until this ISO date, then needs re-attesting"),
|
|
2101
|
+
attested: z.string().optional().describe("ISO date a human last attested (informational)"),
|
|
2102
|
+
})).optional().describe("checkable reasons this decision rests on, at most one check per premise. A dead premise never changes authority, only raises a human escalation. Omit on re-record to keep the incumbent's."),
|
|
1969
2103
|
status: z.enum(["proposed", "accepted", "rejected", "superseded"]).optional(),
|
|
1970
2104
|
commit: z.string().optional(),
|
|
1971
|
-
supersedes: z.string().optional().describe("id of a decision this
|
|
1972
|
-
private: z.boolean().optional().describe("write into the PRIVATE overlay store (HUNCH_PRIVATE_DIR)
|
|
2105
|
+
supersedes: z.string().optional().describe("id of a decision this replaces — closes its valid-time window (invalidate, don't delete)"),
|
|
2106
|
+
private: z.boolean().optional().describe("write into the PRIVATE overlay store (HUNCH_PRIVATE_DIR), not the committed repo — for sensitive decisions. Errors if no private store is configured."),
|
|
1973
2107
|
}),
|
|
1974
|
-
capture_token: z.string().optional().describe("token from hunch_capture_decision — proves this write
|
|
1975
|
-
task_id:
|
|
2108
|
+
capture_token: z.string().optional().describe("token from hunch_capture_decision — proves this write tails a grilling interview (not a signature: the client may still ask the human to confirm). Omit only for a quick manual record (returns a deprecation nudge)."),
|
|
2109
|
+
task_id: saveTaskIdField,
|
|
1976
2110
|
cwd: cwdHintField,
|
|
1977
2111
|
},
|
|
1978
2112
|
}, async ({ decision, capture_token, task_id }) => {
|
|
@@ -2196,18 +2330,18 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2196
2330
|
// -- hunch_record_correction (write-back: "Never Twice") ------------------
|
|
2197
2331
|
server.registerTool("hunch_record_correction", {
|
|
2198
2332
|
title: "Capture a correction as an enforced constraint (Never Twice)",
|
|
2199
|
-
description: "When a human corrects the agent ('
|
|
2333
|
+
description: "When a human corrects the agent ('never call X here'), persist it as a SCOPED Constraint with provenance, so the pre-edit hook and CI Constraint Guard hold every assistant to it. severity:'blocking' only when the human said never/must; applies_to_all:true only for a genuinely repo-wide rule (else scoped to scope_hint_file). Returns the constraint id and scope. Not for a design choice (hunch_record_decision) or an observed gap with no rule yet (hunch_record_finding).",
|
|
2200
2334
|
inputSchema: {
|
|
2201
2335
|
rule: z.string().describe("The invariant in the human's words, e.g. \"never call the pay-per-token API here\"."),
|
|
2202
|
-
scope_hint_file: z.string().optional().describe("
|
|
2336
|
+
scope_hint_file: z.string().optional().describe("File the correction was about; scopes the constraint to it (conservative default). Prefer a repo-relative path — an absolute one is relativized, and one outside the repo is discarded rather than scoped to a path that could never match."),
|
|
2203
2337
|
severity: z.enum(["advisory", "warning", "blocking"]).optional().describe("Default 'warning'. Use 'blocking' only for a hard never/must rule."),
|
|
2204
2338
|
applies_to_all: z.boolean().optional().describe("True ONLY if the rule is genuinely repo-wide (scopes to **); required to make a repo-wide rule blocking."),
|
|
2205
2339
|
type: z.enum(["security", "performance", "correctness", "architecture", "compliance"]).optional(),
|
|
2206
2340
|
rationale: z.string().optional().describe("Why it must hold."),
|
|
2207
2341
|
source_decision: z.string().optional().describe("id of a decision this correction derives from."),
|
|
2208
|
-
private: z.boolean().optional().describe("write into the PRIVATE overlay store (HUNCH_PRIVATE_DIR)
|
|
2209
|
-
capture_token: z.string().optional().describe("token from hunch_capture_decision.
|
|
2210
|
-
task_id:
|
|
2342
|
+
private: z.boolean().optional().describe("write into the PRIVATE overlay store (HUNCH_PRIVATE_DIR), not the committed repo — enforced locally (pre-edit hook + check), never in a public PR comment. Errors if no private store is configured."),
|
|
2343
|
+
capture_token: z.string().optional().describe("token from hunch_capture_decision. Recorded and enforced either way, as agent testimony capped at severity 'warning'. A token never lets it DENY: blocking authority needs a human running the printed `hunch review --confirm <id> --severity <s>`."),
|
|
2344
|
+
task_id: saveTaskIdField,
|
|
2211
2345
|
cwd: cwdHintField,
|
|
2212
2346
|
},
|
|
2213
2347
|
}, async (input) => {
|
|
@@ -2295,12 +2429,12 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2295
2429
|
// -- hunch_record_finding (write-back: observations, no diff) ---------------
|
|
2296
2430
|
server.registerTool("hunch_record_finding", {
|
|
2297
2431
|
title: "Record a finding (an observation with no code change)",
|
|
2298
|
-
description: "Persist an OBSERVATION
|
|
2432
|
+
description: "Persist an OBSERVATION with no code change — an audit gap, a measured number, a platform fact, an incident — anchored to a date + evidence, not a commit. Advisory: grounds future edits to the affected files (pre-edit hook, hunch_context) and is listed by hunch_findings; never blocks. Re-record the SAME title to update triage (e.g. triage:'resolved' + resolved_commit). If it violates an unrecorded rule, record the rule first (hunch_record_correction) and link it via violates_constraint. Not for a rule to enforce (hunch_record_correction) or a choice between alternatives (hunch_record_decision).",
|
|
2299
2433
|
inputSchema: {
|
|
2300
2434
|
finding: z.object({
|
|
2301
2435
|
title: z.string().describe("stable one-line name — re-recording the same title updates the finding"),
|
|
2302
2436
|
observation: z.string().describe("what was observed, in plain words"),
|
|
2303
|
-
evidence: z.array(z.string()).optional().describe("
|
|
2437
|
+
evidence: z.array(z.string()).optional().describe("query/command run + representative output — a finding without evidence is an opinion"),
|
|
2304
2438
|
method: z.string().optional().describe("rb_* runbook that re-runs the audit (makes it re-verifiable)"),
|
|
2305
2439
|
severity: z.enum(["low", "medium", "high", "critical"]).optional(),
|
|
2306
2440
|
triage: z.enum(["open", "accepted-risk", "scheduled", "resolved", "stale"]).optional().describe("default 'open'. 'resolved' should carry resolved_commit."),
|
|
@@ -2309,9 +2443,9 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2309
2443
|
violates_constraint: z.string().optional().describe("con_* this finding is a known violation of"),
|
|
2310
2444
|
spawned_decision: z.string().optional().describe("dec_* recorded in response"),
|
|
2311
2445
|
resolved_commit: z.string().optional().describe("the commit that fixed it (with triage:'resolved')"),
|
|
2312
|
-
private: z.boolean().optional().describe("write into the PRIVATE overlay store
|
|
2446
|
+
private: z.boolean().optional().describe("write into the PRIVATE overlay store, not the committed repo. Errors if no private store is configured."),
|
|
2313
2447
|
}),
|
|
2314
|
-
task_id:
|
|
2448
|
+
task_id: saveTaskIdField,
|
|
2315
2449
|
cwd: cwdHintField,
|
|
2316
2450
|
},
|
|
2317
2451
|
}, async ({ finding, task_id }) => {
|
|
@@ -2557,32 +2691,44 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2557
2691
|
// -- hunch_findings (read: the open-observations ledger) --------------------
|
|
2558
2692
|
server.registerTool("hunch_findings", {
|
|
2559
2693
|
title: "Open findings for a scope",
|
|
2560
|
-
description: "List LIVE findings (observed gaps/debt with no fix yet — triage open/accepted-risk/scheduled)
|
|
2694
|
+
description: "List LIVE findings (observed gaps/debt with no fix yet — triage open/accepted-risk/scheduled) for a file, glob, or symbol; omit scope for the whole ledger. Call before planning work in an area to inherit past audits instead of re-discovering them. Lists each finding's first sentence; pass id for one finding's full observation. Advisory; resolved/stale findings are excluded unless all:true. Not for invariants (hunch_check_constraints) or bug history (hunch_bug_lineage): findings are observations, never rules.",
|
|
2561
2695
|
inputSchema: {
|
|
2562
2696
|
scope: z.string().optional().describe("a path, glob, or symbol (e.g. src/procs/** or dbo.GetOrders); omit for all"),
|
|
2563
2697
|
all: z.boolean().optional().describe("include resolved/stale findings (the full history)"),
|
|
2698
|
+
id: z.string().optional().describe("one finding id (fnd_*) — returns its full observation, whatever its triage; scope/all/limit are ignored"),
|
|
2699
|
+
limit: z.number().int().min(1).max(FINDINGS_MAX).optional().describe(`how many to list (default ${FINDINGS_CAP}, max ${FINDINGS_MAX})`),
|
|
2564
2700
|
},
|
|
2565
|
-
}, async ({ scope, all }) => {
|
|
2701
|
+
}, async ({ scope, all, id, limit }) => {
|
|
2702
|
+
const concerns = (f) => [...f.affected_files, ...f.affected_symbols].join(", ") || "(unscoped)";
|
|
2703
|
+
const links = (f) => [f.violates_constraint ? `violates ${f.violates_constraint}` : "", f.method ? `re-verify via ${f.method}` : "", f.resolved_commit ? `fixed in ${f.resolved_commit.slice(0, 9)}` : ""].filter(Boolean).join("; ");
|
|
2704
|
+
if (id) {
|
|
2705
|
+
const f = store.recs("findings").find((r) => r.id === id);
|
|
2706
|
+
if (!f)
|
|
2707
|
+
return ok(`No finding "${id}".`);
|
|
2708
|
+
return ok(`[${f.triage}/${f.severity}] ${f.title} (${f.id}, observed ${f.observed_at.slice(0, 10)})\n${f.observation}\nconcerns: ${concerns(f)}${links(f) ? `\n${links(f)}` : ""}`);
|
|
2709
|
+
}
|
|
2566
2710
|
const live = (f) => f.triage === "open" || f.triage === "accepted-risk" || f.triage === "scheduled";
|
|
2567
2711
|
const list = (scope ? store.liveFindingsFor(scope) : store.recs("findings").filter(all ? () => true : live))
|
|
2568
2712
|
.filter(all ? () => true : live)
|
|
2569
2713
|
.sort((a, b) => (SEV_BUG[b.severity] ?? 0) - (SEV_BUG[a.severity] ?? 0) || a.id.localeCompare(b.id));
|
|
2570
2714
|
if (!list.length)
|
|
2571
2715
|
return ok(`No ${all ? "" : "live "}findings${scope ? ` for "${scope}"` : ""}. (Record one after an audit with hunch_record_finding.)`);
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2716
|
+
const cap = limit ?? FINDINGS_CAP;
|
|
2717
|
+
const L = list.slice(0, cap).map((f) => {
|
|
2718
|
+
const where = [...f.affected_files, ...f.affected_symbols];
|
|
2719
|
+
const shown = where.length > 3 ? `${where.slice(0, 3).join(", ")} (+${where.length - 3})` : concerns(f);
|
|
2720
|
+
return `• [${f.triage}/${f.severity}] ${f.title} (${f.id}, observed ${f.observed_at.slice(0, 10)})\n ${firstSentence(f.observation)}\n concerns: ${shown}`;
|
|
2575
2721
|
});
|
|
2576
|
-
return ok(`${list.length} finding(s)${scope ? ` for "${scope}"` : ""}:\n${L.join("\n")}${more(list.length,
|
|
2722
|
+
return ok(`${list.length} finding(s)${scope ? ` for "${scope}"` : ""} — hunch_findings(id) for one in full:\n${L.join("\n")}${more(list.length, cap, "raise limit or narrow scope")}`);
|
|
2577
2723
|
});
|
|
2578
2724
|
server.registerTool("hunch_policy_upgrade_correction", {
|
|
2579
2725
|
title: "Build a proved review proposal from one exact correction",
|
|
2580
|
-
description: "Upgrade the exact supported static ESM import-declaration package projection of one captured correction into a deterministic review packet when the baseline is clean. Writes proposal, plan, proof, and evidence artifacts only; never activates, warns, blocks, or grants authority. Unsupported corrections keep their immediate legacy guard and create no policy.",
|
|
2726
|
+
description: "Upgrade the exact supported static ESM import-declaration package projection of one captured correction into a deterministic review packet, when the baseline is clean. Writes proposal, plan, proof, and evidence artifacts only; never activates, warns, blocks, or grants authority. Unsupported corrections keep their immediate legacy guard and create no policy.",
|
|
2581
2727
|
inputSchema: {
|
|
2582
2728
|
constraint_id: z.string().describe("Captured correction constraint id (con_*)."),
|
|
2583
2729
|
public_only: z.boolean().optional().describe("Read and write only the public correction home."),
|
|
2584
|
-
private_only: z.boolean().optional().describe("Keep correction-derived
|
|
2585
|
-
include_artifacts: z.boolean().optional().describe("Include the
|
|
2730
|
+
private_only: z.boolean().optional().describe("Keep correction-derived artifacts in the private overlay; the public source-code graph is refreshed before proof."),
|
|
2731
|
+
include_artifacts: z.boolean().optional().describe("Include the full Policy IR, proof plan, receipts, and evidence. Default is a concise review envelope."),
|
|
2586
2732
|
cwd: cwdHintField,
|
|
2587
2733
|
},
|
|
2588
2734
|
}, async ({ constraint_id, public_only, private_only, include_artifacts }) => {
|
|
@@ -2647,7 +2793,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2647
2793
|
// -- hunch_merge_verdict (Causal Merge Verdict — read-only, client-agnostic) --
|
|
2648
2794
|
server.registerTool("hunch_merge_verdict", {
|
|
2649
2795
|
title: "Causal merge verdict: is this change safe against the recorded WHY?",
|
|
2650
|
-
description: "Before opening or merging a PR, replay a diff against
|
|
2796
|
+
description: "Before opening or merging a PR, replay a diff against memory and return ONE verdict — BLOCK / WARN / PASS — citing the decision and bug behind each invariant in scope, plus advisory blast-radius invariants, re-introduced retired code, and symbols already defined elsewhere. Deterministic, no LLM. Default: staged changes; working:true for all local changes, base (e.g. origin/main) for a PR range, commit for one commit. Not for an advisory impact map (hunch_pr_impact), or intent erosion with no diff (hunch_conformance).",
|
|
2651
2797
|
inputSchema: {
|
|
2652
2798
|
base: z.string().optional().describe("Diff against this base ref (e.g. origin/main) — for a PR/branch."),
|
|
2653
2799
|
commit: z.string().optional().describe("Diff a single commit (sha/ref). Omit base AND commit to check staged changes."),
|
|
@@ -2690,7 +2836,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2690
2836
|
// -- hunch_pr_impact (read-only impact surface — advisory, never gates) ----
|
|
2691
2837
|
server.registerTool("hunch_pr_impact", {
|
|
2692
2838
|
title: "PR impact: the dependency + memory surface of a change",
|
|
2693
|
-
description: "
|
|
2839
|
+
description: "IMPACT SURFACE of a change (default staged; or working:true, base, or commit): files that transitively depend on it, invariants in scope and via blast radius, and decisions on the touched files. Read-only, advisory. Not for a verdict (hunch_merge_verdict).",
|
|
2694
2840
|
inputSchema: {
|
|
2695
2841
|
base: z.string().optional().describe("Diff against this base ref (e.g. origin/main) — for a PR/branch."),
|
|
2696
2842
|
commit: z.string().optional().describe("Impact of a single commit (sha/ref). Omit base AND commit for staged changes."),
|