@davesheffer/hunch 1.46.0 → 1.47.1
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/dist/cli/index.js +280 -358
- package/dist/cli/invocation.d.ts +8 -13
- package/dist/cli/invocation.js +26 -29
- package/dist/cli/taskReport.d.ts +6 -0
- package/dist/cli/taskReport.js +31 -8
- package/dist/constitution/delta.js +2 -2
- package/dist/constitution/sourceMutation.js +28 -18
- package/dist/core/commitrepair.d.ts +43 -138
- package/dist/core/commitrepair.js +76 -167
- package/dist/core/correction.d.ts +2 -0
- package/dist/core/correction.js +23 -10
- package/dist/core/docanchors.js +9 -9
- package/dist/core/drift.js +22 -27
- package/dist/core/escalations.d.ts +16 -58
- package/dist/core/escalations.js +39 -121
- package/dist/core/glob.d.ts +7 -7
- package/dist/core/glob.js +12 -11
- package/dist/core/groundingLag.d.ts +2 -9
- package/dist/core/groundingLag.js +5 -11
- package/dist/core/groundingMerge.d.ts +2 -7
- package/dist/core/groundingMerge.js +35 -54
- package/dist/core/paths.d.ts +17 -24
- package/dist/core/paths.js +37 -43
- package/dist/core/refrepair.d.ts +2 -1
- package/dist/core/refrepair.js +4 -4
- package/dist/core/repairqueue.d.ts +17 -34
- package/dist/core/repairqueue.js +48 -98
- package/dist/core/taskRecord.d.ts +1 -1
- package/dist/core/taskRecord.js +2 -2
- package/dist/core/taskReport.d.ts +8 -3
- package/dist/core/taskReport.js +7 -2
- package/dist/core/taskReportEvidence.d.ts +7 -0
- package/dist/core/taskReportEvidence.js +16 -4
- package/dist/core/taskReportHook.js +2 -2
- package/dist/core/taskReportPublic.js +2 -2
- package/dist/core/taskReportRender.js +2 -2
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/extractors/diff.d.ts +4 -4
- package/dist/extractors/diff.js +25 -29
- package/dist/extractors/git.d.ts +18 -23
- package/dist/extractors/git.js +72 -64
- package/dist/extractors/helm.d.ts +10 -0
- package/dist/extractors/helm.js +43 -59
- package/dist/extractors/indexer.d.ts +6 -0
- package/dist/extractors/indexer.js +126 -178
- package/dist/extractors/k8sManifest.d.ts +23 -80
- package/dist/extractors/k8sManifest.js +319 -690
- package/dist/extractors/languages.d.ts +28 -37
- package/dist/extractors/languages.js +55 -54
- package/dist/extractors/nativeTreeSitter.d.ts +4 -4
- package/dist/extractors/nativeTreeSitter.js +17 -14
- package/dist/extractors/parse.d.ts +7 -13
- package/dist/extractors/parse.js +39 -51
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.js +3 -5
- package/dist/integrations/health.js +1 -1
- package/dist/integrations/hooks.d.ts +9 -14
- package/dist/integrations/hooks.js +19 -28
- package/dist/integrations/mergeDriver.js +13 -14
- package/dist/integrations/probe.d.ts +11 -0
- package/dist/integrations/probe.js +29 -4
- package/dist/integrations/sync.d.ts +16 -4
- package/dist/integrations/sync.js +54 -3
- package/dist/integrations/workspaceLedger.d.ts +1 -1
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +71 -83
- package/dist/mcp/taskReportTools.js +4 -4
- package/dist/serve/app.js +2 -2
- package/dist/store/hunchStore.js +12 -18
- package/dist/synthesis/provider.d.ts +19 -21
- package/dist/synthesis/provider.js +67 -76
- package/dist/synthesis/synthesize.d.ts +8 -8
- package/dist/synthesis/synthesize.js +32 -49
- package/dist/taskReports.d.ts +1 -0
- package/dist/taskReports.js +2 -2
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/mcp/server.js
CHANGED
|
@@ -34,7 +34,7 @@ import { refreshExistingGrounding } from "../integrations/providers.js";
|
|
|
34
34
|
import { workspaceLedgerView, renderWorktreeTable, renderBranchTable, workspaceSummaryLine, snapshotHasHome, recordWorkspaceSnapshot, branchRows, worktreeRows } from "../integrations/workspaceLedger.js";
|
|
35
35
|
import { workspacesConfig } from "../core/config.js";
|
|
36
36
|
import { revParse, asOfDate, revExists, lastChangeDate, checkFreshnessRevision, teamPullIsFresh, rangeFiles, rangeGateDiff, commitFiles, commitGateDiff, stagedFiles, stagedGateDiff, workingFiles, workingGateDiff, pullHunchStatusDetailed, describePullFailure, sameRemoteUrl, currentBranch, worktreePaths, pathKnownToHistory } from "../extractors/git.js";
|
|
37
|
-
import { flushCapture, flushMemoryHome, pinSharedRemote } from "../integrations/sync.js";
|
|
37
|
+
import { flushCapture, flushStateWrite, flushMemoryHome, pinSharedRemote, HELD_ON_DEFAULT_BRANCH } from "../integrations/sync.js";
|
|
38
38
|
import { withWriteLock } from "../serve/writelock.js";
|
|
39
39
|
import { advertisedTeamRemoteContract, ensureTeamOverlay, isTeamStoreTrusted, overlayMatchesTeamRemote, teamWiringConsented, untrustedTeamStoreMessage, readTeamConfig, teamRemoteContract, teamSharedRef } from "../integrations/team.js";
|
|
40
40
|
import { formatSearchHit, formatStructure } from "../core/format.js";
|
|
@@ -90,34 +90,36 @@ const err = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
|
90
90
|
* Transient states that say "retry" in their own words stay unprefixed. */
|
|
91
91
|
const refused = (text) => err(`Refused: ${text}`);
|
|
92
92
|
const invalid = (text) => err(`Invalid: ${text}`);
|
|
93
|
-
/** Shared by every auto-committing write tool
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
93
|
+
/** Shared by every auto-committing write tool. The MCP roots protocol cannot observe an
|
|
94
|
+
* agent-driven directory change or worktree entry, so a stdio server's cached root never
|
|
95
|
+
* moves by itself; this field is the client-agnostic fallback, resolved fresh on every
|
|
96
|
+
* call by the generic tool wrapper. */
|
|
97
97
|
const cwdHintField = z.string().optional().describe("Your current directory, when it differs from where this session started (e.g. a worktree); " +
|
|
98
98
|
"writes then commit there, not to the original root.");
|
|
99
99
|
/** Shared by hunch_record_decision/correction/finding — identical text repeated 3x
|
|
100
100
|
* otherwise. hunch_context's task_id field differs (delivery vs. save) and keeps its
|
|
101
101
|
* own description. */
|
|
102
102
|
const saveTaskIdField = TaskIdSchema.optional().describe("Task ID, to attribute this save to it; never affects capture authority.");
|
|
103
|
-
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
/** Pulls the cwd hint out of a tool call's already-parsed first argument. The wrapper
|
|
104
|
+
* runs for EVERY tool (read and write), so this assumes nothing about any one tool's
|
|
105
|
+
* input shape: anything that is not an object with a non-blank string `cwd` yields
|
|
106
|
+
* undefined. The original (untrimmed) string is returned. */
|
|
107
|
+
function cwdHintOf(input) {
|
|
108
|
+
if (typeof input !== "object" || input === null)
|
|
108
109
|
return undefined;
|
|
109
110
|
const cwd = input.cwd;
|
|
110
|
-
return typeof cwd === "string" && cwd.trim() ? cwd : undefined;
|
|
111
|
+
return typeof cwd === "string" && cwd.trim() !== "" ? cwd : undefined;
|
|
111
112
|
}
|
|
112
113
|
/** Honest auto-commit suffix: reports only what flushCapture ACTUALLY did. A skipped
|
|
113
114
|
* commit (backstop/lock/nothing staged) says nothing — the record is on disk and the
|
|
114
115
|
* next flush sweeps it up; claiming "auto-committed" there would be a lie. */
|
|
115
|
-
const flushNote = (flush, home, mode) => flush === "
|
|
116
|
-
: flush === "committed"
|
|
117
|
-
|
|
118
|
-
?
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
const flushNote = (flush, home, mode) => flush === "held" ? ` (${HELD_ON_DEFAULT_BRANCH})`
|
|
117
|
+
: flush === "pushed" ? ` (committed + pushed to the ${mode === "shared" ? "shared team store" : "private repo"})`
|
|
118
|
+
: flush === "committed"
|
|
119
|
+
? home === "private"
|
|
120
|
+
? " (committed to the overlay repo — push deferred: offline, no upstream, or merge conflict; the next capture or `hunch private --sync` retries)"
|
|
121
|
+
: " (auto-committed to .hunch/ — rides your next push)"
|
|
122
|
+
: "";
|
|
121
123
|
/** When an overlay exists, a PUBLIC write deserves one visible line: a record that
|
|
122
124
|
* lands in the committed store publishes on the next push, and an agent writing
|
|
123
125
|
* strategy/competitive content there is a leak nobody notices until it ships
|
|
@@ -150,11 +152,9 @@ const publicHomeNote = (home, hasPrivate, record, hunchDir) => {
|
|
|
150
152
|
return risk;
|
|
151
153
|
return "\nℹ Landed in the COMMITTED PUBLIC store (publishes with the repo). For sensitive/strategy content, re-record with private:true — the overlay store." + risk;
|
|
152
154
|
};
|
|
153
|
-
/** Self-diagnosing destination
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* appends this so the destination is always visible in the response, whether or
|
|
157
|
-
* not a cwd hint was involved in choosing it. */
|
|
155
|
+
/** Self-diagnosing destination suffix for every auto-committing write tool. A capture
|
|
156
|
+
* silently landing in the wrong repo or branch is only discoverable by auditing git log
|
|
157
|
+
* by hand, so the destination is ALWAYS shown, whether or not a cwd hint chose it. */
|
|
158
158
|
const destinationNote = (destRoot) => {
|
|
159
159
|
const branch = currentBranch(destRoot);
|
|
160
160
|
return ` [captured${branch ? ` on branch ${branch}` : ""} in ${destRoot}]`;
|
|
@@ -586,12 +586,10 @@ function nuryelWriteSubject(facet, record) {
|
|
|
586
586
|
const kind = facet.endsWith("s") ? facet.slice(0, -1) : facet;
|
|
587
587
|
return `${kind}${typeof label === "string" ? ` "${label.slice(0, 60)}"` : ""} (nuryel_write)`;
|
|
588
588
|
}
|
|
589
|
-
/** Where a capture keyed to
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
|
|
593
|
-
* regardless), silently misreporting the destination for a private-homed proof. */
|
|
594
|
-
const resolveDestRoot = (home, store, root) => home === "private" && store.privateDir ? store.privateDir : root;
|
|
589
|
+
/** Where a capture keyed to a storage home actually lands. Centralized so every
|
|
590
|
+
* destination-reporting call site agrees: hunch_policy_upgrade_correction once computed
|
|
591
|
+
* its own home but reported the public root regardless, misreporting a private-homed proof. */
|
|
592
|
+
const destinationRoot = (home, store, root) => home === "private" && store.privateDir ? store.privateDir : root;
|
|
595
593
|
// Read-side token budgets: every tool result is injected into a Claude Code
|
|
596
594
|
// session, so an uncapped list pollutes the context window. Cap each list to its
|
|
597
595
|
// highest-signal head (records are pre-sorted by severity/confidence) and tell the
|
|
@@ -993,13 +991,12 @@ function qualityNudge(rec) {
|
|
|
993
991
|
}
|
|
994
992
|
return "";
|
|
995
993
|
}
|
|
996
|
-
/** Resolve a free-form target (symbol id
|
|
997
|
-
*
|
|
998
|
-
*
|
|
999
|
-
* apart on the same question. */
|
|
994
|
+
/** Resolve a free-form target (symbol id, name, or file path) to symbol records using
|
|
995
|
+
* tiered matching — exact id, then exact name, then exact file, then a path-segment-anchored
|
|
996
|
+
* suffix. Shared with HunchStore.why() through matchSymbolsTiered so the two never disagree. */
|
|
1000
997
|
export function resolveSymbols(store, target) {
|
|
1001
|
-
// An absolute
|
|
1002
|
-
//
|
|
998
|
+
// An absolute path (an agent's verbatim edit-payload path) must match the repo-relative
|
|
999
|
+
// stored file paths (issue #296).
|
|
1003
1000
|
target = repoRelativeTarget(target, store.publicRoot);
|
|
1004
1001
|
const syms = store.json.loadAll("symbols");
|
|
1005
1002
|
const components = store.json.loadAll("components");
|
|
@@ -1372,23 +1369,20 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1372
1369
|
};
|
|
1373
1370
|
const registerTool = server.registerTool.bind(server);
|
|
1374
1371
|
server.registerTool = ((name, config, callback) => hiddenTools.has(name) ? undefined : registerTool(name, config, async (...args) => {
|
|
1375
|
-
// Claude Code CLI never advertises
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
|
|
1382
|
-
// root/store out from under it, so that case is refused rather than risked.
|
|
1383
|
-
const cwdHint = extractCwdHint(args[0]);
|
|
1372
|
+
// The Claude Code CLI never advertises roots/list_changed for an agent `cd` or worktree
|
|
1373
|
+
// entry, so the cached root never moves and writes land where the process was spawned.
|
|
1374
|
+
// Write tools therefore accept an optional `cwd` (see cwdHintField) as a client-agnostic
|
|
1375
|
+
// fallback, resolved fresh on every call rather than cached, re-homing this stdio
|
|
1376
|
+
// process the way a roots notification would. That is only safe when no other request
|
|
1377
|
+
// is in flight, so that case is refused instead of risked.
|
|
1378
|
+
const cwdHint = cwdHintOf(args[0]);
|
|
1384
1379
|
// A pinned root is the whole point of `hunch mcp --root`: a served partition must not
|
|
1385
1380
|
// follow the caller's working directory into some other checkout.
|
|
1386
1381
|
if (cwdHint !== undefined && !pinned) {
|
|
1387
1382
|
const target = canonicalRootPath(findRoot(cwdHint));
|
|
1388
1383
|
if (target !== canonicalRootPath(root)) {
|
|
1389
|
-
if (activeRequests) {
|
|
1390
|
-
return err(`Hunch is
|
|
1391
|
-
`(resolves to ${target}) while another call is in flight. Retry this call once the other one completes.`);
|
|
1384
|
+
if (activeRequests !== 0) {
|
|
1385
|
+
return err(`Hunch is serving ${root} and cannot safely switch to your working directory (${target}) while another call is in flight. Retry once the other call completes.`);
|
|
1392
1386
|
}
|
|
1393
1387
|
try {
|
|
1394
1388
|
setRoot(cwdHint);
|
|
@@ -1923,13 +1917,13 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1923
1917
|
// -- hunch_now (the hot view: recent activity + roadmap) --------------------
|
|
1924
1918
|
// PUBLIC store only, per dec_29eff08c69's jurisdiction rule: an assistant may
|
|
1925
1919
|
// paste this anywhere, so it must be publishable by construction. Union view
|
|
1926
|
-
//
|
|
1927
|
-
// queued commit-repair's liveness is checked against the full store
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
1920
|
+
// is available only through the local terminal command `hunch now --private`.
|
|
1921
|
+
// Exception: a queued commit-repair's liveness is checked against the full store, so a
|
|
1922
|
+
// private-overlay decision's id and commit shas (never its title) may appear in the
|
|
1923
|
+
// escalation line.
|
|
1930
1924
|
server.registerTool("hunch_now", {
|
|
1931
1925
|
title: "Recent activity + the roadmap (the hot view)",
|
|
1932
|
-
description: "What just happened and what
|
|
1926
|
+
description: "What just happened and what is 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. Same data as the wiki's now.md. Public store only — except that a queued commit-repair's liveness is checked against the full store, so a private-overlay decision's answerable repair does not go silently unanswerable; only its id and commit shas ever surface, never its title.",
|
|
1933
1927
|
inputSchema: {
|
|
1934
1928
|
recent_limit: z.number().optional().describe("How many recent decisions to include (default 5)."),
|
|
1935
1929
|
roadmap_limit: z.number().optional().describe("How many roadmap entries to include (default 2); the rest are counted."),
|
|
@@ -1962,18 +1956,15 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1962
1956
|
L.push("", ws);
|
|
1963
1957
|
const escalations = pendingEscalations(store.advisoryRecs("decisions"));
|
|
1964
1958
|
escalations.push(...premiseEscalations(store.advisoryRecs("decisions"), { now: new Date().toISOString(), exists: (p) => existsSync(join(root, p)) }));
|
|
1965
|
-
//
|
|
1966
|
-
//
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
const actionableNow = actionableEscalations(escalations);
|
|
1975
|
-
if (actionableNow.length) {
|
|
1976
|
-
L.push("", `⚖ ${actionableNow.length} decision(s) need the human's call — ASK inline (never queue): ${actionableNow.map((e) => e.question).join(" · ")}`);
|
|
1959
|
+
// Liveness is checked against the FULL store (the title stays scoped to advisory
|
|
1960
|
+
// records), so an overlay decision's repair does not go silent.
|
|
1961
|
+
const repairQueue = readActivePendingRepairs(root);
|
|
1962
|
+
escalations.push(...commitRepairEscalations(repairQueue, store.advisoryRecs("decisions"), store.recs("decisions"), withheldRewrites(root, repairQueue)));
|
|
1963
|
+
// A duplicate-id commit-repair follower whose own answer is "act on a different
|
|
1964
|
+
// entry first" is not a question to raise (#61).
|
|
1965
|
+
const actionable = actionableEscalations(escalations);
|
|
1966
|
+
if (actionable.length) {
|
|
1967
|
+
L.push("", `⚖ ${actionable.length} decision(s) need the human's call — ASK inline (never queue): ${actionable.map((e) => e.question).join(" · ")}`);
|
|
1977
1968
|
}
|
|
1978
1969
|
return ok(L.join("\n"));
|
|
1979
1970
|
});
|
|
@@ -2039,24 +2030,20 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2039
2030
|
// Premise decay: a live decision whose recorded reason died. Question-framed
|
|
2040
2031
|
// like every entry here — authority never changes until the human answers.
|
|
2041
2032
|
items.push(...premiseEscalations(store.advisoryRecs("decisions"), { now: new Date().toISOString(), exists: (p) => existsSync(join(root, p)) }));
|
|
2042
|
-
//
|
|
2043
|
-
const
|
|
2044
|
-
items.push(...commitRepairEscalations(
|
|
2033
|
+
// Live set is the full store, for the reason given in the sibling hunch_now handler.
|
|
2034
|
+
const repairQueue = readActivePendingRepairs(root);
|
|
2035
|
+
items.push(...commitRepairEscalations(repairQueue, store.advisoryRecs("decisions"), store.recs("decisions"), withheldRewrites(root, repairQueue)));
|
|
2045
2036
|
try {
|
|
2046
2037
|
items.push(...policyEscalations(new ConstitutionService(store, root).list({ publicOnly: true }).map((p) => ({ ...p, last_action: p.audit.at(-1)?.action ?? null }))));
|
|
2047
2038
|
}
|
|
2048
2039
|
catch { /* constitution unavailable — memory escalations still surface */ }
|
|
2049
2040
|
if (!items.length)
|
|
2050
2041
|
return ok("✓ Nothing needs a human decision — memory is auto-trusted and self-consistent.");
|
|
2051
|
-
// Only
|
|
2052
|
-
//
|
|
2053
|
-
// a different entry first" still surfaces below for transparency, but
|
|
2054
|
-
// isn't itself something to raise as a decision (#61).
|
|
2042
|
+
// Only actionable entries are questions to raise; non-actionable followers still
|
|
2043
|
+
// appear below for transparency (#61).
|
|
2055
2044
|
const L = [escalationHeadline(items, "mcp"), ""];
|
|
2056
2045
|
for (const e of items) {
|
|
2057
|
-
//
|
|
2058
|
-
// itself something to raise — an entry whose own answer is "act on a
|
|
2059
|
-
// different entry first" reads as informational, not as a question.
|
|
2046
|
+
// Non-actionable entries are marked distinctly so they read as informational, not as a question.
|
|
2060
2047
|
L.push(`${e.actionable === false ? "·" : "⚖"} ${e.question}`);
|
|
2061
2048
|
L.push(` ${e.detail}`);
|
|
2062
2049
|
L.push(` → ${e.resolution}`, "");
|
|
@@ -2474,7 +2461,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2474
2461
|
const where = decision.private
|
|
2475
2462
|
? ` [PRIVATE overlay — not committed to this repo]${flushed}`
|
|
2476
2463
|
: home === "private" ? ` [SHARED store — one source of truth for the whole team]${flushed}` : flushed;
|
|
2477
|
-
const dest = destinationNote(
|
|
2464
|
+
const dest = destinationNote(destinationRoot(home, store, root));
|
|
2478
2465
|
// The chain (nuryel.state/1): a `shipped` receipt in an organization drawer rests on
|
|
2479
2466
|
// this decision by id + the hash ON FILE + this repository's partition. Hand the ref
|
|
2480
2467
|
// over now so the agent never rests on a pre-store hash or re-derives the scope.
|
|
@@ -2585,7 +2572,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2585
2572
|
const tierNote = `
|
|
2586
2573
|
|
|
2587
2574
|
⚠ ${why} — this rule is agent_recorded TESTIMONY${capped ? ` and was capped from "${requested.severity}" to "${rec.severity}"` : ""}. It IS enforced: the pre-edit hook and CI surface it on every matching edit from now on. What it cannot do is DENY an edit — only a rule a human confirmed outside the agent channel may block. The human confirms it by running: ${confirmCommand(rec.id, { private: home === "private", severity: requested.severity })}`;
|
|
2588
|
-
const dest = destinationNote(
|
|
2575
|
+
const dest = destinationNote(destinationRoot(home, store, root));
|
|
2589
2576
|
return ok(`${existing ? "Updated" : "Recorded"} ${rec.severity} constraint ${rec.id}: "${rec.statement}" (scope: ${rec.scope.join(", ")}).${where}${dest} It now ${enforce}.${reviewNote}${tierNote}`);
|
|
2590
2577
|
}
|
|
2591
2578
|
catch (e) {
|
|
@@ -2666,8 +2653,9 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2666
2653
|
? `\n\n△ violates_constraint ${rec.violates_constraint} resolves to no known constraint — if the rule isn't recorded yet, hunch_record_correction it and re-record this finding with the real id.`
|
|
2667
2654
|
: "";
|
|
2668
2655
|
const noEvidence = rec.evidence.length ? "" : "\n\n△ No evidence attached — a finding without the query/output that produced it is an opinion. Re-record with evidence when you have it.";
|
|
2669
|
-
const dest = destinationNote(
|
|
2670
|
-
|
|
2656
|
+
const dest = destinationNote(destinationRoot(home, store, root));
|
|
2657
|
+
const grounds = [...rec.affected_files, ...rec.affected_symbols].join(", ") || "(nothing — add affected_files/symbols so it surfaces at edit time)";
|
|
2658
|
+
return ok(`${existing ? "Updated" : "Recorded"} finding ${id}: "${rec.title}" (${rec.triage}/${rec.severity}, observed ${rec.observed_at.slice(0, 10)}).${where}${dest} It now grounds edits to: ${grounds}.${danglingCon}${noEvidence}`);
|
|
2671
2659
|
}
|
|
2672
2660
|
catch (e) {
|
|
2673
2661
|
return err(`Failed to record finding: ${e.message}`);
|
|
@@ -2780,7 +2768,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2780
2768
|
// not race the HTTP server between the ledger read and the record write.
|
|
2781
2769
|
const { hunchDir } = stateHomeFor(store, input.scope);
|
|
2782
2770
|
const result = await withWriteLock(hunchDir, () => writeState(store, { schema: STATE_WRITE_VERSION, ...input }, {
|
|
2783
|
-
flush: (isPrivate, message) =>
|
|
2771
|
+
flush: (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
2784
2772
|
}));
|
|
2785
2773
|
return stateResult(`${result.outcome} ${result.record_id} (${result.durability}) ${result.record_hash}`, result);
|
|
2786
2774
|
}
|
|
@@ -2797,7 +2785,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2797
2785
|
try {
|
|
2798
2786
|
const { hunchDir } = stateHomeFor(store, input.scope);
|
|
2799
2787
|
const result = await withWriteLock(hunchDir, () => captureState(store, { schema: STATE_CAPTURE_VERSION, ...input }, {
|
|
2800
|
-
flush: (isPrivate, message) =>
|
|
2788
|
+
flush: (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
2801
2789
|
}));
|
|
2802
2790
|
return stateResult(`${result.outcome} observation ${result.record_id} (${result.durability}); this does not assert currentness. ${result.record_hash}`, result);
|
|
2803
2791
|
}
|
|
@@ -2814,7 +2802,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2814
2802
|
try {
|
|
2815
2803
|
const { hunchDir } = stateHomeFor(store, input.scope);
|
|
2816
2804
|
const result = await withWriteLock(hunchDir, () => captureBatchState(store, { schema: STATE_CAPTURE_BATCH_VERSION, ...input }, {
|
|
2817
|
-
flush: (isPrivate, message) =>
|
|
2805
|
+
flush: (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
2818
2806
|
}));
|
|
2819
2807
|
return stateResult(`Capture batch: ${result.results.filter(r => r.status === "saved").length} saved/replayed, ${result.results.filter(r => r.status === "refused").length} refused.${result.reviews ? ` Reviews: ${result.reviews.filter(r => r.status === "saved").length} withdrawn/replayed, ${result.reviews.filter(r => r.status === "refused").length} refused.` : ''} Inspect each indexed result.`, result);
|
|
2820
2808
|
}
|
|
@@ -2950,7 +2938,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2950
2938
|
if (artifactHome === "private") {
|
|
2951
2939
|
flushMemoryHome(store, hunchPaths(root).hunch, "private", `hunch: prove correction ${constraint_id}`, startupTeamRoute ?? undefined);
|
|
2952
2940
|
}
|
|
2953
|
-
const destRoot =
|
|
2941
|
+
const destRoot = destinationRoot(artifactHome, store, root);
|
|
2954
2942
|
const destination = { root: destRoot, branch: currentBranch(destRoot) };
|
|
2955
2943
|
if (include_artifacts)
|
|
2956
2944
|
return ok(JSON.stringify({ ...upgrade, destination }, null, 2));
|
|
@@ -3139,7 +3127,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
3139
3127
|
if (!home)
|
|
3140
3128
|
throw new Error(`policy ${policy_id} has no exact storage home`);
|
|
3141
3129
|
flushMemoryHome(store, hunchPaths(root).hunch, home, `hunch: plan policy ${policy_id}`, startupTeamRoute ?? undefined);
|
|
3142
|
-
const destRoot =
|
|
3130
|
+
const destRoot = destinationRoot(home, store, root);
|
|
3143
3131
|
return ok(JSON.stringify({ ...plan, destination: { root: destRoot, branch: currentBranch(destRoot) } }, null, 2));
|
|
3144
3132
|
}
|
|
3145
3133
|
catch (e) {
|
|
@@ -3437,7 +3425,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
3437
3425
|
dependencyTimeoutMs: dependency_timeout_ms ?? 300_000,
|
|
3438
3426
|
});
|
|
3439
3427
|
flushMemoryHome(store, hunchPaths(root).hunch, "private", "hunch: materialize G2 behavior policies", startupTeamRoute ?? undefined);
|
|
3440
|
-
const destRoot =
|
|
3428
|
+
const destRoot = destinationRoot("private", store, root);
|
|
3441
3429
|
return ok(JSON.stringify({ ...materialized, destination: { root: destRoot, branch: currentBranch(destRoot) } }, null, 2));
|
|
3442
3430
|
}
|
|
3443
3431
|
catch (e) {
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { verificationLauncherFor } from "../core/verifyLauncher.js";
|
|
3
3
|
import { TaskIdSchema, ReportClaimSchema, LessonReferenceSchema, finishReportTask, listReportTasks, readTaskReport, readLessonHistory, recordReportClaim, reportPresentationEnabled, startReportTask } from "../core/taskReport.js";
|
|
4
4
|
import { persistTaskRecord } from "../core/taskRecord.js";
|
|
5
|
-
import {
|
|
5
|
+
import { currentSource, runReportConformance } from "../core/taskReportEvidence.js";
|
|
6
6
|
import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.js";
|
|
7
7
|
function applicationReferences(report) {
|
|
8
8
|
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);
|
|
@@ -85,7 +85,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
|
|
|
85
85
|
// from the model must not fork a second report or fail the start.
|
|
86
86
|
if (!task_id || !/different title/.test(error.message))
|
|
87
87
|
throw error;
|
|
88
|
-
task = readTaskReport(root, task_id,
|
|
88
|
+
task = readTaskReport(root, task_id, currentSource(root)).task;
|
|
89
89
|
}
|
|
90
90
|
const launcher = verificationLauncher();
|
|
91
91
|
// Compact by design (#370): the rules an agent needs to act, and exact
|
|
@@ -120,7 +120,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
|
|
|
120
120
|
catch (error) {
|
|
121
121
|
graphNote = `\nGraph not saved: ${error.message}`;
|
|
122
122
|
}
|
|
123
|
-
const report = readTaskReport(root, task_id,
|
|
123
|
+
const report = readTaskReport(root, task_id, currentSource(root));
|
|
124
124
|
const show = reportPresentationEnabled(root);
|
|
125
125
|
// The HTML evidence view is rendered on demand (hunch_report(html: true),
|
|
126
126
|
// `hunch report <id> --html`, or the VS Code view); finish writes no file.
|
|
@@ -165,7 +165,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
|
|
|
165
165
|
const tasks = listReportTasks(root);
|
|
166
166
|
return { content: [{ type: "text", text: tasks.length ? tasks.map(t => `${t.task_id} · ${t.state} · ${t.title}`).join("\n") : "No task reports yet. Start with hunch_task." }], structuredContent: { tasks } };
|
|
167
167
|
}
|
|
168
|
-
const report = readTaskReport(root, task_id,
|
|
168
|
+
const report = readTaskReport(root, task_id, currentSource(root));
|
|
169
169
|
const file = html ? writeTaskReportHtml(root, task_id) : null;
|
|
170
170
|
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 } };
|
|
171
171
|
}
|
package/dist/serve/app.js
CHANGED
|
@@ -23,7 +23,7 @@ import { STATE_PROOF_CAPABILITY } from '../core/stateProof.js';
|
|
|
23
23
|
import { createServer } from "node:http";
|
|
24
24
|
import { HunchStore } from "../store/hunchStore.js";
|
|
25
25
|
import { hunchPaths } from "../core/paths.js";
|
|
26
|
-
import {
|
|
26
|
+
import { flushStateWrite } from "../integrations/sync.js";
|
|
27
27
|
import { StateRefusal, capabilities, mergeReadResponses, readState, recordsState, stateHomeFor, subscribeState, writeState } from "../store/stateBinding.js";
|
|
28
28
|
import { STATE_READ_VERSION, STATE_RECORDS_VERSION, STATE_SUBSCRIBE_VERSION, STATE_WRITE_VERSION, ReadScopesSchema, ScopeSchema, scopePath } from "../core/stateContract.js";
|
|
29
29
|
import { partitionFor, resolveCredential, readServeConfig } from "./config.js";
|
|
@@ -174,7 +174,7 @@ export function createServeApp(config, opts = {}) {
|
|
|
174
174
|
// Only authenticated grants select stores for cross-partition source visibility.
|
|
175
175
|
const accessOptions = { additionalStores: principal.grants.map(grant => requestStore(grant).store) };
|
|
176
176
|
const { store, root } = requestStore(scope);
|
|
177
|
-
const flush = (isPrivate, message) =>
|
|
177
|
+
const flush = (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message);
|
|
178
178
|
if (route === "read") {
|
|
179
179
|
if (body.observed_page !== undefined && body.scopes !== undefined)
|
|
180
180
|
throw problem(400, 'malformed', 'observation pages require a single partition without scopes');
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -1341,8 +1341,8 @@ export class HunchStore {
|
|
|
1341
1341
|
* instant — "what did we believe as of commit X?". Omit `asOf` for the full,
|
|
1342
1342
|
* history-inclusive view (backward-compatible default). */
|
|
1343
1343
|
why(target, opts = {}) {
|
|
1344
|
-
// An
|
|
1345
|
-
//
|
|
1344
|
+
// An agent's verbatim edit-payload path is absolute; stored paths are
|
|
1345
|
+
// repo-relative, so rewrite before any matching (issue #299).
|
|
1346
1346
|
target = repoRelativeTarget(target, this.paths.root);
|
|
1347
1347
|
// A record id (every delivered brief line ends in `hunch_why("dec_…")`) resolves
|
|
1348
1348
|
// to that record directly (issue #474). With `asOf`, a record not in force then
|
|
@@ -1364,11 +1364,11 @@ export class HunchStore {
|
|
|
1364
1364
|
// so an unanchored suffix pulled unrelated files' records into why()/the
|
|
1365
1365
|
// pre-edit grounding block (issue #32). Segment-anchored matching only.
|
|
1366
1366
|
//
|
|
1367
|
-
// A target that
|
|
1368
|
-
// vscode-extension/index.ts
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1367
|
+
// A target that is a path the index already knows (README.md,
|
|
1368
|
+
// `vscode-extension/index.ts`) names exactly ONE file. Suffix-matching it too
|
|
1369
|
+
// would pull in every other file sharing its basename (issue #299: root
|
|
1370
|
+
// `index.ts` received `vscode-extension/index.ts`'s rules and vice versa). "Known
|
|
1371
|
+
// to the index" is answered from already-loaded graph data, looked up
|
|
1372
1372
|
// first (a symbol's exact file, or an indexed component's path glob — so a
|
|
1373
1373
|
// symbol-less indexed file still counts as real), and only then from the
|
|
1374
1374
|
// working tree, for a real file the index cannot see at all (issue #334).
|
|
@@ -1696,9 +1696,8 @@ export class HunchStore {
|
|
|
1696
1696
|
* longer enforced. Pass `{ asOf }` to instead return the invariants in force at
|
|
1697
1697
|
* that instant (time-travel: "what must I not have broken as of commit X?"). */
|
|
1698
1698
|
checkConstraints(scope, opts = {}) {
|
|
1699
|
-
//
|
|
1700
|
-
//
|
|
1701
|
-
// blocking rule plainly applied (issue #296).
|
|
1699
|
+
// Scopes are repo-relative; an absolute target matched nothing even when a
|
|
1700
|
+
// blocking rule applied (issue #296).
|
|
1702
1701
|
scope = repoRelativeTarget(scope, this.paths.root);
|
|
1703
1702
|
const all = this.recs("constraints");
|
|
1704
1703
|
const asOf = opts.asOf;
|
|
@@ -1713,11 +1712,8 @@ export class HunchStore {
|
|
|
1713
1712
|
* glob, and the queried scope may be either too. Advisory only — findings never
|
|
1714
1713
|
* enter any block path. Sorted worst-first, then id for stable output. */
|
|
1715
1714
|
liveFindingsFor(scope) {
|
|
1716
|
-
// Same absolute-target and same-basename-leak gaps as why() (issue #299):
|
|
1717
|
-
//
|
|
1718
|
-
// below, and an un-guarded suffix fallback could pull in an unrelated file
|
|
1719
|
-
// that merely shares a basename. Normalize once and reuse the same "is this
|
|
1720
|
-
// path known to the index" guard.
|
|
1715
|
+
// Same absolute-target and same-basename-leak gaps as why() (issue #299):
|
|
1716
|
+
// normalize once and reuse the known-path guard.
|
|
1721
1717
|
const t = repoRelativeTarget(scope, this.paths.root);
|
|
1722
1718
|
const indexed = this.isKnownPath(t);
|
|
1723
1719
|
const live = (f) => f.triage === "open" || f.triage === "accepted-risk" || f.triage === "scheduled";
|
|
@@ -2340,9 +2336,7 @@ export class HunchStore {
|
|
|
2340
2336
|
* a task on `target`, ordered by what matters most — invariants first, then the
|
|
2341
2337
|
* why, then blast radius and bug history — trimmed to a rough token budget. */
|
|
2342
2338
|
assembleContext(target, budget = 1500, opts = {}) {
|
|
2343
|
-
//
|
|
2344
|
-
// repo-relative BEFORE it reaches why()/liveFindingsFor() below, or an
|
|
2345
|
-
// absolute path never matches the repo-relative stored data (issue #299).
|
|
2339
|
+
// Normalize before it reaches why()/liveFindingsFor() below (issue #299).
|
|
2346
2340
|
target = repoRelativeTarget(target, this.paths.root);
|
|
2347
2341
|
const w = this.why(target, opts);
|
|
2348
2342
|
const symIds = w.symbols.map((s) => s.id);
|
|
@@ -111,8 +111,9 @@ export interface SynthProvider {
|
|
|
111
111
|
}
|
|
112
112
|
/** Every selectable synthesis mode. `auto` is a preference value rather than a
|
|
113
113
|
* provider: it resolves the invocation's origin without choosing by availability.
|
|
114
|
-
* "openai-compat" is the opt-in local
|
|
115
|
-
* LM Studio
|
|
114
|
+
* "openai-compat" is the opt-in local or self-hosted HTTP provider (Ollama, vLLM,
|
|
115
|
+
* LM Studio and so on). It is not a subscription, but it can be selected
|
|
116
|
+
* explicitly like one. */
|
|
116
117
|
export declare const SYNTH_PROVIDER_NAMES: readonly ["claude-cli", "codex-cli", "cursor-agent", "kimi-cli", "openai-compat", "deterministic"];
|
|
117
118
|
export declare const SYNTH_PREFERENCES: readonly ["auto", "claude-cli", "codex-cli", "cursor-agent", "kimi-cli", "openai-compat", "deterministic"];
|
|
118
119
|
export type SynthProviderName = (typeof SYNTH_PROVIDER_NAMES)[number];
|
|
@@ -172,8 +173,11 @@ export declare function safeModel(v: string | undefined, fallback: undefined): s
|
|
|
172
173
|
* the explicit skip flag preserves that neutral-cwd isolation without loading a
|
|
173
174
|
* target repo's agent rules or MCP configuration. */
|
|
174
175
|
export declare function codexExecArgs(model?: string): string[];
|
|
176
|
+
/** Sanitize HUNCH_SYNTH_TIMEOUT_MS: a finite number > 0, else `fallback`. */
|
|
175
177
|
export declare function safeTimeout(v: string | undefined, fallback: number): number;
|
|
178
|
+
/** Sanitize HUNCH_SYNTH_MAX_TOKENS: a finite number > 0, else `fallback`. */
|
|
176
179
|
export declare function safeMaxTokens(v: string | undefined, fallback: number): number;
|
|
180
|
+
/** True only when the user explicitly opted in to public (possibly metered) hosts. */
|
|
177
181
|
export declare function meteredHostsAllowed(env?: NodeJS.ProcessEnv): boolean;
|
|
178
182
|
export declare class OpenAICompatProvider extends PromptSynthProvider {
|
|
179
183
|
readonly name = "openai-compat";
|
|
@@ -182,9 +186,10 @@ export declare class OpenAICompatProvider extends PromptSynthProvider {
|
|
|
182
186
|
}
|
|
183
187
|
/** Best-effort: does the configured openai-compat endpoint look like Ollama with
|
|
184
188
|
* an UNSET num_ctx? Returns
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
189
|
+
* a warning string when the model does not pin num_ctx, or null when the
|
|
190
|
+
* endpoint is unreachable, does not look like Ollama's /api/show response shape,
|
|
191
|
+
* or already has num_ctx set. Diagnostics only: it never throws and never blocks.
|
|
192
|
+
* Deliberately does NOT try to
|
|
188
193
|
* report the model's effective context length: modern Ollama defaults may come
|
|
189
194
|
* from server configuration or VRAM tiers, and model_info keys are not a stable
|
|
190
195
|
* parse target. We therefore report only the observed fact — whether num_ctx is
|
|
@@ -203,19 +208,12 @@ export declare class DeterministicProvider implements SynthProvider {
|
|
|
203
208
|
* tagged. If nothing parses, hand the raw output to the mapper (→ it finds the JSON
|
|
204
209
|
* draft or throws → deterministic fallback). Tolerant by design: drift degrades, never crashes. */
|
|
205
210
|
export declare function extractCodexText(out: string): string;
|
|
206
|
-
/** Test-only:
|
|
207
|
-
*
|
|
208
|
-
* cached by an earlier call in the same process. Never call from production code. */
|
|
211
|
+
/** Test-only: empty the availability cache so a test that changes env vars
|
|
212
|
+
* mid-process is not served a stale cached result. */
|
|
209
213
|
export declare function __resetAvailabilityCacheForTests(): void;
|
|
210
|
-
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* users reach for that name first. Applied to the HUNCH_SYNTH_PROVIDER env var in
|
|
214
|
-
* resolveSynthesisProvider below, and exported so the `hunch provider <name>` CLI
|
|
215
|
-
* command (index.ts) normalizes it the same way before validating/persisting a
|
|
216
|
-
* local preference — the two paths must agree, or a user who sets one and reads
|
|
217
|
-
* the other back gets a confusing "unknown provider" message for a name that
|
|
218
|
-
* actually works. */
|
|
214
|
+
/** The provider is not Ollama-specific, but Ollama is the name users try first, so
|
|
215
|
+
* "ollama" is an alias for "openai-compat". The env var and `hunch provider <name>`
|
|
216
|
+
* both normalize through here so the two paths agree. */
|
|
219
217
|
export declare function normalizeProviderName(v: string | undefined): string | undefined;
|
|
220
218
|
/** Read a per-user, gitignored choice. Invalid/missing local state is treated as auto;
|
|
221
219
|
* `writeSynthesisPreference` refuses to overwrite malformed data so this forgiveness
|
|
@@ -258,10 +256,10 @@ export declare class EnsembleProvider implements SynthProvider {
|
|
|
258
256
|
export declare function selectEnsemble(opts?: ProviderSelectionOptions & {
|
|
259
257
|
samples?: number;
|
|
260
258
|
}): Promise<EnsembleProvider | null>;
|
|
261
|
-
/** Pick
|
|
262
|
-
* synthesis would use (subscription CLI or the opt-in openai-compat endpoint),
|
|
263
|
-
*
|
|
264
|
-
* resolves to the deterministic fallback
|
|
259
|
+
/** Pick the provider for the Critic pass: the same resolved provider normal
|
|
260
|
+
* synthesis would use (a subscription CLI or the opt-in openai-compat endpoint),
|
|
261
|
+
* under the same env / local-preference / auto policy. Returns null when that
|
|
262
|
+
* resolves to the deterministic fallback, in which case verification does nothing. The
|
|
265
263
|
* un-audited draft stands (graceful degradation; dec_18a81c8291). */
|
|
266
264
|
export declare function selectVerifier(opts?: ProviderSelectionOptions): Promise<SynthProvider | null>;
|
|
267
265
|
/** Map model text → RelevanceVerdict, or null when nothing usable parses (→ the
|