@davesheffer/hunch 1.44.0 → 1.45.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 +6 -16
- package/dist/cli/index.js +153 -70
- package/dist/core/agenthook.d.ts +1 -0
- package/dist/core/agenthook.js +11 -0
- package/dist/core/escalations.d.ts +20 -0
- package/dist/core/escalations.js +47 -0
- package/dist/core/hookText.js +1 -1
- package/dist/integrations/claudemd.js +1 -1
- package/dist/integrations/gitignore.d.ts +16 -0
- package/dist/integrations/gitignore.js +30 -0
- package/dist/integrations/providers.d.ts +9 -0
- package/dist/integrations/providers.js +31 -2
- package/dist/integrations/scaffold.js +1 -1
- package/dist/mcp/leanToolList.d.ts +9 -0
- package/dist/mcp/leanToolList.js +89 -0
- package/dist/mcp/server.js +4 -2
- package/dist/serve/mcpHttp.js +2 -0
- package/package.json +2 -5
- package/server.json +2 -2
- package/tooling/md1-benchmark.mjs +1 -1
- package/tooling/competitive-watch.mjs +0 -142
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ hunch task list
|
|
|
127
127
|
hunch report <task-id> --html
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
A delivered lesson, an agent's claim, and a passing test are different evidence. Hunch keeps them separate. Automatic presentation depends on the host following the task lifecycle; missing evidence stays unverified.
|
|
130
|
+
A delivered lesson, an agent's claim, and a passing test are different evidence. Hunch keeps them separate. Automatic presentation depends on the host following the task lifecycle; missing evidence stays unverified.
|
|
131
131
|
|
|
132
132
|
## Share one living repository memory with your team
|
|
133
133
|
|
|
@@ -152,19 +152,19 @@ Hunch ships `hunch serve`: a self-hosted HTTP service for organization, team, us
|
|
|
152
152
|
|
|
153
153
|
Open `/operator` on your server to inspect current records, completed work and commitments in a read-only browser view. Writer-supplied citations can point to an exact summary field or text passage and its recorded sources. They show traceability; they do not prove that a source supports a claim.
|
|
154
154
|
|
|
155
|
-
Agents can use the same contract through MCP, `hunch state read|write|records|subscribe`, the `@davesheffer/hunch/state` TypeScript client, or the
|
|
155
|
+
Agents can use the same contract through MCP, `hunch state read|write|records|subscribe`, the `@davesheffer/hunch/state` TypeScript client, or the Python client. The Python package is built and tested from this repository; it is not yet published to PyPI. Scoped conventions let a person record sourced user, team or organization preferences. Those preferences remain advisory and do not silently become blocking rules.
|
|
156
156
|
|
|
157
157
|
Records can describe decisions, action outcomes, commitments, entities, relationships, and summaries that name their dependencies. Actions retain their status, including unknown or unverified outcomes. Repeated writes have stable identities, conflicting current decisions are refused, and confirmed human records receive protections against agent overwrites. These are defined checks on structured records; Hunch cannot establish every fact in the outside world on its own.
|
|
158
158
|
|
|
159
159
|
This is what **deterministic state** means here: explicit rules govern the stored record, rather than having each agent reconstruct it from scratch. Git holds the durable data; SQLite is a rebuildable index. The server binds to loopback and requires deployment and agent integration by its operator. Hunch does not provide a managed CRM or email connector service.
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
See the [full documentation](https://www.hunchmemory.com/docs).
|
|
162
162
|
|
|
163
163
|
### The vision, and what is still being tested
|
|
164
164
|
|
|
165
165
|
The vision is continuity across people, tools, and agents: an operations agent records a customer issue, a coding agent finds the relevant decision and fixes the cause, and the operations agent closes the commitment using evidence of the fix.
|
|
166
166
|
|
|
167
|
-
The underlying memory and state tools ship today. The broader claim—that different agents reliably use that shared state and avoid contradictory work—is still being measured
|
|
167
|
+
The underlying memory and state tools ship today. The broader claim—that different agents reliably use that shared state and avoid contradictory work—is still being measured.
|
|
168
168
|
|
|
169
169
|
**Hunch remains the product name.** The `nuryel.state/1` protocol and `nuryel_*` tool names are existing technical identifiers; a possible future rename is undecided.
|
|
170
170
|
|
|
@@ -178,7 +178,7 @@ hunch dna context
|
|
|
178
178
|
hunch dna diff <older-ref> <newer-ref>
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
Profiles retain their revision, sources, confidence, and freshness.
|
|
181
|
+
Profiles retain their revision, sources, confidence, and freshness.
|
|
182
182
|
|
|
183
183
|
## Your data and your authority
|
|
184
184
|
|
|
@@ -192,18 +192,8 @@ Profiles retain their revision, sources, confidence, and freshness. [Project DNA
|
|
|
192
192
|
|
|
193
193
|
- [Full documentation](https://www.hunchmemory.com/docs)
|
|
194
194
|
- [Copy-paste cookbook](https://www.hunchmemory.com/cookbook)
|
|
195
|
-
- [
|
|
196
|
-
- [Deterministic organizational state](docs/deterministic-state.md)
|
|
197
|
-
- [Project DNA](docs/project-dna.md)
|
|
198
|
-
- [Native change proof](docs/change-proof.md)
|
|
199
|
-
- [Engineering Landscape](docs/engineering-landscape.md)
|
|
200
|
-
- [Review memory](docs/review-memory.md)
|
|
201
|
-
- [Agent-origin handling](docs/agent-origin.md)
|
|
202
|
-
- [Autonomy ladder](docs/autonomy-ladder.md)
|
|
203
|
-
- [Autonomous development](https://github.com/davesheffer/hunch-private/blob/main/projects/hunch/docs/autonomous-development.md)
|
|
204
|
-
- [Changelog](CHANGELOG.md) · [Roadmap](ROADMAP.md)
|
|
195
|
+
- [Changelog](CHANGELOG.md)
|
|
205
196
|
- [VS Code extension](vscode-extension/README.md)
|
|
206
|
-
- [Architecture benchmark](bench/architectural-conformance.md)
|
|
207
197
|
- [Contributing](CONTRIBUTING.md)
|
|
208
198
|
|
|
209
199
|
Apache-2.0
|
package/dist/cli/index.js
CHANGED
|
@@ -60,7 +60,7 @@ import { installPostCommitHook, installPreCommitHook, installPostMergeHook, inst
|
|
|
60
60
|
import { ensureSharedOverlayPointer } from "../integrations/worktree.js";
|
|
61
61
|
import { flushCapture, flushMemoryHome, flushMemoryHomes, pinSharedRemote, sharedRemoteFor } from "../integrations/sync.js";
|
|
62
62
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
63
|
-
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS, upgradeManagedGitignore, describeGitignoreUpgrade } from "../integrations/gitignore.js";
|
|
63
|
+
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS, upgradeManagedGitignore, describeGitignoreUpgrade, memoryIgnoreRule, describeMemoryIgnoreRule } from "../integrations/gitignore.js";
|
|
64
64
|
import { writeCiWorkflow } from "../integrations/ciAction.js";
|
|
65
65
|
import { applyUninstall, formatUninstallPlan, isOptedOut, OPT_OUT_FILE, optOutMessage, planUninstall } from "../integrations/uninstall.js";
|
|
66
66
|
import { updateClaudeMd, renderHunchSection } from "../integrations/claudemd.js";
|
|
@@ -69,7 +69,7 @@ import { measureFootprint } from "../core/footprint.js";
|
|
|
69
69
|
import { classifyGroundingBlock, describeGroundingFreshness } from "../core/groundingLag.js";
|
|
70
70
|
import { mergeGroundingFile } from "../core/groundingMerge.js";
|
|
71
71
|
import { writeMcpJson, writeSlashCommands, installClaudeHooks } from "../integrations/scaffold.js";
|
|
72
|
-
import { scaffoldProviders, regenerateGrounding, refreshExistingGrounding, refreshCommittableGrounding, GROUNDING_DOC_PATHS } from "../integrations/providers.js";
|
|
72
|
+
import { scaffoldProviders, detectAssistants, PROVIDER_ASSISTANTS, regenerateGrounding, refreshExistingGrounding, refreshCommittableGrounding, GROUNDING_DOC_PATHS } from "../integrations/providers.js";
|
|
73
73
|
import { healClaudeConfigCaseSplit } from "../integrations/claudeConfig.js";
|
|
74
74
|
import { formatSearchHit, formatStructure } from "../core/format.js";
|
|
75
75
|
import { isStateKind, renderStateLine, stateSupplements } from "../core/stateDelivery.js";
|
|
@@ -94,7 +94,7 @@ import { isHumanConfirmed } from "../core/strictgate.js";
|
|
|
94
94
|
import { appendEvent, readEvents } from "../core/events.js";
|
|
95
95
|
import { computeStats, formatStats } from "../core/stats.js";
|
|
96
96
|
import { clearTaskSelection, injectionMode, loadTaskSelection, resetSessionInjections, saveTaskSelection, taskSelectionEnabled } from "../core/hookcache.js";
|
|
97
|
-
import { isBareFollowUp, isFilterableSelectionId, liveSelectionRecords, selectForTask } from "../core/taskSelection.js";
|
|
97
|
+
import { isBareFollowUp, isFilterableSelectionId, liveSelectionRecords, promptPaths, selectForTask } from "../core/taskSelection.js";
|
|
98
98
|
import { recordServed, servedSummary } from "../core/served.js";
|
|
99
99
|
import { readTaskReport, recordTaskDelivery, reportActivity, reportHash, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
|
|
100
100
|
import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
|
|
@@ -102,7 +102,7 @@ import { renderRecalledLine } from "../core/taskReportRender.js";
|
|
|
102
102
|
import { closeHookTask, hookReportTaskId, isNotificationPrompt, nativeHookCwd, settleHookSession, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
103
103
|
import { persistTaskRecord } from "../core/taskRecord.js";
|
|
104
104
|
import { recordHookObservation } from "../core/hookObservations.js";
|
|
105
|
-
import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
|
|
105
|
+
import { contextHookOutput, deliversPromptEvent, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
|
|
106
106
|
import { PIPELINE_LOOP, armExecutionObligations, beforeEditProbeVerdict, compileExecutableProbes, environmentExecutableProbes, environmentExecutionObligations, executionObligationBrief, isProductPath, loadPipelineState, onCommand, onEdit, onPrompt, onSkill, pipelineEnabled, proofCheckpoint, savePipelineState, stopVerdict, onLessonsDelivered, lessonReminder, unverifiedNag, } from "../core/pipeline.js";
|
|
107
107
|
import { draftDuplicateOf, isAcceptedDuplicateAnchor } from "../core/dupdetect.js";
|
|
108
108
|
import { planAutoReview, planMutations } from "../core/autoreview.js";
|
|
@@ -118,7 +118,7 @@ import { countersignConstraint, countersignDecision } from "../core/countersign.
|
|
|
118
118
|
import { applyImportedAdrReview, carryImportedAdrReview, importedAdrReviewHash, importedAdrSourceHash, isImportedAdrDecision, pendingImportedAdrReviews } from "../core/importReview.js";
|
|
119
119
|
import { exportMadrCorpus, isRegenerableMadr } from "../integrations/madrExport.js";
|
|
120
120
|
import { buildMadrManifest, writeMadrManifest, refreshMadrCorpus } from "../integrations/madrManifest.js";
|
|
121
|
-
import { pendingEscalations, policyEscalations, commitRepairEscalations, actionableEscalations, escalationHeadline } from "../core/escalations.js";
|
|
121
|
+
import { pendingEscalations, policyEscalations, commitRepairEscalations, actionableEscalations, escalationHeadline, relevantEscalations } from "../core/escalations.js";
|
|
122
122
|
import { premiseEscalations } from "../core/premises.js";
|
|
123
123
|
import { parseDocAnchors, renderDocGrounding } from "../core/docanchors.js";
|
|
124
124
|
import { pathMatchesGlob } from "../core/glob.js";
|
|
@@ -354,6 +354,8 @@ program
|
|
|
354
354
|
.option("--private-sync", "post-commit synthesis writes captured decisions into the overlay repo (HUNCH_PRIVATE_DIR), never the public store")
|
|
355
355
|
.option("--shared-sync", "alias of --private-sync (for teams using one shared overlay repo for any code repo)")
|
|
356
356
|
.option("--no-auto-commit", "DON'T auto-commit captures (default: ON in every mode — the overlay repo is committed+pushed; the public .hunch/ is committed only and rides your next push)")
|
|
357
|
+
.option("--all-assistants", "configure every supported assistant, not only those found on this machine or in this repository")
|
|
358
|
+
.option("--details", "print every setup step and the per-assistant integration status")
|
|
357
359
|
.action((opts) => {
|
|
358
360
|
// Validate --firmness up front, before any side effects (indexing, git hooks,
|
|
359
361
|
// .mcp.json) or opening the store — a bad value must not leave a half-init.
|
|
@@ -367,32 +369,51 @@ program
|
|
|
367
369
|
// teammate/agent resolves the same single source of truth with zero manual setup.
|
|
368
370
|
const { store, teamWired } = openTeamStore(root);
|
|
369
371
|
const inv = resolveInvocation();
|
|
370
|
-
|
|
372
|
+
// Step lines are collected: the default output is a short summary with one next
|
|
373
|
+
// step; `--details` prints every step as written. Warnings always print.
|
|
374
|
+
const lines = [];
|
|
375
|
+
const say = (text) => { lines.push({ text, warn: text.includes("⚠") }); };
|
|
376
|
+
// A hook warning is followed by the snippet to add by hand: keep the group together.
|
|
377
|
+
const sayHook = (group, installed) => {
|
|
378
|
+
const warn = group.some((l) => l.includes("⚠"));
|
|
379
|
+
for (const text of group)
|
|
380
|
+
lines.push({ text, warn });
|
|
381
|
+
if (!warn)
|
|
382
|
+
summary.hooks.push(installed);
|
|
383
|
+
};
|
|
384
|
+
const summary = { index: "", hooks: [], assistants: ["Claude Code"], skipped: [] };
|
|
385
|
+
say(`🧠 Initializing Hunch at ${root}`);
|
|
371
386
|
store.json.ensureDirs(); // stamps the manifest at the current version when fresh
|
|
372
|
-
|
|
387
|
+
say(` ✓ .hunch/ scaffolded (schema v${readManifest(paths).schema_version})`);
|
|
373
388
|
if (teamWired)
|
|
374
|
-
|
|
389
|
+
say(` ✓ connected to the team's shared memory store (from .hunch/team.json) → ${teamWired}`);
|
|
375
390
|
// Exclude the derived SQLite index BEFORE it's written, so the working tree
|
|
376
391
|
// never goes dirty on the MCP server's index writes (which blocks branch
|
|
377
392
|
// switches). The .hunch/*.json graph stays tracked.
|
|
378
393
|
const gi = ensureGitignore(root);
|
|
379
394
|
if (gi.action !== "unchanged")
|
|
380
|
-
|
|
395
|
+
say(` ✓ .gitignore ${gi.action} (Hunch runtime index excluded)`);
|
|
381
396
|
// A repo migrated to a private overlay by an older release: bring its private-only
|
|
382
397
|
// block up to date (and stop publishing newly ignored memory dirs).
|
|
383
398
|
for (const line of describeGitignoreUpgrade(upgradeManagedGitignore(root)))
|
|
384
|
-
|
|
399
|
+
say(` ✓ ${line}`);
|
|
400
|
+
if (!store.privateDir) {
|
|
401
|
+
const ignored = memoryIgnoreRule(root);
|
|
402
|
+
if (ignored)
|
|
403
|
+
say(` ⚠ ${describeMemoryIgnoreRule(ignored)}`);
|
|
404
|
+
}
|
|
385
405
|
if (opts.index !== false) {
|
|
386
406
|
if (isGitRepo(root) && revExists("HEAD", root)) {
|
|
387
407
|
const res = indexRepo(store, root, { source: { kind: "commit", ref: "HEAD" } });
|
|
388
408
|
store.reindex();
|
|
389
|
-
|
|
409
|
+
say(` ✓ indexed committed HEAD (${res.files} files) → ${res.symbols} symbols, ${res.edges} edges, ${res.components} components`);
|
|
410
|
+
summary.index = `indexed committed HEAD (${res.files} files, ${res.symbols} symbols)`;
|
|
390
411
|
for (const item of res.coverage) {
|
|
391
412
|
const reasons = Object.entries(item.reasons).map(([reason, count]) => `${reason}:${count}`).join(", ");
|
|
392
|
-
|
|
413
|
+
say(` coverage ${item.language}: ${item.eligible} eligible · ${item.parsed} parsed · ${item.skipped} skipped${reasons ? ` (${reasons})` : ""}`);
|
|
393
414
|
}
|
|
394
415
|
if (res.skipped)
|
|
395
|
-
|
|
416
|
+
say(` ⚠ ${res.skipped} file(s) could not be parsed (skipped)`);
|
|
396
417
|
}
|
|
397
418
|
else {
|
|
398
419
|
// A checkout-derived graph can be swept into a later memory commit by
|
|
@@ -402,7 +423,7 @@ program
|
|
|
402
423
|
store.json.replaceAll("edges", mergeScannedEdges(store.json.loadAll("edges"), []));
|
|
403
424
|
store.json.replaceAll("components", []);
|
|
404
425
|
store.reindex();
|
|
405
|
-
|
|
426
|
+
say(" ⚠ skipped code graph: no committed HEAD — commit code, then run `hunch index`");
|
|
406
427
|
}
|
|
407
428
|
}
|
|
408
429
|
// Auto-commit is ON by default in every mode; --no-auto-commit persists the opt-out in
|
|
@@ -424,11 +445,11 @@ program
|
|
|
424
445
|
}
|
|
425
446
|
}
|
|
426
447
|
if (existing === null) {
|
|
427
|
-
|
|
448
|
+
say(` ⚠ refusing to rewrite ${localFile}: it is not a JSON object and may hold your private-overlay pointer. Fix or remove it, then re-run \`hunch init --no-auto-commit\`.`);
|
|
428
449
|
}
|
|
429
450
|
else {
|
|
430
451
|
writeFileAtomic(localFile, JSON.stringify({ ...existing, autoCommit: false }, null, 2) + "\n");
|
|
431
|
-
|
|
452
|
+
say(" ✓ local auto-commit preference OFF (an explicit hook --commit flag still takes precedence)");
|
|
432
453
|
}
|
|
433
454
|
}
|
|
434
455
|
// Collected so the linked-worktree note below can say what actually happened
|
|
@@ -446,21 +467,18 @@ program
|
|
|
446
467
|
}
|
|
447
468
|
const h = installPostCommitHook(root, inv.shell, { private: syncToOverlay, commit: hookCommit, localOnly: syncToOverlay });
|
|
448
469
|
installs.push(h);
|
|
449
|
-
|
|
450
|
-
console.log(line);
|
|
470
|
+
sayHook(formatHookInstall(root, "post-commit hook", h, ` (learning loop)${syncToOverlay ? " — syncs to the shared overlay" : ""}${hookCommit ? " — auto-commit on" : ""}`), hookCommit ? "post-commit (auto-commit on)" : "post-commit");
|
|
451
471
|
if (opts.autoCommit === false && hookReport(root).postCommit.flags?.includes("--commit")) {
|
|
452
|
-
|
|
472
|
+
say(" ⚠ the shared post-commit hook still auto-commits. To change it, re-run `hunch init --no-auto-commit` using a global install or the main checkout's Hunch.");
|
|
453
473
|
}
|
|
454
474
|
const pm = installPostMergeHook(root, inv.shell);
|
|
455
475
|
installs.push(pm);
|
|
456
|
-
|
|
457
|
-
console.log(line);
|
|
476
|
+
sayHook(formatHookInstall(root, "post-merge hook", pm, " (squash-merge provenance repair + re-syncs grounding docs after a merge that brought memory in)"), "post-merge");
|
|
458
477
|
const pc = installPostCheckoutHook(root, inv.shell);
|
|
459
478
|
installs.push(pc);
|
|
460
|
-
|
|
461
|
-
console.log(line);
|
|
479
|
+
sayHook(formatHookInstall(root, "post-checkout hook", pc, " (workspace ledger: records this machine's branches + worktrees on checkout)"), "post-checkout");
|
|
462
480
|
const m = installMergeDriver(root, inv.shell);
|
|
463
|
-
|
|
481
|
+
say(` ✓ team merge driver ${m.action}`);
|
|
464
482
|
// Auto-install the pre-commit guard by default (advisory: flags invariants
|
|
465
483
|
// touched directly OR via blast radius, never blocks). Opt out with
|
|
466
484
|
// --no-enforce; --enforce-strict makes blocking near/direct hits fail the commit.
|
|
@@ -468,12 +486,11 @@ program
|
|
|
468
486
|
const strict = !!opts.enforceStrict;
|
|
469
487
|
const p = installPreCommitHook(root, inv.shell, strict);
|
|
470
488
|
installs.push(p);
|
|
471
|
-
|
|
472
|
-
console.log(line);
|
|
489
|
+
sayHook(formatHookInstall(root, "pre-commit constraint guard", p, ` (${strict ? "strict — fails only on direct, high-confidence, non-stale blocking invariants" : "advisory — flags invariants in scope or blast radius"})`), `pre-commit guard (${strict ? "strict" : "advisory"})`);
|
|
473
490
|
}
|
|
474
491
|
}
|
|
475
492
|
else {
|
|
476
|
-
|
|
493
|
+
say(" ⚠ not a git repo — skipped hooks (run `git init` to enable the learning loop)");
|
|
477
494
|
}
|
|
478
495
|
// .mcp.json is the CANONICAL registration: Claude Code resolves it by file path,
|
|
479
496
|
// so it's immune to the Windows ~/.claude.json drive-letter case-split that a
|
|
@@ -481,18 +498,18 @@ program
|
|
|
481
498
|
// existing file is refused (con_8460b6770f) — warn and continue, never clobber.
|
|
482
499
|
try {
|
|
483
500
|
const mcp = writeMcpJson(root, inv.mcp);
|
|
484
|
-
|
|
501
|
+
say(` ✓ wrote ${rel(root, mcp)} (registers the Hunch MCP server — canonical, path-keyed; prefer over a global \`claude mcp add\`)`);
|
|
485
502
|
}
|
|
486
503
|
catch (e) {
|
|
487
|
-
|
|
504
|
+
say(` ⚠ skipped .mcp.json: ${e.message}`);
|
|
488
505
|
}
|
|
489
506
|
const cmds = writeSlashCommands(root);
|
|
490
|
-
|
|
507
|
+
say(` ✓ wrote ${cmds.written.length} slash commands (/hunch-why, /hunch-fix, /hunch-fragile, /capture, /heal, /audit)`);
|
|
491
508
|
for (const s of cmds.skipped) {
|
|
492
|
-
|
|
509
|
+
say(` ⚠ kept your existing ${rel(root, s)} (no hunch:generated marker — delete the file and re-run init to adopt Hunch's version)`);
|
|
493
510
|
}
|
|
494
511
|
const cmd = updateClaudeMd(root, store);
|
|
495
|
-
|
|
512
|
+
say(` ✓ updated ${rel(root, cmd)} with ambient Hunch context`);
|
|
496
513
|
// Firmness: stamp .hunch/config.json (default advisory) so `hunch hook` reads a
|
|
497
514
|
// level even before the user runs `hunch firmness` (--firmness validated above).
|
|
498
515
|
const firmness = writeConfig(paths, opts.firmness ? { firmness: opts.firmness } : {}).firmness;
|
|
@@ -500,18 +517,25 @@ program
|
|
|
500
517
|
// adapter reads firmness at run time, so changing it needs no config rewrite.
|
|
501
518
|
if (opts.agentHooks !== false) {
|
|
502
519
|
const a = installClaudeHooks(root, `${inv.agentHookShell} hook`);
|
|
503
|
-
|
|
520
|
+
say(` ✓ Claude Code agent hooks ${a.action} (firmness: ${firmness} — change with \`hunch firmness <level>\`)`);
|
|
504
521
|
}
|
|
505
522
|
// Multi-assistant compatibility: MCP + grounding + lifecycle adapters share
|
|
506
523
|
// the same .hunch/ graph across Cursor / VS Code / Windsurf / Antigravity.
|
|
507
524
|
if (opts.providers !== false) {
|
|
508
|
-
const
|
|
525
|
+
const detected = detectAssistants(root);
|
|
526
|
+
const only = opts.allAssistants || detected.size === 0 ? undefined : detected;
|
|
527
|
+
const ps = scaffoldProviders(root, inv.mcp, store, { agentHooks: opts.agentHooks !== false, only });
|
|
509
528
|
const ok = ps.filter((p) => !p.error);
|
|
529
|
+
summary.assistants.push(...ok.map((p) => p.assistant === "Any (AGENTS.md)" ? "any tool that reads AGENTS.md" : p.assistant));
|
|
530
|
+
if (only)
|
|
531
|
+
summary.skipped = PROVIDER_ASSISTANTS.filter((a) => !only.has(a));
|
|
532
|
+
if (summary.skipped.length)
|
|
533
|
+
say(` ✓ not configured (not found on this machine or in this repository): ${summary.skipped.join(", ")} — add them with \`hunch init --all-assistants\``);
|
|
510
534
|
const total = ok.reduce((a, p) => a + p.files.length, 0);
|
|
511
|
-
|
|
535
|
+
say(` ✓ wrote ${total} multi-assistant config file(s) → ${ok.map((p) => p.assistant).join(", ")}`);
|
|
512
536
|
for (const p of ps)
|
|
513
537
|
if (p.error)
|
|
514
|
-
|
|
538
|
+
say(` ⚠ skipped ${p.assistant}: ${p.error}`);
|
|
515
539
|
}
|
|
516
540
|
// Windows self-heal: if an earlier global `claude mcp add` left a drive-letter
|
|
517
541
|
// case-split in ~/.claude.json, merge it so hunch resolves under either casing.
|
|
@@ -521,16 +545,36 @@ program
|
|
|
521
545
|
// worktree of this repo auto-discovers it (also backfills pre-0.32 single-worktree setups),
|
|
522
546
|
// and note when we're initializing inside a linked worktree (memory is shared, not separate).
|
|
523
547
|
if (ensureSharedOverlayPointer(root, store.privateDir, store.privateAutoCommit, store.mode === "shared" ? "shared" : "private", true)) {
|
|
524
|
-
|
|
548
|
+
say(` ✓ private overlay registered at the git common dir — shared by every worktree of this repo`);
|
|
525
549
|
}
|
|
526
550
|
for (const line of sharedHooksNote(root, installs))
|
|
527
|
-
|
|
551
|
+
say(line);
|
|
528
552
|
store.close();
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
553
|
+
if (opts.details) {
|
|
554
|
+
for (const l of lines)
|
|
555
|
+
console.log(l.text);
|
|
556
|
+
console.log("\n" + formatIntegrationHealth(inspectIntegrations(root)));
|
|
557
|
+
console.log("\nTask reporting is configured through Hunch's agent instructions; actual agent activity has not been verified by setup. Reconnect the agent, then work normally. Completed task reports are available with `hunch report`.");
|
|
558
|
+
console.log(reportActivity(root));
|
|
559
|
+
console.log("Cold start? Seed from history: hunch backfill --since 90d");
|
|
560
|
+
console.log("\n⭐ If Hunch earns its keep, a star helps others find it → https://github.com/davesheffer/hunch");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
console.log(`🧠 Hunch is set up in ${root}`);
|
|
564
|
+
console.log(` ✓ memory: .hunch/${teamWired ? ` (connected to your team's shared store)` : ""}`);
|
|
565
|
+
if (summary.index)
|
|
566
|
+
console.log(` ✓ code index: ${summary.index}`);
|
|
567
|
+
if (summary.hooks.length)
|
|
568
|
+
console.log(` ✓ git hooks: ${summary.hooks.join(", ")}`);
|
|
569
|
+
console.log(` ✓ assistants: ${summary.assistants.join(", ")}`);
|
|
570
|
+
if (summary.skipped.length)
|
|
571
|
+
console.log(` not found here, so not configured: ${summary.skipped.join(", ")} (add with --all-assistants)`);
|
|
572
|
+
for (const l of lines)
|
|
573
|
+
if (l.warn)
|
|
574
|
+
console.log(l.text);
|
|
575
|
+
console.log("\nNext: reload your assistant, then ask it why a file you're about to change is built that way.");
|
|
576
|
+
console.log("No memory yet? Draft it from this repository's history: hunch backfill --since 90d");
|
|
577
|
+
console.log("More: hunch init --details (everything that was written) · hunch doctor (check the setup)");
|
|
534
578
|
});
|
|
535
579
|
// ---- uninstall ------------------------------------------------------------
|
|
536
580
|
program
|
|
@@ -5190,6 +5234,16 @@ program
|
|
|
5190
5234
|
}
|
|
5191
5235
|
}
|
|
5192
5236
|
catch { /* the next Stop or prompt retries */ }
|
|
5237
|
+
// Escalations this prompt's task touches (dec_12ba37db1c): asked once per
|
|
5238
|
+
// session, never answered for the human; the rest stay pending in
|
|
5239
|
+
// `hunch escalations`. A broken store must not take the prompt hook down.
|
|
5240
|
+
try {
|
|
5241
|
+
store ??= new HunchStore(paths);
|
|
5242
|
+
const ask = await promptEscalations(root, store, provider, evt);
|
|
5243
|
+
if (ask)
|
|
5244
|
+
parts.push(ask);
|
|
5245
|
+
}
|
|
5246
|
+
catch { /* escalations stay listed by `hunch escalations` */ }
|
|
5193
5247
|
// Pipeline turn bookkeeping (fresh block budget) + the one nag that must
|
|
5194
5248
|
// repeat: edits from an earlier turn still unverified.
|
|
5195
5249
|
if (evt.session_id && pipelineEnabled()) {
|
|
@@ -5351,36 +5405,17 @@ program
|
|
|
5351
5405
|
// further than a terminal.
|
|
5352
5406
|
const decisions = s.advisoryRecs("decisions");
|
|
5353
5407
|
const { pendingReview } = nowData(decisions, 3);
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
//
|
|
5357
|
-
//
|
|
5358
|
-
//
|
|
5359
|
-
|
|
5360
|
-
// out of session transcripts. Only the id and commit shas surface.
|
|
5361
|
-
const sessionStartQueue = readActivePendingRepairs(paths.root);
|
|
5362
|
-
escalations.push(...commitRepairEscalations(sessionStartQueue, decisions, s.recs("decisions"), withheldRewrites(paths.root, sessionStartQueue)));
|
|
5363
|
-
try {
|
|
5364
|
-
// Constitution human moments ride the same line; a broken policy store
|
|
5365
|
-
// must never take session-start orientation down (fail open). Public
|
|
5366
|
-
// store only — session transcripts travel further than a terminal.
|
|
5367
|
-
const { ConstitutionService: CS } = await import("../constitution/service.js");
|
|
5368
|
-
escalations.push(...policyEscalations(new CS(s, paths.root).list({ publicOnly: true }).map((p) => ({ ...p, last_action: p.audit.at(-1)?.action ?? null }))));
|
|
5369
|
-
}
|
|
5370
|
-
catch { /* constitution unavailable */ }
|
|
5371
|
-
// Only ACTIONABLE entries are worth asking inline — a duplicate-id
|
|
5372
|
-
// commit-repair follower whose own resolution says "act on a
|
|
5373
|
-
// different entry first" isn't a question the assistant can put to
|
|
5374
|
-
// the human directly (#61). Filtered BEFORE the bail check below so
|
|
5375
|
-
// "is there anything to say" and "what do we say" share one
|
|
5376
|
-
// predicate — an escalations list that's entirely non-actionable
|
|
5377
|
-
// must bail exactly like an empty one would.
|
|
5378
|
-
const actionableEsc = actionableEscalations(escalations);
|
|
5408
|
+
// Escalations are task-scoped (dec_12ba37db1c): session start has no task
|
|
5409
|
+
// yet, so it asks nothing when the provider delivers prompts — the prompt
|
|
5410
|
+
// hook asks about the ones a prompt touches; every entry stays pending in
|
|
5411
|
+
// `hunch escalations`. A provider with no prompt event would never ask,
|
|
5412
|
+
// so its orientation event keeps asking every actionable entry.
|
|
5413
|
+
const actionableEsc = deliversPromptEvent(provider) ? [] : (await actionableEscalationsWithAnchors(paths.root, s)).actionable;
|
|
5379
5414
|
if (!decisions.length && !actionableEsc.length) {
|
|
5380
5415
|
// Fresh graph and nothing else to raise: nothing to orient on, but
|
|
5381
5416
|
// the operating loop still ships. A queued commit-repair escalation
|
|
5382
|
-
// (checked against the full store
|
|
5383
|
-
//
|
|
5417
|
+
// (checked against the full store) is enough reason NOT to bail here
|
|
5418
|
+
// even when the visible decisions list is empty.
|
|
5384
5419
|
if (pipelineEnabled())
|
|
5385
5420
|
emitContext(provider, "SessionStart", [PIPELINE_LOOP, controllerBrief].filter(Boolean).join("\n\n"));
|
|
5386
5421
|
return;
|
|
@@ -7501,6 +7536,9 @@ program
|
|
|
7501
7536
|
console.log(team
|
|
7502
7537
|
? `overlay: off, but .hunch/team.json advertises the team store (${team.shared_repo}) — ${isTeamStoreTrusted(root, team) ? "run \`hunch init\` to auto-connect" : "if it is your team's store, run \`hunch shared --trust\` to connect"}`
|
|
7503
7538
|
: dim(`private: off — run \`hunch shared\` (or \`hunch private\`) to use one overlay repo across teammates/worktrees (or set HUNCH_PRIVATE_DIR)`));
|
|
7539
|
+
const ignored = memoryIgnoreRule(root);
|
|
7540
|
+
if (ignored)
|
|
7541
|
+
console.log(`memory: ⚠ ${describeMemoryIgnoreRule(ignored)}`);
|
|
7504
7542
|
}
|
|
7505
7543
|
// Worktree posture: linked worktrees share ONE memory via the git common dir. Only
|
|
7506
7544
|
// surfaced in a linked worktree (no noise in a normal single checkout), so a
|
|
@@ -7625,6 +7663,51 @@ function shellWriteGrounding(root, store, provider, evt, written) {
|
|
|
7625
7663
|
* A follow-up prompt widens the qualifying set rather than replacing it, so a
|
|
7626
7664
|
* "go" cannot unselect the task's memory. */
|
|
7627
7665
|
const TASK_SELECTION_TITLE_CHARS = 80;
|
|
7666
|
+
/** Every actionable escalation plus each entry id's anchor paths (a decision's
|
|
7667
|
+
* related_files, a policy's scope paths/globs). Titles come from the advisory
|
|
7668
|
+
* scope (public-only in private mode: session transcripts travel further than
|
|
7669
|
+
* a terminal); commit-repair liveness and the anchors read the full store, so a
|
|
7670
|
+
* private decision's repair stays answerable by id. */
|
|
7671
|
+
async function actionableEscalationsWithAnchors(root, store) {
|
|
7672
|
+
const decisions = store.advisoryRecs("decisions");
|
|
7673
|
+
const all = store.recs("decisions");
|
|
7674
|
+
const anchors = new Map(all.map((d) => [d.id, d.related_files]));
|
|
7675
|
+
const escalations = pendingEscalations(decisions);
|
|
7676
|
+
escalations.push(...premiseEscalations(decisions, { now: new Date().toISOString(), exists: (p) => existsSync(join(root, p)) }));
|
|
7677
|
+
const queue = readActivePendingRepairs(root);
|
|
7678
|
+
escalations.push(...commitRepairEscalations(queue, decisions, all, withheldRewrites(root, queue)));
|
|
7679
|
+
try {
|
|
7680
|
+
// Constitution human moments ride the same line; a broken policy store must
|
|
7681
|
+
// never take the hook down (fail open). Public store only.
|
|
7682
|
+
const { ConstitutionService: CS } = await import("../constitution/service.js");
|
|
7683
|
+
const policies = new CS(store, root).list({ publicOnly: true });
|
|
7684
|
+
for (const p of policies)
|
|
7685
|
+
anchors.set(p.id, p.scope.paths);
|
|
7686
|
+
escalations.push(...policyEscalations(policies.map((p) => ({ ...p, last_action: p.audit.at(-1)?.action ?? null }))));
|
|
7687
|
+
}
|
|
7688
|
+
catch { /* constitution unavailable */ }
|
|
7689
|
+
// Only ACTIONABLE entries are worth asking inline — a duplicate-id commit-repair
|
|
7690
|
+
// follower whose resolution says "act on a different entry first" isn't a
|
|
7691
|
+
// question the assistant can put to the human directly (#61).
|
|
7692
|
+
return { actionable: actionableEscalations(escalations), anchors };
|
|
7693
|
+
}
|
|
7694
|
+
/** The ASK line for the escalations one prompt touches, or "" (dec_12ba37db1c). */
|
|
7695
|
+
async function promptEscalations(root, store, provider, evt) {
|
|
7696
|
+
if (!evt.prompt || isNotificationPrompt(evt.prompt))
|
|
7697
|
+
return "";
|
|
7698
|
+
const { actionable, anchors } = await actionableEscalationsWithAnchors(root, store);
|
|
7699
|
+
if (!actionable.length)
|
|
7700
|
+
return "";
|
|
7701
|
+
// A qualifying record outside the top-K is still part of this task.
|
|
7702
|
+
const selection = loadTaskSelection(hookReportTaskId(root, provider, evt));
|
|
7703
|
+
const selectedIds = new Set([...(selection?.top ?? []), ...(selection?.qualifying ?? [])]);
|
|
7704
|
+
const relevant = relevantEscalations(actionable, { prompt: evt.prompt, paths: promptPaths(evt.prompt, root), selectedIds }, (id) => anchors.get(id) ?? []);
|
|
7705
|
+
// Each question is asked once per session, however the touched set changes.
|
|
7706
|
+
const unasked = relevant.filter((e) => injectionMode(evt.session_id, `prompt-escalation:${e.kind}:${e.topic}:${e.decisionIds.join(",")}`, e.question) === "full");
|
|
7707
|
+
if (!unasked.length)
|
|
7708
|
+
return "";
|
|
7709
|
+
return `⚖ ${unasked.length} decision(s) this task touches need the human's call — ASK the user inline, never decide for them: ${unasked.map((e) => e.question).join(" · ")}`;
|
|
7710
|
+
}
|
|
7628
7711
|
function promptTaskSelection(root, store, provider, evt) {
|
|
7629
7712
|
// A host notification is not a task prompt: the earlier selection stands untouched.
|
|
7630
7713
|
if (!taskSelectionEnabled() || isNotificationPrompt(evt.prompt))
|
package/dist/core/agenthook.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export declare const HOOK_PROVIDERS: readonly ["claude", "codex", "vscode", "windsurf", "antigravity", "cursor"];
|
|
9
9
|
export type HookProvider = (typeof HOOK_PROVIDERS)[number];
|
|
10
|
+
export declare function deliversPromptEvent(provider: HookProvider): boolean;
|
|
10
11
|
export type HunchHookEvent = "PreToolUse" | "PostToolUse" | "PostToolUseFailure" | "UserPromptSubmit" | "SessionStart" | "SubagentStart" | "PreCompact" | "Stop";
|
|
11
12
|
/** One file section of a Codex `apply_patch` payload. */
|
|
12
13
|
export interface HunchPatchFile {
|
package/dist/core/agenthook.js
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
* the same small, fail-open shape regardless of the assistant that emitted it.
|
|
7
7
|
*/
|
|
8
8
|
export const HOOK_PROVIDERS = ["claude", "codex", "vscode", "windsurf", "antigravity", "cursor"];
|
|
9
|
+
/** Whether a provider's hooks deliver the user's prompt text (a UserPromptSubmit
|
|
10
|
+
* equivalent). Prompt-scoped behavior needs it; a provider without one gets that
|
|
11
|
+
* behavior at its session/orientation event instead. Antigravity's PreInvocation
|
|
12
|
+
* payload carries only conversation/invocation counters, no turn text. A Record,
|
|
13
|
+
* so a new provider must declare its answer. */
|
|
14
|
+
const PROMPT_EVENT = {
|
|
15
|
+
claude: true, codex: true, vscode: true, windsurf: true, cursor: true, antigravity: false,
|
|
16
|
+
};
|
|
17
|
+
export function deliversPromptEvent(provider) {
|
|
18
|
+
return PROMPT_EVENT[provider];
|
|
19
|
+
}
|
|
9
20
|
function obj(value) {
|
|
10
21
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
11
22
|
}
|
|
@@ -127,3 +127,23 @@ export interface PolicyLite {
|
|
|
127
127
|
* accept/reject) is a human call. Machine conclusions never appear here as
|
|
128
128
|
* approvals — every entry is a QUESTION with its explicit resolution verb. */
|
|
129
129
|
export declare function policyEscalations(policies: readonly PolicyLite[]): Escalation[];
|
|
130
|
+
/** What one prompt is about, for task-scoped escalation delivery (dec_12ba37db1c). */
|
|
131
|
+
export interface EscalationScope {
|
|
132
|
+
/** the prompt text as the human wrote it. */
|
|
133
|
+
prompt: string;
|
|
134
|
+
/** repo-relative paths the prompt names (taskSelection.promptPaths). */
|
|
135
|
+
paths: readonly string[];
|
|
136
|
+
/** record ids the prompt's task selection put on top, when there is one. */
|
|
137
|
+
selectedIds?: ReadonlySet<string>;
|
|
138
|
+
}
|
|
139
|
+
/** The escalations one prompt's task touches. A prompt hook asks the human only
|
|
140
|
+
* about these; every other entry stays exactly as pending — still listed by
|
|
141
|
+
* `hunch escalations`, hunch_escalations and hunch_now, never dropped and
|
|
142
|
+
* never answered for the human. Deterministic explicit anchors only: a
|
|
143
|
+
* prompt that asks about escalations themselves (path tokens aside), an entry
|
|
144
|
+
* id or topic the prompt names, an id the task selection chose, or a recorded
|
|
145
|
+
* anchor path the prompt names (a directory covers the files under it; a glob
|
|
146
|
+
* covers the paths it matches). `relatedFiles` maps an entry id to its anchor
|
|
147
|
+
* paths: a decision's related_files, a policy's scope paths/globs. An entry
|
|
148
|
+
* with no anchor the prompt names stays CLI/MCP-only. */
|
|
149
|
+
export declare function relevantEscalations(items: readonly Escalation[], scope: EscalationScope, relatedFiles: (id: string) => readonly string[]): Escalation[];
|
package/dist/core/escalations.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { topicCollisions } from "./topics.js";
|
|
2
2
|
import { liveRewrites, deadRewrites, commitRepairReviewHash, firstFor } from "./commitrepair.js";
|
|
3
3
|
import { importedAdrReviewHash, importedAdrSourceHash, pendingImportedAdrReviews } from "./importReview.js";
|
|
4
|
+
import { pathMatchesGlob } from "./glob.js";
|
|
4
5
|
/** Escalations gating surfaces (CI exit codes, "needs your call" counts/lists)
|
|
5
6
|
* should act on: everything except an entry explicitly marked
|
|
6
7
|
* actionable:false (#61). The full list — including non-actionable
|
|
@@ -259,4 +260,50 @@ export function policyEscalations(policies) {
|
|
|
259
260
|
}
|
|
260
261
|
return out;
|
|
261
262
|
}
|
|
263
|
+
const trimSlash = (x) => x.replace(/\/+$/, "");
|
|
264
|
+
const samePathOrNested = (x, y) => {
|
|
265
|
+
const a = trimSlash(x), b = trimSlash(y);
|
|
266
|
+
return !!a && !!b && (a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`));
|
|
267
|
+
};
|
|
268
|
+
/** A recorded related path or scope glob vs a path the prompt names. A plain
|
|
269
|
+
* entry relates both ways by directory nesting; a glob entry matches a path it
|
|
270
|
+
* covers, or a directory that holds its literal prefix (`src/` ~ `src/store/**`). */
|
|
271
|
+
const relatedPathHit = (entry, promptPath) => {
|
|
272
|
+
if (!/[*?]/.test(entry))
|
|
273
|
+
return samePathOrNested(entry, promptPath);
|
|
274
|
+
if (pathMatchesGlob(promptPath, entry))
|
|
275
|
+
return true;
|
|
276
|
+
const literal = trimSlash(entry.split("/").filter((_, i, segs) => !segs.slice(0, i + 1).some((s) => /[*?]/.test(s))).join("/"));
|
|
277
|
+
const dir = trimSlash(promptPath);
|
|
278
|
+
return !!literal && !!dir && (literal === dir || literal.startsWith(`${dir}/`));
|
|
279
|
+
};
|
|
280
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
281
|
+
/** The prompt's prose without path-like tokens (a `/` or a file extension), so a
|
|
282
|
+
* file named `escalations.test.ts` is not a question about escalations. */
|
|
283
|
+
const proseOnly = (prompt) => prompt.split(/\s+/).filter((t) => { const w = t.replace(/[)\],.;:!?'"`]+$/, ""); return !w.includes("/") && !/\.[a-z0-9]{1,8}$/i.test(w); }).join(" ");
|
|
284
|
+
/** The escalations one prompt's task touches. A prompt hook asks the human only
|
|
285
|
+
* about these; every other entry stays exactly as pending — still listed by
|
|
286
|
+
* `hunch escalations`, hunch_escalations and hunch_now, never dropped and
|
|
287
|
+
* never answered for the human. Deterministic explicit anchors only: a
|
|
288
|
+
* prompt that asks about escalations themselves (path tokens aside), an entry
|
|
289
|
+
* id or topic the prompt names, an id the task selection chose, or a recorded
|
|
290
|
+
* anchor path the prompt names (a directory covers the files under it; a glob
|
|
291
|
+
* covers the paths it matches). `relatedFiles` maps an entry id to its anchor
|
|
292
|
+
* paths: a decision's related_files, a policy's scope paths/globs. An entry
|
|
293
|
+
* with no anchor the prompt names stays CLI/MCP-only. */
|
|
294
|
+
export function relevantEscalations(items, scope, relatedFiles) {
|
|
295
|
+
const prompt = scope.prompt ?? "";
|
|
296
|
+
if (/(?:^|[^a-z0-9-])escalations?(?:$|[^a-z0-9])/i.test(proseOnly(prompt)))
|
|
297
|
+
return [...items];
|
|
298
|
+
const lower = prompt.toLowerCase();
|
|
299
|
+
const named = (term) => {
|
|
300
|
+
const t = term.trim().toLowerCase();
|
|
301
|
+
if (t.length < 3)
|
|
302
|
+
return false;
|
|
303
|
+
return new RegExp(`(?:^|[^a-z0-9_-])${escapeRegExp(t)}(?:$|[^a-z0-9_-])`).test(lower);
|
|
304
|
+
};
|
|
305
|
+
return items.filter((e) => named(e.topic)
|
|
306
|
+
|| e.decisionIds.some((id) => named(id) || scope.selectedIds?.has(id))
|
|
307
|
+
|| e.decisionIds.some((id) => relatedFiles(id).some((file) => scope.paths.some((p) => relatedPathHit(file, p)))));
|
|
308
|
+
}
|
|
262
309
|
//# sourceMappingURL=escalations.js.map
|
package/dist/core/hookText.js
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
export const HOOK_REMINDER = "Hunch (engineering memory) is available for this repo. Before editing, call " +
|
|
5
5
|
"hunch_check_constraints(scope) for do-not-break invariants and hunch_why(target) " +
|
|
6
6
|
"for the rationale; use hunch_get_dependents for blast radius and hunch_bug_lineage " +
|
|
7
|
-
"for prior root causes.
|
|
7
|
+
"for prior root causes. Record only what the human decides or corrects.";
|
|
8
8
|
//# sourceMappingURL=hookText.js.map
|
|
@@ -98,7 +98,7 @@ export function renderHunchSection(store, root) {
|
|
|
98
98
|
lines.push("This repo has **Hunch**, a graph of *why* the code is the way it is. It holds " +
|
|
99
99
|
`${renderCountsMatch(counts)}. Use the \`hunch_*\` MCP tools by moment:`);
|
|
100
100
|
lines.push("");
|
|
101
|
-
lines.push("- **Start:** reuse the task ID and `task verify` command the prompt hook printed; with none, call `hunch_task(action: \"start\", title)` once. Then `hunch_context(target, task_id)` first. Orient with `hunch_structure`, `hunch_workspaces`, `hunch_runbook(task)`.
|
|
101
|
+
lines.push("- **Start:** reuse the task ID and `task verify` command the prompt hook printed; with none, call `hunch_task(action: \"start\", title)` once. Then `hunch_context(target, task_id)` first. Orient with `hunch_structure`, `hunch_workspaces`, `hunch_runbook(task)`. When the prompt hook raises an escalation this task touches, ask the user; silence is never approval. `hunch_escalations()` lists the rest.");
|
|
102
102
|
lines.push("- **Design:** `hunch_why(target)` (includes what was rejected), `hunch_current_decision(topic)`, `hunch_bug_lineage(symptom_or_symbol)`, `hunch_compare(candidates)`, `hunch_query(query)`.");
|
|
103
103
|
lines.push("- **Edit:** `hunch_check_constraints(scope)`, `hunch_get_dependents(symbol)` / `hunch_blast_radius(target)`, `hunch_findings(scope?)`.");
|
|
104
104
|
lines.push("- **Merge:** `hunch_conformance()`, `hunch_pr_impact(base?)`, `hunch_merge_verdict`." +
|
|
@@ -39,6 +39,22 @@ export interface ManagedGitignoreUpgrade {
|
|
|
39
39
|
export declare function upgradeManagedGitignore(root: string): ManagedGitignoreUpgrade;
|
|
40
40
|
/** Output lines describing what an upgrade changed; empty when nothing did. */
|
|
41
41
|
export declare function describeGitignoreUpgrade(upgrade: ManagedGitignoreUpgrade): string[];
|
|
42
|
+
export interface MemoryIgnoreRule {
|
|
43
|
+
/** Ignore file that holds the rule, as git reports it (e.g. `.gitignore`). */
|
|
44
|
+
source: string;
|
|
45
|
+
/** 1-based line of the rule in `source`. */
|
|
46
|
+
line: number;
|
|
47
|
+
pattern: string;
|
|
48
|
+
}
|
|
49
|
+
/** The user's ignore rule that keeps engineering memory out of Git, or null.
|
|
50
|
+
* A repository that ignores `.hunch/` (or `.hunch/decisions/`) silently loses every
|
|
51
|
+
* capture: the records are written, never committed, and gone on a fresh clone.
|
|
52
|
+
* Hunch's own private-only block ignores the same paths on purpose (memory lives
|
|
53
|
+
* in an overlay), so a rule inside that block is not reported. Unknown git state
|
|
54
|
+
* yields null: the caller then says nothing rather than guessing. */
|
|
55
|
+
export declare function memoryIgnoreRule(root: string): MemoryIgnoreRule | null;
|
|
56
|
+
/** One line telling the user their memory will not reach Git, and how to fix it. */
|
|
57
|
+
export declare function describeMemoryIgnoreRule(rule: MemoryIgnoreRule): string;
|
|
42
58
|
/** `.gitignore` text with both managed Hunch blocks removed (markers inclusive),
|
|
43
59
|
* every other byte kept, for `hunch uninstall --purge` (#449). `damaged` when a
|
|
44
60
|
* marker line has no matching end line: the block is never guessed at. */
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* block written by an older release gains entries added later) and is otherwise
|
|
13
13
|
* byte-identical; everything outside the markers is left untouched.
|
|
14
14
|
*/
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
15
16
|
import { readFileSync, existsSync, lstatSync, realpathSync } from "node:fs";
|
|
16
17
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
17
18
|
import { writeFileAtomic } from "../core/io.js";
|
|
@@ -231,6 +232,35 @@ export function describeGitignoreUpgrade(upgrade) {
|
|
|
231
232
|
}
|
|
232
233
|
return out;
|
|
233
234
|
}
|
|
235
|
+
/** The user's ignore rule that keeps engineering memory out of Git, or null.
|
|
236
|
+
* A repository that ignores `.hunch/` (or `.hunch/decisions/`) silently loses every
|
|
237
|
+
* capture: the records are written, never committed, and gone on a fresh clone.
|
|
238
|
+
* Hunch's own private-only block ignores the same paths on purpose (memory lives
|
|
239
|
+
* in an overlay), so a rule inside that block is not reported. Unknown git state
|
|
240
|
+
* yields null: the caller then says nothing rather than guessing. */
|
|
241
|
+
export function memoryIgnoreRule(root) {
|
|
242
|
+
const probe = ".hunch/decisions/dec_0000000000.json";
|
|
243
|
+
const r = spawnSync("git", ["check-ignore", "-v", "--no-index", "--", probe], { cwd: root, encoding: "utf8", windowsHide: true });
|
|
244
|
+
if (r.error || r.status !== 0)
|
|
245
|
+
return null;
|
|
246
|
+
const m = /^(.*):(\d+):(.*)\t/.exec(r.stdout ?? "");
|
|
247
|
+
if (!m)
|
|
248
|
+
return null;
|
|
249
|
+
const rule = { source: m[1], line: Number(m[2]), pattern: m[3] };
|
|
250
|
+
if (rule.pattern.startsWith("!"))
|
|
251
|
+
return null;
|
|
252
|
+
const file = resolve(root, rule.source);
|
|
253
|
+
if (existsSync(file)) {
|
|
254
|
+
const span = findBlock(readFileSync(file, "utf8").split("\n"), MEM_MARK, MEM_END);
|
|
255
|
+
if (span && rule.line - 1 > span.start && rule.line - 1 < span.end)
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
return rule;
|
|
259
|
+
}
|
|
260
|
+
/** One line telling the user their memory will not reach Git, and how to fix it. */
|
|
261
|
+
export function describeMemoryIgnoreRule(rule) {
|
|
262
|
+
return `${rule.source} line ${rule.line} (\`${rule.pattern}\`) ignores .hunch/, so captured memory is never committed or shared and is lost on a fresh clone. Remove that line, or keep memory in a private repository with \`hunch private --repo <url>\`.`;
|
|
263
|
+
}
|
|
234
264
|
/** `.gitignore` text with both managed Hunch blocks removed (markers inclusive),
|
|
235
265
|
* every other byte kept, for `hunch uninstall --purge` (#449). `damaged` when a
|
|
236
266
|
* marker line has no matching end line: the block is never guessed at. */
|
|
@@ -115,7 +115,16 @@ export interface ProviderScaffold {
|
|
|
115
115
|
* per-assistant summary for `hunch init` to print. Each assistant is isolated:
|
|
116
116
|
* a writer that refuses to clobber a malformed file degrades to a warning rather
|
|
117
117
|
* than aborting the rest. Claude Code is handled separately by scaffold.ts. */
|
|
118
|
+
/** The assistants `scaffoldProviders` can configure, besides the always-written AGENTS.md. */
|
|
119
|
+
export declare const PROVIDER_ASSISTANTS: readonly ["Cursor", "VS Code (Copilot)", "Codex CLI", "Windsurf", "Google Antigravity"];
|
|
120
|
+
export type ProviderAssistant = typeof PROVIDER_ASSISTANTS[number];
|
|
121
|
+
/** Assistants with a footprint here: a config already in the repository (a teammate
|
|
122
|
+
* set it up), a settings folder in the user's home, or a command on PATH. `hunch init`
|
|
123
|
+
* configures only these, so a repository is not filled with settings for tools nobody
|
|
124
|
+
* on it uses; when none is found it configures every assistant, as before. */
|
|
125
|
+
export declare function detectAssistants(root: string, home?: string, env?: NodeJS.ProcessEnv): Set<ProviderAssistant>;
|
|
118
126
|
export declare function scaffoldProviders(root: string, inv: Invocation, store: HunchStore, options?: {
|
|
119
127
|
agentHooks?: boolean;
|
|
120
128
|
home?: string;
|
|
129
|
+
only?: ReadonlySet<string>;
|
|
121
130
|
}): ProviderScaffold[];
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
21
21
|
import { writeFileAtomic } from "../core/io.js";
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
|
-
import { join, dirname } from "node:path";
|
|
23
|
+
import { join, dirname, delimiter } from "node:path";
|
|
24
24
|
import { isHunchHookCommand } from "./hookmatch.js";
|
|
25
25
|
import { renderHunchSection, stripManagedSection, upsertSection, updateClaudeMd, preserveNewerTemplate } from "./claudemd.js";
|
|
26
26
|
import { headFileContent, isGitCleanPath } from "../extractors/git.js";
|
|
@@ -560,6 +560,32 @@ function runProvider(writers) {
|
|
|
560
560
|
* per-assistant summary for `hunch init` to print. Each assistant is isolated:
|
|
561
561
|
* a writer that refuses to clobber a malformed file degrades to a warning rather
|
|
562
562
|
* than aborting the rest. Claude Code is handled separately by scaffold.ts. */
|
|
563
|
+
/** The assistants `scaffoldProviders` can configure, besides the always-written AGENTS.md. */
|
|
564
|
+
export const PROVIDER_ASSISTANTS = ["Cursor", "VS Code (Copilot)", "Codex CLI", "Windsurf", "Google Antigravity"];
|
|
565
|
+
function onPath(names, env) {
|
|
566
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
567
|
+
const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").concat("") : [""];
|
|
568
|
+
return dirs.some((dir) => names.some((name) => exts.some((ext) => existsSync(join(dir, name + ext)))));
|
|
569
|
+
}
|
|
570
|
+
/** Assistants with a footprint here: a config already in the repository (a teammate
|
|
571
|
+
* set it up), a settings folder in the user's home, or a command on PATH. `hunch init`
|
|
572
|
+
* configures only these, so a repository is not filled with settings for tools nobody
|
|
573
|
+
* on it uses; when none is found it configures every assistant, as before. */
|
|
574
|
+
export function detectAssistants(root, home = homedir(), env = process.env) {
|
|
575
|
+
const has = (...paths) => paths.some((p) => existsSync(p));
|
|
576
|
+
const found = new Set();
|
|
577
|
+
if (has(join(root, ".cursor"), join(home, ".cursor")) || onPath(["cursor", "cursor-agent"], env))
|
|
578
|
+
found.add("Cursor");
|
|
579
|
+
if (has(join(root, ".vscode"), join(root, ".github", "copilot-instructions.md"), join(home, ".vscode")) || onPath(["code", "code-insiders"], env))
|
|
580
|
+
found.add("VS Code (Copilot)");
|
|
581
|
+
if (has(join(root, ".codex"), join(home, ".codex")) || onPath(["codex"], env))
|
|
582
|
+
found.add("Codex CLI");
|
|
583
|
+
if (has(join(root, ".windsurf"), join(home, ".codeium", "windsurf")) || onPath(["windsurf"], env))
|
|
584
|
+
found.add("Windsurf");
|
|
585
|
+
if (has(join(root, ".agents", "mcp_config.json"), join(home, ".gemini")) || onPath(["antigravity"], env))
|
|
586
|
+
found.add("Google Antigravity");
|
|
587
|
+
return found;
|
|
588
|
+
}
|
|
563
589
|
export function scaffoldProviders(root, inv, store, options = {}) {
|
|
564
590
|
const hooks = options.agentHooks !== false;
|
|
565
591
|
const home = options.home;
|
|
@@ -584,6 +610,9 @@ export function scaffoldProviders(root, inv, store, options = {}) {
|
|
|
584
610
|
}],
|
|
585
611
|
["Any (AGENTS.md)", () => runProvider([() => writeAgentsMd(root, store)])],
|
|
586
612
|
];
|
|
587
|
-
|
|
613
|
+
const only = options.only;
|
|
614
|
+
return tasks
|
|
615
|
+
.filter(([assistant]) => !only || only.has(assistant) || assistant === "Any (AGENTS.md)")
|
|
616
|
+
.map(([assistant, run]) => ({ assistant, ...run() }));
|
|
588
617
|
}
|
|
589
618
|
//# sourceMappingURL=providers.js.map
|
|
@@ -62,7 +62,7 @@ Follow the Hunch-grounded workflow (DESIGN §5) — do NOT skip the memory looku
|
|
|
62
62
|
2. Identify the suspect symbol/file, then \`hunch_get_dependents(<symbol>)\` to learn the blast radius.
|
|
63
63
|
3. \`hunch_check_constraints(<scope>)\` — list invariants you must preserve.
|
|
64
64
|
4. Propose a fix that honors past root causes AND constraints. Apply it and run the tests.
|
|
65
|
-
5. If the
|
|
65
|
+
5. If the human decided or corrected something during the fix, record it (\`hunch_record_decision\` / \`hunch_record_correction\`); the commit itself is captured automatically.
|
|
66
66
|
`;
|
|
67
67
|
const FRAGILE_CMD = `---
|
|
68
68
|
description: Report the most fragile parts of this codebase, with evidence
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
type JsonObject = Record<string, unknown>;
|
|
3
|
+
/** One tool definition as the host should see it. */
|
|
4
|
+
export declare function leanTool(tool: JsonObject): JsonObject;
|
|
5
|
+
/** Route the SDK's tools/list result through leanTool. Call right after
|
|
6
|
+
* constructing the server: McpServer installs its tools/list handler on the
|
|
7
|
+
* first registerTool, through this public setRequestHandler. */
|
|
8
|
+
export declare function installLeanToolList(server: McpServer): void;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3
|
+
// Keywords whose meaning differs between draft-07 and 2020-12 — the dialect MCP
|
|
4
|
+
// assumes when `$schema` is absent. A schema that uses any keeps its marker.
|
|
5
|
+
// A property merely NAMED like one also keeps it: conservative, never wrong.
|
|
6
|
+
const DIALECT_SENSITIVE = new Set(["definitions", "dependencies", "additionalItems", "$ref", "$id", "$recursiveRef"]);
|
|
7
|
+
function dialectNeutral(node) {
|
|
8
|
+
if (Array.isArray(node))
|
|
9
|
+
return node.every(dialectNeutral);
|
|
10
|
+
if (!isObject(node))
|
|
11
|
+
return true;
|
|
12
|
+
for (const [key, value] of Object.entries(node)) {
|
|
13
|
+
if (DIALECT_SENSITIVE.has(key) || (key === "items" && Array.isArray(value)))
|
|
14
|
+
return false;
|
|
15
|
+
if (!dialectNeutral(value))
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
// Keywords whose value is data, not a subschema: copied as-is.
|
|
21
|
+
const VALUE_KEYWORDS = new Set(["default", "const", "enum", "examples"]);
|
|
22
|
+
/** A copy without zod's safe-integer bounds, which only restate what JSON numbers can hold. */
|
|
23
|
+
function withoutSafeIntegerBounds(node) {
|
|
24
|
+
if (Array.isArray(node))
|
|
25
|
+
return node.map(withoutSafeIntegerBounds);
|
|
26
|
+
if (!isObject(node))
|
|
27
|
+
return node;
|
|
28
|
+
const integer = node.type === "integer";
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [key, value] of Object.entries(node)) {
|
|
31
|
+
if (integer && ((key === "maximum" && value === Number.MAX_SAFE_INTEGER) || (key === "minimum" && value === Number.MIN_SAFE_INTEGER)))
|
|
32
|
+
continue;
|
|
33
|
+
out[key] = VALUE_KEYWORDS.has(key) ? value : withoutSafeIntegerBounds(value);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
// Only these markers are safe to drop on a dialect-neutral schema; any other
|
|
38
|
+
// dialect (draft-04's boolean exclusiveMinimum, say) keeps its marker.
|
|
39
|
+
const DROPPABLE_MARKERS = new Set([
|
|
40
|
+
"http://json-schema.org/draft-07/schema#",
|
|
41
|
+
"https://json-schema.org/draft/2020-12/schema",
|
|
42
|
+
]);
|
|
43
|
+
function leanSchema(schema) {
|
|
44
|
+
const lean = withoutSafeIntegerBounds(schema);
|
|
45
|
+
if (DROPPABLE_MARKERS.has(schema.$schema) && dialectNeutral(schema))
|
|
46
|
+
delete lean.$schema;
|
|
47
|
+
return lean;
|
|
48
|
+
}
|
|
49
|
+
const isDefaultExecution = (v) => isObject(v) && Object.entries(v).every(([key, value]) => key === "taskSupport" && (value === undefined || value === "forbidden"));
|
|
50
|
+
/** One tool definition as the host should see it. */
|
|
51
|
+
export function leanTool(tool) {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const [key, value] of Object.entries(tool)) {
|
|
54
|
+
if (key === "execution" && isDefaultExecution(value))
|
|
55
|
+
continue;
|
|
56
|
+
out[key] = (key === "inputSchema" || key === "outputSchema") && isObject(value) ? leanSchema(value) : value;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** The request method a schema handles — by identity, or by its method literal
|
|
61
|
+
* when a second SDK copy supplied a different schema object. */
|
|
62
|
+
function handlesToolsList(schema) {
|
|
63
|
+
if (schema === ListToolsRequestSchema)
|
|
64
|
+
return true;
|
|
65
|
+
try {
|
|
66
|
+
return schema.shape?.method?.value === "tools/list";
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Route the SDK's tools/list result through leanTool. Call right after
|
|
73
|
+
* constructing the server: McpServer installs its tools/list handler on the
|
|
74
|
+
* first registerTool, through this public setRequestHandler. */
|
|
75
|
+
export function installLeanToolList(server) {
|
|
76
|
+
const protocol = server.server;
|
|
77
|
+
const setRequestHandler = protocol.setRequestHandler.bind(protocol);
|
|
78
|
+
protocol.setRequestHandler = ((schema, handler) => {
|
|
79
|
+
if (!handlesToolsList(schema))
|
|
80
|
+
return setRequestHandler(schema, handler);
|
|
81
|
+
setRequestHandler(schema, async (...args) => {
|
|
82
|
+
const result = await handler(...args);
|
|
83
|
+
if (!isObject(result) || !Array.isArray(result.tools))
|
|
84
|
+
return result;
|
|
85
|
+
return { ...result, tools: result.tools.map((t) => (isObject(t) ? leanTool(t) : t)) };
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=leanToolList.js.map
|
package/dist/mcp/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { z } from "zod";
|
|
|
16
16
|
import { hunchPaths, findRoot, toPosixTarget, repoRelativeTarget } from "../core/paths.js";
|
|
17
17
|
import { matchSymbolsTiered } from "../core/glob.js";
|
|
18
18
|
import { resolveMcpToolset } from "./toolset.js";
|
|
19
|
+
import { installLeanToolList } from "./leanToolList.js";
|
|
19
20
|
import { PolicyRepository } from "../constitution/repository.js";
|
|
20
21
|
import { readConfig } from "../core/config.js";
|
|
21
22
|
import { canonicalRootPath, resolveActiveRoot } from "./roots.js";
|
|
@@ -1129,7 +1130,7 @@ function prepareRoot(root, explicitOverlay, requireIndex) {
|
|
|
1129
1130
|
export const MCP_INSTRUCTIONS = [
|
|
1130
1131
|
"Hunch is this repository's engineering memory: decisions, bug history, invariants, components, with provenance.",
|
|
1131
1132
|
"Per user task: (1) hunch_task(action:\"start\", title) once — skip it when the host's prompt hook already opened the task and printed its verify command, and reuse that task_id; (2) hunch_context(target, task_id) FIRST, before reading or editing, for the file, symbol, or task phrase; (3) hunch_check_constraints(scope) before editing shared code; (4) hunch_task(action:\"finish\", task_id) before the final response, showing its contribution card — required, with one exception: the host's prompt hook opened the task AND its instruction said that host closes the task AND the task used no Hunch (no hunch_* call on this task_id, no task verify check, no hook context you acted on, nothing to claim). A task you started with hunch_task is always finished by you.",
|
|
1132
|
-
"Then by moment: hunch_why(target) for rationale and rejected alternatives, hunch_bug_lineage before fixing a failure,
|
|
1133
|
+
"Then by moment: hunch_why(target) for rationale and rejected alternatives, hunch_bug_lineage before fixing a failure, hunch_record_correction when a human corrects you, hunch_record_decision for a decision the human makes; record only what the human decides or corrects.",
|
|
1133
1134
|
"Hosts without lifecycle hooks (Windsurf, Cursor, or Codex before its hooks are trusted) receive no automatic grounding: call these tools yourself.",
|
|
1134
1135
|
].join("\n");
|
|
1135
1136
|
export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
@@ -1207,6 +1208,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1207
1208
|
if (toolset.hidden.length)
|
|
1208
1209
|
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`);
|
|
1209
1210
|
const server = new McpServer({ name: "hunch", version: HUNCH_VERSION }, { instructions: MCP_INSTRUCTIONS });
|
|
1211
|
+
installLeanToolList(server);
|
|
1210
1212
|
let activeRequests = 0;
|
|
1211
1213
|
let pendingRoot = null;
|
|
1212
1214
|
let pendingScheduled = false;
|
|
@@ -2103,7 +2105,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2103
2105
|
// -- hunch_record_decision (write-back) -----------------------------------
|
|
2104
2106
|
server.registerTool("hunch_record_decision", {
|
|
2105
2107
|
title: "Record a decision (write-back)",
|
|
2106
|
-
description: "Persist a Decision (ADR) with provenance
|
|
2108
|
+
description: "Persist a Decision (ADR) with provenance when the human makes or confirms one in conversation; routine choices are captured from commits. 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.",
|
|
2107
2109
|
inputSchema: {
|
|
2108
2110
|
decision: z.object({
|
|
2109
2111
|
title: z.string(),
|
package/dist/serve/mcpHttp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { installLeanToolList } from "../mcp/leanToolList.js";
|
|
2
3
|
// The web-standard transport, not the Node one: the Node wrapper imports @hono/node-server,
|
|
3
4
|
// which the production dependency audit keeps unreachable (tooling/production-dependency-audit.mjs).
|
|
4
5
|
// Bridging IncomingMessage → Request → ServerResponse here is a few lines and keeps that boundary.
|
|
@@ -51,6 +52,7 @@ const TOOLS = [
|
|
|
51
52
|
];
|
|
52
53
|
function buildServer(dispatch, problemOf, version) {
|
|
53
54
|
const server = new McpServer({ name: "hunch-serve", version });
|
|
55
|
+
installLeanToolList(server);
|
|
54
56
|
for (const tool of TOOLS) {
|
|
55
57
|
server.registerTool(tool.name, { title: tool.title, description: tool.description, inputSchema: tool.input }, async (input) => {
|
|
56
58
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"mcpName": "io.github.davesheffer/hunch",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
"contracts/change-proof/*.json",
|
|
45
45
|
"server.json",
|
|
46
46
|
"bench/constitution-exp03-v1.json",
|
|
47
|
-
"tooling/competitive-watch.mjs",
|
|
48
47
|
"tooling/md1-benchmark.mjs",
|
|
49
48
|
"LICENSE",
|
|
50
49
|
"NOTICE"
|
|
@@ -91,12 +90,10 @@
|
|
|
91
90
|
"site:proof": "npm run build && node tooling/generate-public-proof.mjs",
|
|
92
91
|
"site:sitemap": "node tooling/generate-sitemap.mjs",
|
|
93
92
|
"bench:md1": "node tooling/md1-benchmark.mjs",
|
|
94
|
-
"research:competitors": "node tooling/competitive-watch.mjs",
|
|
95
|
-
"outreach": "node tooling/outreach-pipeline.mjs",
|
|
96
93
|
"prepublishOnly": "npm run build"
|
|
97
94
|
},
|
|
98
95
|
"dependencies": {
|
|
99
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
96
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
100
97
|
"@tree-sitter-grammars/tree-sitter-yaml": "^0.6.1",
|
|
101
98
|
"commander": "^15.0.0",
|
|
102
99
|
"cross-spawn": "^7.0.6",
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://www.hunchmemory.com",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.45.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.
|
|
16
|
+
"version": "1.45.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|
|
@@ -18,7 +18,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
|
|
21
|
-
export const BENCHMARK_BASELINE_REF = "
|
|
21
|
+
export const BENCHMARK_BASELINE_REF = "a1ad6d89631cce7c194f3f679abf7ed4e56d13e3";
|
|
22
22
|
export const BENCHMARK_SCHEMA = "hunch.md1a.performance.v1";
|
|
23
23
|
|
|
24
24
|
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const repos = [
|
|
4
|
-
"davesheffer/hunch",
|
|
5
|
-
"gitmem-dev/gitmem",
|
|
6
|
-
"ismaelkedir/knowit",
|
|
7
|
-
"weigibbor/mnemo",
|
|
8
|
-
"oldskultxo/aictx",
|
|
9
|
-
"riponcm/projectmem",
|
|
10
|
-
"Cranot/roam-code",
|
|
11
|
-
"blackwell-systems/knowing",
|
|
12
|
-
"markmhendrickson/neotoma",
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const distinctivePhrases = [
|
|
16
|
-
"Causal Merge Verdict",
|
|
17
|
-
"corrections become enforced",
|
|
18
|
-
"content-matched constraints",
|
|
19
|
-
"deterministic Change Gate for AI-assisted codebases",
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
let token = process.env.GITHUB_TOKEN?.trim();
|
|
23
|
-
// A rejected token must DEGRADE the run, never kill it: ambient CI/proxy tokens
|
|
24
|
-
// are often invalid for api.github.com, and the metadata half of this watch
|
|
25
|
-
// works unauthenticated. Flipped on the first 401 so every later call skips
|
|
26
|
-
// the bad credential instead of failing eight times.
|
|
27
|
-
let tokenRejected = false;
|
|
28
|
-
|
|
29
|
-
function headers() {
|
|
30
|
-
return {
|
|
31
|
-
Accept: "application/vnd.github+json",
|
|
32
|
-
"User-Agent": "hunch-competitive-watch",
|
|
33
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
34
|
-
...(token && !tokenRejected ? { Authorization: `Bearer ${token}` } : {}),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function github(path) {
|
|
39
|
-
let response = await fetch(`https://api.github.com${path}`, { headers: headers() });
|
|
40
|
-
if (response.status === 401 && token && !tokenRejected) {
|
|
41
|
-
tokenRejected = true;
|
|
42
|
-
process.stderr.write("competitive-watch: GITHUB_TOKEN rejected (401) — continuing unauthenticated; phrase search will be skipped.\n");
|
|
43
|
-
response = await fetch(`https://api.github.com${path}`, { headers: headers() });
|
|
44
|
-
}
|
|
45
|
-
if (!response.ok) {
|
|
46
|
-
const detail = (await response.text()).slice(0, 300).replaceAll("\n", " ");
|
|
47
|
-
throw new Error(`GitHub ${response.status} for ${path}: ${detail}`);
|
|
48
|
-
}
|
|
49
|
-
return response.json();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function repoSnapshot(repo) {
|
|
53
|
-
// One vanished/renamed repo is itself a signal worth a row — never a reason
|
|
54
|
-
// to lose the other seven.
|
|
55
|
-
try {
|
|
56
|
-
const data = await github(`/repos/${repo}`);
|
|
57
|
-
return {
|
|
58
|
-
repo,
|
|
59
|
-
created: data.created_at,
|
|
60
|
-
pushed: data.pushed_at,
|
|
61
|
-
stars: data.stargazers_count,
|
|
62
|
-
forks: data.forks_count,
|
|
63
|
-
issues: data.open_issues_count,
|
|
64
|
-
url: data.html_url,
|
|
65
|
-
};
|
|
66
|
-
} catch (error) {
|
|
67
|
-
return { repo, error: error instanceof Error ? error.message : String(error) };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function phraseSnapshot(phrase) {
|
|
72
|
-
const query = encodeURIComponent(`"${phrase}"`);
|
|
73
|
-
const data = await github(`/search/code?q=${query}&per_page=100`);
|
|
74
|
-
const external = data.items
|
|
75
|
-
.filter((item) => !item.repository.full_name.startsWith("davesheffer/"))
|
|
76
|
-
.map((item) => ({
|
|
77
|
-
repo: item.repository.full_name,
|
|
78
|
-
path: item.path,
|
|
79
|
-
url: item.html_url,
|
|
80
|
-
}));
|
|
81
|
-
return { phrase, total: data.total_count, external };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function render(snapshot, phrases) {
|
|
85
|
-
const lines = [
|
|
86
|
-
`# Competitive watch — ${new Date().toISOString()}`,
|
|
87
|
-
"",
|
|
88
|
-
"## Public repository signals",
|
|
89
|
-
"",
|
|
90
|
-
"| Repository | Created | Last push | Stars | Forks | Open issues |",
|
|
91
|
-
"| --- | --- | --- | ---: | ---: | ---: |",
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
for (const item of snapshot) {
|
|
95
|
-
if (item.error) {
|
|
96
|
-
lines.push(`| ${item.repo} | — | — | — | — | — |`);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
lines.push(
|
|
100
|
-
`| [${item.repo}](${item.url}) | ${item.created.slice(0, 10)} | ${item.pushed.slice(0, 10)} | ${item.stars} | ${item.forks} | ${item.issues} |`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
const failed = snapshot.filter((item) => item.error);
|
|
104
|
-
if (failed.length) {
|
|
105
|
-
lines.push("");
|
|
106
|
-
for (const item of failed) lines.push(`- ⚠ ${item.repo}: ${item.error}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
lines.push("", "## Distinctive phrase search", "");
|
|
110
|
-
if (!token) {
|
|
111
|
-
lines.push("Skipped: set `GITHUB_TOKEN` to enable authenticated GitHub code search.");
|
|
112
|
-
} else if (tokenRejected) {
|
|
113
|
-
lines.push("Skipped: `GITHUB_TOKEN` was rejected by api.github.com (401) — repo metadata above was collected unauthenticated. Provide a valid token (e.g. `GITHUB_TOKEN=\"$(gh auth token)\"`) to run the phrase-copy check.");
|
|
114
|
-
} else {
|
|
115
|
-
for (const result of phrases) {
|
|
116
|
-
lines.push(`- **${result.phrase}** — ${result.external.length} external indexed match(es)`);
|
|
117
|
-
for (const match of result.external) {
|
|
118
|
-
lines.push(` - [${match.repo} · ${match.path}](${match.url})`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
lines.push(
|
|
124
|
-
"",
|
|
125
|
-
"> Signals are leads, not copying findings. Re-check chronology and substantial similarity before drawing a conclusion.",
|
|
126
|
-
);
|
|
127
|
-
return `${lines.join("\n")}\n`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
const snapshot = [];
|
|
132
|
-
// Sequential on purpose: the first call settles token validity before the
|
|
133
|
-
// rest, and unauthenticated rate limits are too small to burst against.
|
|
134
|
-
for (const repo of repos) snapshot.push(await repoSnapshot(repo));
|
|
135
|
-
const phrases = token && !tokenRejected
|
|
136
|
-
? await Promise.all(distinctivePhrases.map(phraseSnapshot))
|
|
137
|
-
: [];
|
|
138
|
-
process.stdout.write(render(snapshot, phrases));
|
|
139
|
-
} catch (error) {
|
|
140
|
-
process.stderr.write(`competitive-watch: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
141
|
-
process.exitCode = 1;
|
|
142
|
-
}
|