@davesheffer/hunch 1.47.0 → 1.47.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +146 -46
- package/dist/cli/taskReport.d.ts +6 -0
- package/dist/cli/taskReport.js +31 -8
- package/dist/core/correction.d.ts +2 -0
- package/dist/core/correction.js +15 -1
- package/dist/core/subagentrole.d.ts +6 -0
- package/dist/core/subagentrole.js +32 -0
- 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/extractors/git.d.ts +10 -0
- package/dist/extractors/git.js +27 -0
- package/dist/integrations/health.js +1 -1
- 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.js +11 -10
- package/dist/mcp/taskReportTools.js +4 -4
- package/dist/serve/app.js +2 -2
- package/dist/taskReports.d.ts +1 -0
- package/dist/taskReports.js +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -23,7 +23,8 @@ import { Command } from "commander";
|
|
|
23
23
|
import { hunchPaths, hunchPathsForDir, findRoot, toPosixTarget, isDir, realpathNorm, repoRelativeTarget } from "../core/paths.js";
|
|
24
24
|
import { writeFileAtomic } from "../core/io.js";
|
|
25
25
|
import { boundedSpawnSync } from "../core/boundedSpawn.js";
|
|
26
|
-
import { looksLikeCorrection, CORRECTION_NUDGE } from "../core/correction.js";
|
|
26
|
+
import { looksLikeCorrection, CORRECTION_NUDGE, stripHostBlocks } from "../core/correction.js";
|
|
27
|
+
import { roleHasHunchTools } from "../core/subagentrole.js";
|
|
27
28
|
import { HUNCH_VERSION } from "../core/version.js";
|
|
28
29
|
import { registerIntegrationCommands } from "./integrations.js";
|
|
29
30
|
import { registerTaskReportCommands } from "./taskReport.js";
|
|
@@ -46,7 +47,7 @@ import { isParserLoadError, loadNativeTreeSitter } from "../extractors/nativeTre
|
|
|
46
47
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
47
48
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
48
49
|
import { readSynthesisPreference, resolveSynthesisProvider, selectProvider, SYNTH_PREFERENCES, writeSynthesisPreference, normalizeProviderName, } from "../synthesis/provider.js";
|
|
49
|
-
import { isGitRepo, isGitRepoRoot, sameGitPublication, sameRemoteUrl, canonicalRemoteUrl, repositoryUsesRemote, headSha, isolatedHeadSha, logSince, lastChangeDate, checkFreshnessRevision, teamPullIsFresh, firstCommitForFile, stagedFiles, workingFiles, commitFiles, asOfDate, stagedGateDiff, workingGateDiff, commitGateDiff, rangeFiles, rangeGateDiff, rangeCommits, revExists, revParse, commitAndPushHunch, pullHunchStatus, pullHunchStatusDetailed, describePullFailure, syncExistingHunch, gitUntrackCached, gitCommonDir, checkoutCommonDir, separateGitDirCandidate, separateGitDirLink, hooksDir, isLinkedWorktree, mainWorktreeRoot, gitMemoryLog, memoryMoveDiff, revertMemoryMove, pushCurrentBranch, commitChanges, commitRepairStatusAgainst, commitPatchIds, mergeRangeChanges, commitsExist } from "../extractors/git.js";
|
|
50
|
+
import { isGitRepo, isGitRepoRoot, sameGitPublication, sameRemoteUrl, canonicalRemoteUrl, repositoryUsesRemote, headSha, isolatedHeadSha, logSince, lastChangeDate, checkFreshnessRevision, teamPullIsFresh, firstCommitForFile, stagedFiles, workingFiles, commitFiles, asOfDate, stagedGateDiff, workingGateDiff, commitGateDiff, rangeFiles, rangeGateDiff, rangeCommits, revExists, revParse, sameFilesystemEntry, commitAndPushHunch, pullHunchStatus, pullHunchStatusDetailed, describePullFailure, syncExistingHunch, gitUntrackCached, gitCommonDir, checkoutCommonDir, separateGitDirCandidate, separateGitDirLink, hooksDir, isLinkedWorktree, mainWorktreeRoot, gitMemoryLog, memoryMoveDiff, revertMemoryMove, pushCurrentBranch, commitChanges, commitRepairStatusAgainst, commitPatchIds, mergeRangeChanges, commitsExist } from "../extractors/git.js";
|
|
50
51
|
import { runbookDraft } from "../core/runbookDraft.js";
|
|
51
52
|
import { parseMemoryLog } from "../core/memorylog.js";
|
|
52
53
|
import { renamesOf, planRepair, repairDecision, repairConstraint } from "../core/repair.js";
|
|
@@ -61,7 +62,7 @@ import { renderText, renderMarkdown, renderSarif, renderImpact, reportFailsStric
|
|
|
61
62
|
import { partitionReview, isReviewDraft, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
|
|
62
63
|
import { installPostCommitHook, installPreCommitHook, installPostMergeHook, installPostCheckoutHook, hookStatus, hookReport, hookInvocationLines, formatHookInstall, sharedHooksNote } from "../integrations/hooks.js";
|
|
63
64
|
import { ensureSharedOverlayPointer } from "../integrations/worktree.js";
|
|
64
|
-
import { flushCapture, flushMemoryHome, flushMemoryHomes, pinSharedRemote, sharedRemoteFor } from "../integrations/sync.js";
|
|
65
|
+
import { flushCapture, flushMemoryHome, flushMemoryHomes, pinSharedRemote, sharedRemoteFor, holdsOnDefaultBranch, HELD_ON_DEFAULT_BRANCH } from "../integrations/sync.js";
|
|
65
66
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
66
67
|
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS, upgradeManagedGitignore, describeGitignoreUpgrade, memoryIgnoreRule, describeMemoryIgnoreRule } from "../integrations/gitignore.js";
|
|
67
68
|
import { writeCiWorkflow } from "../integrations/ciAction.js";
|
|
@@ -210,7 +211,7 @@ registerReviewMemoryCommands(program, (records, repository, privateOnly) => {
|
|
|
210
211
|
for (const record of records)
|
|
211
212
|
store.putCapture("constraints", record, privateOnly);
|
|
212
213
|
store.reindex();
|
|
213
|
-
if (home === "public" && !store
|
|
214
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
214
215
|
refreshExistingGrounding(root, store);
|
|
215
216
|
pumpMemoryHome(store, root, home, `hunch: capture ${records.length} sourced review rule(s)`);
|
|
216
217
|
}, (repository, privateOnly) => {
|
|
@@ -447,8 +448,25 @@ function storeFor(opts = {}) {
|
|
|
447
448
|
const { store, teamPullStatus, teamPullDetail } = openTeamStore(root, opts);
|
|
448
449
|
return { store, root, teamPullStatus, teamPullDetail };
|
|
449
450
|
}
|
|
451
|
+
/** Whether a public memory flush commits the tracked grounding docs itself. When it
|
|
452
|
+
* won't (auto-commit off, or held on the default branch), a by-hand command refreshes
|
|
453
|
+
* them on disk instead, so CLAUDE.md never keeps enforcing what the command just changed. */
|
|
454
|
+
function publicFlushCommitsGrounding(store, root) {
|
|
455
|
+
return store.autoCommit && !holdsOnDefaultBranch(hunchPaths(root).hunch);
|
|
456
|
+
}
|
|
457
|
+
// A held public flush says so once per command, on stderr: stdout may be a --json payload.
|
|
458
|
+
let heldNoticePrinted = false;
|
|
459
|
+
function noteHeld() {
|
|
460
|
+
if (heldNoticePrinted)
|
|
461
|
+
return;
|
|
462
|
+
heldNoticePrinted = true;
|
|
463
|
+
console.error(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
464
|
+
}
|
|
450
465
|
function pumpMemoryHome(store, root, home, message) {
|
|
451
|
-
flushMemoryHome(store, hunchPaths(root).hunch, home, message);
|
|
466
|
+
const flush = flushMemoryHome(store, hunchPaths(root).hunch, home, message);
|
|
467
|
+
if (flush === "held")
|
|
468
|
+
noteHeld();
|
|
469
|
+
return flush;
|
|
452
470
|
}
|
|
453
471
|
function pumpPolicyHome(store, root, service, policyId, message) {
|
|
454
472
|
const home = service.repository.homeOfPolicy(policyId);
|
|
@@ -457,7 +475,9 @@ function pumpPolicyHome(store, root, service, policyId, message) {
|
|
|
457
475
|
pumpMemoryHome(store, root, home, message);
|
|
458
476
|
}
|
|
459
477
|
function pumpMemoryHomes(store, root, homes, message) {
|
|
460
|
-
flushMemoryHomes(store, hunchPaths(root).hunch, homes, message);
|
|
478
|
+
const flushes = flushMemoryHomes(store, hunchPaths(root).hunch, homes, message);
|
|
479
|
+
if (flushes.public === "held")
|
|
480
|
+
noteHeld();
|
|
461
481
|
}
|
|
462
482
|
// ---- init -----------------------------------------------------------------
|
|
463
483
|
program
|
|
@@ -767,7 +787,7 @@ program
|
|
|
767
787
|
// while the docs are still Git-clean so it can stage them atomically with
|
|
768
788
|
// the graph. Pre-refreshing would make the safety filter treat generated
|
|
769
789
|
// docs as user-dirty and leave them stranded outside the memory commit.
|
|
770
|
-
const healed = shouldAutoCommit ? [] : refreshExistingGrounding(root, store);
|
|
790
|
+
const healed = shouldAutoCommit && publicFlushCommitsGrounding(store, root) ? [] : refreshExistingGrounding(root, store);
|
|
771
791
|
console.log(`Indexed ${res.files} files:`);
|
|
772
792
|
console.log(` ${counts.symbols} symbols, ${counts.edges} edges, ${counts.components} components`);
|
|
773
793
|
for (const item of res.coverage) {
|
|
@@ -995,7 +1015,13 @@ program
|
|
|
995
1015
|
// each home's artifacts into at most one memory commit.
|
|
996
1016
|
const publicCapture = r.status === "written" && !toOverlay;
|
|
997
1017
|
const privateCapture = r.status === "written" && toOverlay;
|
|
998
|
-
const
|
|
1018
|
+
const publicNeedsFlush = publicCapture || graphRefreshed || publicRepairTouched;
|
|
1019
|
+
// The default-branch hold covers the post-commit hook too. Its baked-in --commit is
|
|
1020
|
+
// the installed auto-commit default, so only a --commit typed by hand overrides it.
|
|
1021
|
+
// Decided before the grounding refresh: a held capture leaves tracked docs at HEAD,
|
|
1022
|
+
// so `git commit -a` cannot commit counts whose record it leaves untracked.
|
|
1023
|
+
const publicHeld = doCommit && publicNeedsFlush && (opts.fromHook || opts.commit === undefined) && holdsOnDefaultBranch(hunchPaths(root).hunch);
|
|
1024
|
+
const groundingToStage = publicNeedsFlush && !publicHeld ? refreshCommittableGrounding(root, store) : [];
|
|
999
1025
|
if (!opts.fromHook && (r.status === "written" || graphRefreshed || publicRepairTouched)) {
|
|
1000
1026
|
const healed = refreshExistingGrounding(root, store);
|
|
1001
1027
|
const refreshed = [...new Set([...groundingToStage.map((file) => relative(root, file)), ...healed])];
|
|
@@ -1003,9 +1029,10 @@ program
|
|
|
1003
1029
|
console.log(` ↳ grounding refreshed: ${refreshed.join(", ")}`);
|
|
1004
1030
|
}
|
|
1005
1031
|
if (doCommit) {
|
|
1006
|
-
const publicNeedsFlush = publicCapture || graphRefreshed || publicRepairTouched;
|
|
1007
1032
|
const privateNeedsFlush = privateCapture || (privateCorrectionQueued && graphRefreshed) || g2Recorded > 0 || privateRepairTouched;
|
|
1008
|
-
|
|
1033
|
+
if (publicHeld && !opts.quiet)
|
|
1034
|
+
console.log(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
1035
|
+
const publicResult = publicNeedsFlush && !publicHeld
|
|
1009
1036
|
? commitAndPushHunch(hunchPaths(root).hunch, publicCapture
|
|
1010
1037
|
? `hunch: capture ${r.decision?.id ?? "decision"}`
|
|
1011
1038
|
: graphRefreshed
|
|
@@ -2283,12 +2310,13 @@ program
|
|
|
2283
2310
|
}
|
|
2284
2311
|
}
|
|
2285
2312
|
store.reindex();
|
|
2286
|
-
|
|
2287
|
-
flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: capture ${dec + con} inline intent(s)`);
|
|
2313
|
+
const flush = dec || con ? flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: capture ${dec + con} inline intent(s)`) : null;
|
|
2288
2314
|
if (!intents.length)
|
|
2289
2315
|
console.log("No `hunch-why:` / `hunch-rule:` comments found.");
|
|
2290
2316
|
else
|
|
2291
2317
|
console.log(`✓ captured ${dec} decision(s) + ${con} constraint(s) from inline comments${opts.private ? " [private overlay]" : ""}`);
|
|
2318
|
+
if (flush === "held")
|
|
2319
|
+
console.log(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
2292
2320
|
store.close();
|
|
2293
2321
|
});
|
|
2294
2322
|
// ---- conform (Architectural Conformance: does the code still satisfy recorded intent) ----
|
|
@@ -2330,7 +2358,7 @@ program
|
|
|
2330
2358
|
valid_from: now,
|
|
2331
2359
|
});
|
|
2332
2360
|
store.reindex();
|
|
2333
|
-
if (home === "public" && !store
|
|
2361
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
2334
2362
|
refreshExistingGrounding(root, store);
|
|
2335
2363
|
pumpMemoryHome(store, root, home, `hunch: record architectural invariant ${d.id}`);
|
|
2336
2364
|
console.log(`✓ recorded architectural invariant ${d.id}: "${opts.add}"`);
|
|
@@ -4047,7 +4075,7 @@ program
|
|
|
4047
4075
|
}
|
|
4048
4076
|
store.reindex();
|
|
4049
4077
|
const home = store.captureHome(!!opts.private);
|
|
4050
|
-
if (home === "public" && !store
|
|
4078
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
4051
4079
|
refreshExistingGrounding(root, store);
|
|
4052
4080
|
const flush = flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: import ${decisions.length} ADR(s) from ${dir}`);
|
|
4053
4081
|
const live = decisions.filter((d) => d.status === "accepted").length;
|
|
@@ -4066,6 +4094,8 @@ program
|
|
|
4066
4094
|
}
|
|
4067
4095
|
if (flush === "pushed")
|
|
4068
4096
|
console.log(" ↳ private memory committed + pushed");
|
|
4097
|
+
if (flush === "held")
|
|
4098
|
+
console.log(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
4069
4099
|
}
|
|
4070
4100
|
finally {
|
|
4071
4101
|
store.close();
|
|
@@ -4203,7 +4233,7 @@ program
|
|
|
4203
4233
|
store.reindex();
|
|
4204
4234
|
// Public grounding is a publishable artifact. Private rules stay local and
|
|
4205
4235
|
// are surfaced by local checks/MCP, never copied into committed agent docs.
|
|
4206
|
-
if (home === "public" && !store
|
|
4236
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
4207
4237
|
refreshExistingGrounding(root, store);
|
|
4208
4238
|
const flush = flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: capture ${c.id}`);
|
|
4209
4239
|
console.log(`✓ recorded ${c.severity} constraint ${c.id}: "${c.statement}" (scope: ${scope.join(", ") || "repo"})${opts.private ? " [private overlay]" : ""}`);
|
|
@@ -4217,6 +4247,8 @@ program
|
|
|
4217
4247
|
}
|
|
4218
4248
|
if (flush === "pushed")
|
|
4219
4249
|
console.log(" ↳ private memory committed + pushed");
|
|
4250
|
+
if (flush === "held")
|
|
4251
|
+
console.log(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
4220
4252
|
store.close();
|
|
4221
4253
|
});
|
|
4222
4254
|
// ---- test (failure-learning loop) -----------------------------------------
|
|
@@ -4960,7 +4992,7 @@ program
|
|
|
4960
4992
|
// is on, but no commit happens to carry that refresh when it's off, so do it here
|
|
4961
4993
|
// too — otherwise a retired constraint keeps showing as enforced on disk (the same
|
|
4962
4994
|
// gap record-constraint and `conform --add` close for their own capture path).
|
|
4963
|
-
if (home === "public" && !store
|
|
4995
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
4964
4996
|
refreshExistingGrounding(root, store);
|
|
4965
4997
|
// `--reason` goes in the commit BODY, never the subject: `hunch log`'s move
|
|
4966
4998
|
// classifier (src/core/memorylog.ts) regexes the commit SUBJECT for keywords
|
|
@@ -4968,11 +5000,14 @@ program
|
|
|
4968
5000
|
// in the subject could accidentally match one and misclassify the move.
|
|
4969
5001
|
const willCommit = home === "private" ? store.privateAutoCommit : store.autoCommit;
|
|
4970
5002
|
const message = `hunch: retire constraint ${id}${opts.reason ? `\n\n${opts.reason}` : ""}`;
|
|
4971
|
-
pumpMemoryHome(store, root, home, message);
|
|
5003
|
+
const flush = pumpMemoryHome(store, root, home, message);
|
|
4972
5004
|
console.log(`✓ ${retired.id} retired — window closed at ${retired.valid_to?.slice(0, 10)}.`);
|
|
4973
5005
|
if (opts.reason && !willCommit) {
|
|
4974
5006
|
console.log(" ⚠ --reason is not recorded anywhere: auto-commit is off, so no commit message captured it.");
|
|
4975
5007
|
}
|
|
5008
|
+
else if (opts.reason && flush === "held") {
|
|
5009
|
+
console.log(" ⚠ --reason is not recorded anywhere: the commit was held on the default branch, so no commit message captured it.");
|
|
5010
|
+
}
|
|
4976
5011
|
store.close();
|
|
4977
5012
|
});
|
|
4978
5013
|
// ---- firmness (agent-hook enforcement level) ------------------------------
|
|
@@ -5198,10 +5233,14 @@ program
|
|
|
5198
5233
|
if (!evt)
|
|
5199
5234
|
return;
|
|
5200
5235
|
eventName = evt.hook_event_name;
|
|
5201
|
-
|
|
5236
|
+
// Grounding reads the store of the checkout the event's file is in; the prompt's
|
|
5237
|
+
// task and the hook evidence stay with the process root that opened them.
|
|
5238
|
+
const processRoot = findRoot();
|
|
5239
|
+
const routed = hookEventRoot(processRoot, evt);
|
|
5240
|
+
const root = routed.root;
|
|
5202
5241
|
// The host delivered this event: runtime evidence for `hunch integrations check`,
|
|
5203
5242
|
// recorded before any policy decision so firmness never hides delivery itself.
|
|
5204
|
-
recordHookObservation(
|
|
5243
|
+
recordHookObservation(processRoot, provider, evt.hook_event_name, evt.tool_outcome?.status);
|
|
5205
5244
|
const paths = hunchPaths(root);
|
|
5206
5245
|
const firmness = readConfig(paths).firmness;
|
|
5207
5246
|
if (firmness === "off")
|
|
@@ -5225,7 +5264,7 @@ program
|
|
|
5225
5264
|
// Same rule as the pre-edit path: strict never grounds from stale team rules.
|
|
5226
5265
|
const stale = firmness === "strict" && opened.teamPullStatus && !teamPullIsFresh(opened.teamPullStatus);
|
|
5227
5266
|
if (!stale)
|
|
5228
|
-
shellGround = shellWriteGrounding(root, store, provider, evt, written);
|
|
5267
|
+
shellGround = shellWriteGrounding(root, store, provider, evt, written, processRoot);
|
|
5229
5268
|
}
|
|
5230
5269
|
}
|
|
5231
5270
|
else {
|
|
@@ -5418,9 +5457,11 @@ program
|
|
|
5418
5457
|
return flat.length > max ? `${flat.slice(0, max - 1).trimEnd()}…` : flat;
|
|
5419
5458
|
};
|
|
5420
5459
|
const type = (evt.agent_type ?? "").toLowerCase();
|
|
5460
|
+
// A role whose tool list leaves out Hunch's MCP tools is not told to call them.
|
|
5461
|
+
const tools = roleHasHunchTools(root, evt.agent_type ?? "");
|
|
5421
5462
|
const L = [];
|
|
5422
5463
|
const served = [];
|
|
5423
|
-
const route = routedCwd
|
|
5464
|
+
const route = routedCwd && tools
|
|
5424
5465
|
? `Worktree routing: call hunch_context first with cwd: ${JSON.stringify(routedCwd)}, and pass the same cwd to hunch_task, hunch_report, and every Hunch capture/write call in this delegated task.`
|
|
5425
5466
|
: null;
|
|
5426
5467
|
const activeProvider = provider;
|
|
@@ -5440,8 +5481,9 @@ program
|
|
|
5440
5481
|
served.push({ kind: "components", record_id: c.id, token_cost: Math.ceil([...line].length / 4) });
|
|
5441
5482
|
}
|
|
5442
5483
|
if (components.length > 12)
|
|
5443
|
-
L.push(`…and ${components.length - 12} more — hunch_structure() for the full map.`);
|
|
5444
|
-
|
|
5484
|
+
L.push(`…and ${components.length - 12} more${tools ? " — hunch_structure() for the full map" : ""}.`);
|
|
5485
|
+
if (tools)
|
|
5486
|
+
L.push("Orient: hunch_structure(target) · hunch_why(target) · hunch_context(task).");
|
|
5445
5487
|
}
|
|
5446
5488
|
else if (/plan|architect|design/.test(type)) {
|
|
5447
5489
|
// A plan drafted blind re-proposes what the graph already rejected.
|
|
@@ -5458,7 +5500,8 @@ program
|
|
|
5458
5500
|
L.push(line);
|
|
5459
5501
|
served.push({ kind: "decisions", record_id: d.id, token_cost: Math.ceil([...line].length / 4) });
|
|
5460
5502
|
}
|
|
5461
|
-
|
|
5503
|
+
if (tools)
|
|
5504
|
+
L.push("Before finalizing a plan: hunch_why(target) · hunch_current_decision(topic) · hunch_check_constraints(scope).");
|
|
5462
5505
|
}
|
|
5463
5506
|
else {
|
|
5464
5507
|
const sevRank = { blocking: 0, warning: 1, advisory: 2 };
|
|
@@ -5476,8 +5519,9 @@ program
|
|
|
5476
5519
|
served.push({ kind: "constraints", record_id: c.id, token_cost: Math.ceil([...line].length / 4) });
|
|
5477
5520
|
}
|
|
5478
5521
|
if (constraints.length > 8)
|
|
5479
|
-
L.push(`…and ${constraints.length - 8} more — hunch_check_constraints(scope) for your files.`);
|
|
5480
|
-
|
|
5522
|
+
L.push(`…and ${constraints.length - 8} more${tools ? " — hunch_check_constraints(scope) for your files" : "; the edit hook shows the ones your files touch"}.`);
|
|
5523
|
+
if (tools)
|
|
5524
|
+
L.push("Before editing: hunch_check_constraints(scope) · hunch_why(target). Orient: hunch_context(task).");
|
|
5481
5525
|
}
|
|
5482
5526
|
if (route)
|
|
5483
5527
|
L.push(route);
|
|
@@ -5582,9 +5626,13 @@ program
|
|
|
5582
5626
|
return;
|
|
5583
5627
|
}
|
|
5584
5628
|
const targets = editTargets(root, evt.tool_input);
|
|
5585
|
-
// Nothing inside the repo → nothing for Hunch to say
|
|
5586
|
-
|
|
5629
|
+
// Nothing inside the repo → nothing for Hunch to say, except for a file in a
|
|
5630
|
+
// linked worktree of this repository Hunch could not ground (never silently).
|
|
5631
|
+
if (!targets.length) {
|
|
5632
|
+
if (routed.ungrounded)
|
|
5633
|
+
emitContext(provider, "PreToolUse", routed.ungrounded);
|
|
5587
5634
|
return;
|
|
5635
|
+
}
|
|
5588
5636
|
// Grounding below stays about the first in-repo file; the strict gate checks every target.
|
|
5589
5637
|
const { abs, target } = targets[0];
|
|
5590
5638
|
// A compiled red→green probe is only meaningful if its red receipt exists
|
|
@@ -5646,12 +5694,12 @@ program
|
|
|
5646
5694
|
: `Hunch: this patch is denied for ${denials.length} files (${denials.map((d) => d.target).join(", ")}).\n${denials.map((d) => d.hit.reason).join("\n")}`;
|
|
5647
5695
|
emitDeny(provider, reason);
|
|
5648
5696
|
for (const d of denials)
|
|
5649
|
-
observeHookDenial(
|
|
5697
|
+
observeHookDenial(processRoot, provider, evt, d.target, d.hit);
|
|
5650
5698
|
return;
|
|
5651
5699
|
}
|
|
5652
5700
|
}
|
|
5653
5701
|
// advisory / firm / strict(non-blocking): inject the relevant Hunch slice.
|
|
5654
|
-
const grounded = fileGrounding(root, store, provider, evt, target, abs);
|
|
5702
|
+
const grounded = fileGrounding(root, store, provider, evt, target, abs, processRoot);
|
|
5655
5703
|
if (!grounded)
|
|
5656
5704
|
return; // no noise on files Hunch hasn't learned yet
|
|
5657
5705
|
emitContext(provider, "PreToolUse", grounded.text, grounded.mode === "full" ? grounded.recalled ?? undefined : undefined);
|
|
@@ -5830,7 +5878,7 @@ program
|
|
|
5830
5878
|
store.reindex();
|
|
5831
5879
|
if (home === "public") {
|
|
5832
5880
|
publicGroundingChanged = true;
|
|
5833
|
-
if (!store
|
|
5881
|
+
if (!publicFlushCommitsGrounding(store, root))
|
|
5834
5882
|
refreshExistingGrounding(root, store);
|
|
5835
5883
|
}
|
|
5836
5884
|
console.log(opts.approveImport
|
|
@@ -5890,7 +5938,7 @@ program
|
|
|
5890
5938
|
store.reindex();
|
|
5891
5939
|
if (home === "public") {
|
|
5892
5940
|
publicGroundingChanged = true;
|
|
5893
|
-
if (!store
|
|
5941
|
+
if (!publicFlushCommitsGrounding(store, root))
|
|
5894
5942
|
refreshExistingGrounding(root, store);
|
|
5895
5943
|
}
|
|
5896
5944
|
}
|
|
@@ -5909,7 +5957,7 @@ program
|
|
|
5909
5957
|
store.reindex();
|
|
5910
5958
|
if (home === "public") {
|
|
5911
5959
|
publicGroundingChanged = true;
|
|
5912
|
-
if (!store
|
|
5960
|
+
if (!publicFlushCommitsGrounding(store, root))
|
|
5913
5961
|
refreshExistingGrounding(root, store);
|
|
5914
5962
|
}
|
|
5915
5963
|
console.log(`✓ accepted ${opts.accept} (now ${source}, confidence 0.95${armed ? `, ${armed} tripwire(s) now blocking` : ""})`);
|
|
@@ -5968,7 +6016,7 @@ program
|
|
|
5968
6016
|
armedTotal += accepted.armed;
|
|
5969
6017
|
}
|
|
5970
6018
|
store.reindex();
|
|
5971
|
-
if (publicGroundingChanged && !store
|
|
6019
|
+
if (publicGroundingChanged && !publicFlushCommitsGrounding(store, root))
|
|
5972
6020
|
refreshExistingGrounding(root, store);
|
|
5973
6021
|
console.log(`✓ accepted ${ready.length} verified draft(s); ${armedTotal} tripwire(s) now blocking.`);
|
|
5974
6022
|
for (const it of ready)
|
|
@@ -6128,7 +6176,7 @@ program
|
|
|
6128
6176
|
}
|
|
6129
6177
|
if (accepted || rejected) {
|
|
6130
6178
|
store.reindex();
|
|
6131
|
-
if (publicAccepted && !store
|
|
6179
|
+
if (publicAccepted && !publicFlushCommitsGrounding(store, root))
|
|
6132
6180
|
refreshExistingGrounding(root, store);
|
|
6133
6181
|
pumpMemoryHomes(store, root, touchedHomes, "hunch: auto-review decision lifecycle");
|
|
6134
6182
|
}
|
|
@@ -7599,15 +7647,64 @@ function toRepoRel(root, abs) {
|
|
|
7599
7647
|
return "";
|
|
7600
7648
|
return rel;
|
|
7601
7649
|
}
|
|
7650
|
+
/** The repository root a hook event belongs to. Normally the process root, but an
|
|
7651
|
+
* edit to a file in another LINKED worktree of the same repository (or a subagent
|
|
7652
|
+
* started in one) belongs to that worktree's memory: grounded against the process
|
|
7653
|
+
* root it was dropped as "outside the repo", with no output at all. Re-roots only
|
|
7654
|
+
* to a worktree sharing this repository's git common dir that carries its own
|
|
7655
|
+
* .hunch/; one without it is reported as ungrounded rather than skipped silently.
|
|
7656
|
+
* Checkouts are told apart by their `.git` entry, never by a .hunch/ hit, so a
|
|
7657
|
+
* nested package .hunch/ inside this checkout (a monorepo) never displaces the
|
|
7658
|
+
* process root and its blocking constraints. Git is spawned only for a file in a
|
|
7659
|
+
* different checkout than the process root's. */
|
|
7660
|
+
function hookEventRoot(processRoot, evt) {
|
|
7661
|
+
const toolEvent = evt.hook_event_name === "PreToolUse" || evt.hook_event_name === "PostToolUse" || evt.hook_event_name === "PostToolUseFailure";
|
|
7662
|
+
const subject = evt.hook_event_name === "SubagentStart" ? evt.cwd
|
|
7663
|
+
: toolEvent ? (evt.tool_input?.patch_files?.[0]?.path ?? evt.tool_input?.file_path) : undefined;
|
|
7664
|
+
if (!subject || !isAbsolute(subject))
|
|
7665
|
+
return { root: processRoot };
|
|
7666
|
+
try {
|
|
7667
|
+
const ownerTop = checkoutTop(evt.hook_event_name === "SubagentStart" ? subject : dirname(subject));
|
|
7668
|
+
const processTop = checkoutTop(processRoot);
|
|
7669
|
+
if (!ownerTop || !processTop || sameFilesystemEntry(ownerTop, processTop))
|
|
7670
|
+
return { root: processRoot };
|
|
7671
|
+
const common = checkoutCommonDir(processRoot);
|
|
7672
|
+
const ownerCommon = common ? checkoutCommonDir(ownerTop) : "";
|
|
7673
|
+
if (!common || !ownerCommon || !sameFilesystemEntry(common, ownerCommon))
|
|
7674
|
+
return { root: processRoot };
|
|
7675
|
+
// The same Hunch root inside the other worktree: its top, or the package dir a
|
|
7676
|
+
// subdirectory-rooted process uses.
|
|
7677
|
+
const owner = join(ownerTop, relative(processTop, processRoot));
|
|
7678
|
+
if (isDir(join(owner, ".hunch")))
|
|
7679
|
+
return { root: owner };
|
|
7680
|
+
return { root: processRoot, ungrounded: `Hunch: ${toPosixTarget(subject)} is in linked worktree ${toPosixTarget(ownerTop)}, which has no .hunch/ — this edit proceeds ungrounded.` };
|
|
7681
|
+
}
|
|
7682
|
+
catch {
|
|
7683
|
+
return { root: processRoot };
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
/** The nearest directory at or above `start` holding a `.git` entry (a checkout's top;
|
|
7687
|
+
* a linked worktree's `.git` is a file), or "" outside every checkout. Pure fs. */
|
|
7688
|
+
function checkoutTop(start) {
|
|
7689
|
+
let cur = resolve(start);
|
|
7690
|
+
for (;;) {
|
|
7691
|
+
if (existsSync(join(cur, ".git")))
|
|
7692
|
+
return cur;
|
|
7693
|
+
const parent = dirname(cur);
|
|
7694
|
+
if (parent === cur)
|
|
7695
|
+
return "";
|
|
7696
|
+
cur = parent;
|
|
7697
|
+
}
|
|
7698
|
+
}
|
|
7602
7699
|
/** Files a shell command wrote get the grounding the edit tools would have
|
|
7603
7700
|
* delivered before the edit — late, but while the agent can still revise.
|
|
7604
7701
|
* Already-served grounding (a delta) is not repeated. */
|
|
7605
7702
|
const MAX_SHELL_GROUNDED = 3;
|
|
7606
|
-
function shellWriteGrounding(root, store, provider, evt, written) {
|
|
7703
|
+
function shellWriteGrounding(root, store, provider, evt, written, ledgerRoot = root) {
|
|
7607
7704
|
const parts = [];
|
|
7608
7705
|
const grounded = [];
|
|
7609
7706
|
for (const target of written.slice(0, MAX_SHELL_GROUNDED)) {
|
|
7610
|
-
const g = fileGrounding(root, store, provider, evt, target, join(root, target));
|
|
7707
|
+
const g = fileGrounding(root, store, provider, evt, target, join(root, target), ledgerRoot);
|
|
7611
7708
|
if (g?.mode === "full") {
|
|
7612
7709
|
parts.push(g.text);
|
|
7613
7710
|
grounded.push(target);
|
|
@@ -7677,19 +7774,21 @@ function promptTaskSelection(root, store, provider, evt) {
|
|
|
7677
7774
|
const taskId = hookReportTaskId(root, provider, evt);
|
|
7678
7775
|
if (!taskId)
|
|
7679
7776
|
return "";
|
|
7777
|
+
// Host-injected blocks (reminders, browser instructions) are not the task the human named.
|
|
7778
|
+
const humanText = stripHostBlocks(evt.prompt ?? "");
|
|
7680
7779
|
const selection = selectForTask(liveSelectionRecords({
|
|
7681
7780
|
decisions: store.recs("decisions"),
|
|
7682
7781
|
bugs: store.recs("bugs"),
|
|
7683
7782
|
constraints: store.recs("constraints"),
|
|
7684
7783
|
findings: store.recs("findings"),
|
|
7685
|
-
}),
|
|
7784
|
+
}), humanText, { root, pathExists: (p) => existsSync(join(root, p)) });
|
|
7686
7785
|
// Same task id again (a notification alias) keeps its own earlier selection; a
|
|
7687
7786
|
// new task that CONTINUES the session's previous one inherits that task's
|
|
7688
7787
|
// selection only on a bare follow-up ("continue", "go on"), so it is not left
|
|
7689
7788
|
// ungrounded. A substantive prompt names its own task: a same-session prompt
|
|
7690
7789
|
// inside the continuation window must not hide memory behind an old selection.
|
|
7691
7790
|
let previous = loadTaskSelection(taskId);
|
|
7692
|
-
if (!previous && isBareFollowUp(
|
|
7791
|
+
if (!previous && isBareFollowUp(humanText)) {
|
|
7693
7792
|
try {
|
|
7694
7793
|
const continues = readTaskReport(root, taskId).task.continues;
|
|
7695
7794
|
previous = continues ? loadTaskSelection(continues) : null;
|
|
@@ -7726,8 +7825,9 @@ const SIBLING_MEMORY_BUDGET_TOKENS = 800;
|
|
|
7726
7825
|
* retired code, doc anchors, recent tasks and sibling-fix lessons. `delta` when
|
|
7727
7826
|
* identical grounding was already served this session, null when Hunch knows
|
|
7728
7827
|
* nothing about the file. Serves the pre-edit hook and files a shell command
|
|
7729
|
-
* wrote (which never pass through the edit tools).
|
|
7730
|
-
|
|
7828
|
+
* wrote (which never pass through the edit tools). `ledgerRoot` is where the prompt's
|
|
7829
|
+
* task lives — the process root when the event was re-rooted to a linked worktree. */
|
|
7830
|
+
function fileGrounding(root, store, provider, evt, target, abs, ledgerRoot = root) {
|
|
7731
7831
|
// Decision-grounding for PROSE (doc≠graph): a markdown target that declares
|
|
7732
7832
|
// <!-- hunch:topic … --> anchors gets each topic's CURRENT decision — the
|
|
7733
7833
|
// graph outranks the prose being edited, and a stale pin is called out inline.
|
|
@@ -7744,7 +7844,7 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7744
7844
|
// at warning, not relevance guesses. No selection (a host without a prompt
|
|
7745
7845
|
// hook, a legacy session, a prompt that selected nothing) keeps the unfiltered
|
|
7746
7846
|
// grounding.
|
|
7747
|
-
const selection = loadTaskSelection(hookReportTaskId(
|
|
7847
|
+
const selection = loadTaskSelection(hookReportTaskId(ledgerRoot, provider, evt));
|
|
7748
7848
|
const assembled = store.assembleContext(target);
|
|
7749
7849
|
const selected = selection ? new Set(selection.qualifying) : null;
|
|
7750
7850
|
const ctx = selected ? {
|
|
@@ -7779,7 +7879,7 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7779
7879
|
const retired = store.retiredForFile(target).filter((r) => r.symbols.length || r.deps.length);
|
|
7780
7880
|
// Recent-task history is advisory; under a task selection it is dropped
|
|
7781
7881
|
// (`hunch task list` still has it).
|
|
7782
|
-
const recentTasks = selected ? [] : taskSelectionSupplements(store.selectTasksAuto(target, buildTaskRankingQuery(
|
|
7882
|
+
const recentTasks = selected ? [] : taskSelectionSupplements(store.selectTasksAuto(target, buildTaskRankingQuery(ledgerRoot, hookReportTaskId(ledgerRoot, provider, evt), target, { excludeTargetDeliveries: true })), target);
|
|
7783
7883
|
const hasContent = ctx.constraints.length ||
|
|
7784
7884
|
ctx.decisions.length ||
|
|
7785
7885
|
ctx.bugs.length ||
|
|
@@ -7843,7 +7943,7 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7843
7943
|
delivery_profile: envelope.profile, ranking_policy: envelope.ranking_policy,
|
|
7844
7944
|
})),
|
|
7845
7945
|
]);
|
|
7846
|
-
const reportTaskId = hookReportTaskId(
|
|
7946
|
+
const reportTaskId = hookReportTaskId(ledgerRoot, provider, evt);
|
|
7847
7947
|
// A new authoritative prompt gets its own full delivery. An earlier
|
|
7848
7948
|
// prompt's session-level delta cannot establish this task's receipt.
|
|
7849
7949
|
// A subagent reports to the prompt's task but starts with FRESH context:
|
|
@@ -7870,8 +7970,8 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7870
7970
|
const snapshots = snapshotDeliveredRecords(store, envelope);
|
|
7871
7971
|
// The first time a lesson reaches this prompt's task, tell the USER in one
|
|
7872
7972
|
// line (systemMessage); repeats of the same revision stay silent.
|
|
7873
|
-
recalled = reportPresentationEnabled(
|
|
7874
|
-
const occurrence = recordTaskDelivery(
|
|
7973
|
+
recalled = reportPresentationEnabled(ledgerRoot) ? renderRecalledLine(unseenLessons(ledgerRoot, reportTaskId, snapshots)) : null;
|
|
7974
|
+
const occurrence = recordTaskDelivery(ledgerRoot, reportTaskId, envelope, snapshots, undefined, target);
|
|
7875
7975
|
reportNotice = `\n\nHunch task ${reportTaskId} · delivery ${occurrence}. Inspect exact application references with hunch_report(task_id).`;
|
|
7876
7976
|
}
|
|
7877
7977
|
catch {
|
package/dist/cli/taskReport.d.ts
CHANGED
|
@@ -4,3 +4,9 @@ export declare function registerTaskReportCommands(program: Command, openStore:
|
|
|
4
4
|
store: HunchStore;
|
|
5
5
|
root: string;
|
|
6
6
|
}): void;
|
|
7
|
+
/** The root whose task ledger holds `id`: `root` itself, else a sibling worktree of
|
|
8
|
+
* the same repository that has a .hunch/. A hook rooted at the main checkout opens
|
|
9
|
+
* the task while the agent works in a linked worktree (or the reverse); the check
|
|
10
|
+
* still runs, and is snapshotted, where the caller stands. `root` when no sibling
|
|
11
|
+
* has the task, so the usual "task not found" stays the error. */
|
|
12
|
+
export declare function taskLedgerRoot(root: string, id: string): string;
|
package/dist/cli/taskReport.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { findRoot, isDir } from "../core/paths.js";
|
|
4
|
+
import { sameFilesystemEntry, worktreePaths } from "../extractors/git.js";
|
|
3
5
|
import { writeFileAtomic } from "../core/io.js";
|
|
4
|
-
import { finishReportTask, forgetReportTask, listReportTasks, listTaskSummaries, pruneReportHistory, readTaskReport, readLessonHistory, renderTaskStatusLine, startReportTask, summarizeTaskReport, taskReportStats } from "../core/taskReport.js";
|
|
6
|
+
import { finishReportTask, forgetReportTask, reportTaskExists, listReportTasks, listTaskSummaries, pruneReportHistory, readTaskReport, readLessonHistory, renderTaskStatusLine, startReportTask, summarizeTaskReport, taskReportStats } from "../core/taskReport.js";
|
|
5
7
|
import { promptTaskId } from "../core/taskReportHook.js";
|
|
6
|
-
import { DEFAULT_CHECK_TIMEOUT_MS, MAX_CHECK_TIMEOUT_MS,
|
|
8
|
+
import { DEFAULT_CHECK_TIMEOUT_MS, MAX_CHECK_TIMEOUT_MS, currentSource, runReportCheck, runReportConformance } from "../core/taskReportEvidence.js";
|
|
7
9
|
import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.js";
|
|
8
10
|
import { assertReportPath } from "../core/taskReportPaths.js";
|
|
9
11
|
import { publicTaskReport } from "../core/taskReportPublic.js";
|
|
@@ -61,7 +63,7 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
61
63
|
catch (error) {
|
|
62
64
|
graph = `\nGraph not saved: ${error.message}`;
|
|
63
65
|
}
|
|
64
|
-
console.log(renderTaskReport(readTaskReport(root, id,
|
|
66
|
+
console.log(renderTaskReport(readTaskReport(root, id, currentSource(root))) + graph);
|
|
65
67
|
});
|
|
66
68
|
task.command("list").description("Recent tasks observed in this repository with what Hunch delivered, saved, guarded, and checked")
|
|
67
69
|
.option("--limit <n>", "how many recent tasks (max 30)", "30")
|
|
@@ -69,7 +71,7 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
69
71
|
.action((opts) => {
|
|
70
72
|
const root = findRoot();
|
|
71
73
|
const limit = Number(opts.limit) || 30;
|
|
72
|
-
let summaries = listTaskSummaries(root, limit,
|
|
74
|
+
let summaries = listTaskSummaries(root, limit, currentSource(root));
|
|
73
75
|
// Graph records (this machine's or a teammate's) join the local ledger view.
|
|
74
76
|
try {
|
|
75
77
|
const opened = openStore();
|
|
@@ -161,7 +163,7 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
161
163
|
catch { /* a malformed host payload falls back to the most recent task */ }
|
|
162
164
|
let summary = null;
|
|
163
165
|
try {
|
|
164
|
-
const snapshot =
|
|
166
|
+
const snapshot = currentSource(root);
|
|
165
167
|
summary = taskId ? summarizeTaskReport(root, taskId, snapshot) : listTaskSummaries(root, 1, snapshot)[0] ?? null;
|
|
166
168
|
}
|
|
167
169
|
catch {
|
|
@@ -207,7 +209,9 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
207
209
|
process.on("SIGINT", interrupt);
|
|
208
210
|
process.on("SIGTERM", terminate);
|
|
209
211
|
try {
|
|
210
|
-
const
|
|
212
|
+
const here = findRoot();
|
|
213
|
+
const result = await runReportCheck(taskLedgerRoot(here, id), id, command, opts.label, seconds * 1000, {
|
|
214
|
+
workRoot: here,
|
|
211
215
|
signal: controller.signal,
|
|
212
216
|
onStdout: opts.json ? undefined : chunk => { process.stdout.write(chunk); },
|
|
213
217
|
onStderr: opts.json ? undefined : chunk => { process.stderr.write(chunk); },
|
|
@@ -258,7 +262,7 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
258
262
|
}
|
|
259
263
|
let report;
|
|
260
264
|
try {
|
|
261
|
-
report = readTaskReport(root, id,
|
|
265
|
+
report = readTaskReport(root, id, currentSource(root));
|
|
262
266
|
}
|
|
263
267
|
catch (error) {
|
|
264
268
|
// Not in this machine's ledger: the graph record (if any) is what remains.
|
|
@@ -277,6 +281,25 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
277
281
|
console.log(opts.json ? JSON.stringify(report, null, 2) : renderTaskReport(report));
|
|
278
282
|
});
|
|
279
283
|
}
|
|
284
|
+
/** The root whose task ledger holds `id`: `root` itself, else a sibling worktree of
|
|
285
|
+
* the same repository that has a .hunch/. A hook rooted at the main checkout opens
|
|
286
|
+
* the task while the agent works in a linked worktree (or the reverse); the check
|
|
287
|
+
* still runs, and is snapshotted, where the caller stands. `root` when no sibling
|
|
288
|
+
* has the task, so the usual "task not found" stays the error. */
|
|
289
|
+
export function taskLedgerRoot(root, id) {
|
|
290
|
+
if (reportTaskExists(root, id))
|
|
291
|
+
return root;
|
|
292
|
+
for (const other of worktreePaths(root)) {
|
|
293
|
+
if (sameFilesystemEntry(other, root) || !isDir(join(other, ".hunch")) || !existsSync(join(other, ".hunch-cache", "served.db")))
|
|
294
|
+
continue;
|
|
295
|
+
try {
|
|
296
|
+
if (reportTaskExists(other, id))
|
|
297
|
+
return other;
|
|
298
|
+
}
|
|
299
|
+
catch { /* unreadable ledger: keep looking */ }
|
|
300
|
+
}
|
|
301
|
+
return root;
|
|
302
|
+
}
|
|
280
303
|
function readStdinText() {
|
|
281
304
|
return new Promise(resolve => {
|
|
282
305
|
let data = "";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Constraint } from "./types.js";
|
|
2
|
+
/** The prompt with host-generated blocks removed: what the human actually typed. */
|
|
3
|
+
export declare function stripHostBlocks(prompt: string): string;
|
|
2
4
|
export declare function looksLikeCorrection(prompt: string | undefined | null): boolean;
|
|
3
5
|
/** One-line nudge appended to the UserPromptSubmit hook context when a prompt
|
|
4
6
|
* reads like a correction — surfaces the write tool so the rule gets ENFORCED,
|
package/dist/core/correction.js
CHANGED
|
@@ -29,10 +29,24 @@ const CORRECTION_PATTERNS = [
|
|
|
29
29
|
/\b(undo|revert) (that|this|it|your)\b/i,
|
|
30
30
|
/\b(not like that|don'?t do (that|this)( again)?|stop doing (that|this))\b/i,
|
|
31
31
|
];
|
|
32
|
+
/** Host-generated blocks a prompt can carry: a background task's completion
|
|
33
|
+
* (`<task-notification>`, which also wraps a subagent's hand-back report),
|
|
34
|
+
* injected context (`<system-reminder>`, hook output) and the browser
|
|
35
|
+
* extension's standing instructions (`<browser_instruction>`). Their words are not the
|
|
36
|
+
* human's, so "never use X" quoted from a constraint is not a correction. A tag
|
|
37
|
+
* left unclosed drops only the rest of its own line, never the human's text below. */
|
|
38
|
+
const HOST_TAGS = "task-notification|system-reminder|local-command-stdout|local-command-stderr|command-message|command-name|command-args|browser_instruction";
|
|
39
|
+
const HOST_BLOCK = new RegExp(`<(${HOST_TAGS})>[\\s\\S]*?<\\/\\1>`, "gi");
|
|
40
|
+
const HOST_OPEN_LINE = new RegExp(`<(?:${HOST_TAGS})>[^\\n]*`, "gi");
|
|
41
|
+
/** The prompt with host-generated blocks removed: what the human actually typed. */
|
|
42
|
+
export function stripHostBlocks(prompt) {
|
|
43
|
+
return prompt.replace(HOST_BLOCK, " ").replace(HOST_OPEN_LINE, " ");
|
|
44
|
+
}
|
|
32
45
|
export function looksLikeCorrection(prompt) {
|
|
33
46
|
if (!prompt || typeof prompt !== "string")
|
|
34
47
|
return false;
|
|
35
|
-
|
|
48
|
+
const human = stripHostBlocks(prompt);
|
|
49
|
+
return CORRECTION_PATTERNS.some((re) => re.test(human));
|
|
36
50
|
}
|
|
37
51
|
/** One-line nudge appended to the UserPromptSubmit hook context when a prompt
|
|
38
52
|
* reads like a correction — surfaces the write tool so the rule gets ENFORCED,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Whether a delegated agent can call Hunch's MCP tools. A named role defined in
|
|
2
|
+
* `.claude/agents/<type>.md` (project first, then user) with a `tools:` list that
|
|
3
|
+
* names no `mcp__hunch` tool and no wildcard cannot: telling it to call
|
|
4
|
+
* hunch_why only costs it a failed lookup. Built-in and unknown types inherit
|
|
5
|
+
* the parent's tools. */
|
|
6
|
+
export declare function roleHasHunchTools(root: string, agentType: string, home?: string): boolean;
|