@davesheffer/hunch 1.42.0 → 1.44.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 +14 -0
- package/dist/cli/index.js +206 -37
- package/dist/core/changeProof.js +2 -1
- package/dist/core/commitrepair.d.ts +9 -3
- package/dist/core/commitrepair.js +23 -6
- package/dist/core/correction.d.ts +1 -1
- package/dist/core/correction.js +4 -3
- package/dist/core/delivery.d.ts +12 -0
- package/dist/core/delivery.js +4 -4
- package/dist/core/docanchors.d.ts +11 -2
- package/dist/core/docanchors.js +232 -65
- package/dist/core/drift.d.ts +1 -1
- package/dist/core/drift.js +10 -3
- package/dist/core/eol.d.ts +2 -0
- package/dist/core/eol.js +6 -0
- package/dist/core/hookcache.d.ts +22 -0
- package/dist/core/hookcache.js +53 -2
- package/dist/core/procstart.d.ts +15 -4
- package/dist/core/procstart.js +56 -18
- package/dist/core/publication.js +17 -3
- package/dist/core/runbookDraft.d.ts +7 -0
- package/dist/core/runbookDraft.js +43 -0
- package/dist/core/taskReportHook.js +1 -1
- package/dist/core/taskSelection.d.ts +67 -0
- package/dist/core/taskSelection.js +196 -0
- package/dist/core/taskTouched.d.ts +2 -0
- package/dist/core/taskTouched.js +1 -1
- package/dist/extractors/git.d.ts +36 -6
- package/dist/extractors/git.js +86 -9
- package/dist/extractors/indexer.js +53 -14
- package/dist/extractors/k8sManifest.d.ts +31 -1
- package/dist/extractors/k8sManifest.js +133 -8
- package/dist/integrations/claudemd.d.ts +2 -0
- package/dist/integrations/claudemd.js +4 -2
- package/dist/integrations/gitignore.d.ts +7 -0
- package/dist/integrations/gitignore.js +18 -0
- package/dist/integrations/hooks.d.ts +15 -0
- package/dist/integrations/hooks.js +49 -1
- package/dist/integrations/mergeDriver.d.ts +2 -0
- package/dist/integrations/mergeDriver.js +2 -2
- package/dist/integrations/providers.d.ts +7 -0
- package/dist/integrations/providers.js +6 -5
- package/dist/integrations/scaffold.d.ts +5 -0
- package/dist/integrations/scaffold.js +14 -8
- package/dist/integrations/uninstall.d.ts +40 -0
- package/dist/integrations/uninstall.js +435 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.js +58 -4
- package/dist/mcp/taskReportTools.js +1 -1
- package/dist/serve/writelock.d.ts +7 -0
- package/dist/serve/writelock.js +30 -3
- package/dist/store/changeLedger.d.ts +5 -2
- package/dist/store/changeLedger.js +88 -37
- package/dist/store/hunchStore.js +4 -1
- package/package.json +3 -3
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -12,6 +12,10 @@ Hunch keeps that record in Git and makes the relevant parts available to your ag
|
|
|
12
12
|
|
|
13
13
|
The goal is simple: agents working from the same maintained record, with sources they can inspect. Hunch supplies memory and checks; the assistant still does the work.
|
|
14
14
|
|
|
15
|
+
[](https://www.youtube.com/watch?v=m2PM6pV7D9Y)
|
|
16
|
+
|
|
17
|
+
▶ **[Watch Hunch Memory in 2 minutes](https://www.youtube.com/watch?v=m2PM6pV7D9Y)**, then the [7-episode series](https://www.youtube.com/playlist?list=PLIXqtdE0LcNI) on setup, hooks, memory capture, delivery, rules, drift and teams.
|
|
18
|
+
|
|
15
19
|
## Start with your coding assistant
|
|
16
20
|
|
|
17
21
|
Requires **Node 22.13+** and a Git repository.
|
|
@@ -102,6 +106,16 @@ hunch integrations check --harness codex --require context
|
|
|
102
106
|
|
|
103
107
|
The first command checks configuration. The probe starts a fresh MCP process and reads memory. The context check requires observed hook delivery on the expected version, so run it after the new assistant session begins. `--require` fails when the named capability is not verified; none of these checks proves that a model followed the advice.
|
|
104
108
|
|
|
109
|
+
## Remove Hunch from a repository
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
hunch uninstall --dry-run # show what would change
|
|
113
|
+
hunch uninstall # remove what Hunch installed; keep .hunch/ (your memory)
|
|
114
|
+
hunch uninstall --purge # also delete .hunch/, the derived cache and Hunch's .gitignore blocks
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
It removes the instruction blocks, MCP entries, agent and git hooks, merge driver, slash commands and CI workflow that Hunch installed, and leaves your own content in those files alone. If a file it needs to edit can't be parsed, it changes nothing and says which file. It writes `.hunch-removed`: while that file exists, no Hunch command sets Hunch up here again. Commit it so every clone stays opted out; to reinstall, delete it and run `hunch init`. Close running `hunch mcp` / `hunch serve` processes first, so they can't recreate files.
|
|
118
|
+
|
|
105
119
|
## See what Hunch contributed
|
|
106
120
|
|
|
107
121
|
Task reports answer: what memory reached this task, what did the agent say it used, and what checks actually ran?
|
package/dist/cli/index.js
CHANGED
|
@@ -43,7 +43,8 @@ import { isParserLoadError, loadNativeTreeSitter } from "../extractors/nativeTre
|
|
|
43
43
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
44
44
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
45
45
|
import { readSynthesisPreference, resolveSynthesisProvider, selectProvider, SYNTH_PREFERENCES, writeSynthesisPreference, normalizeProviderName, } from "../synthesis/provider.js";
|
|
46
|
-
import { isGitRepo, isGitRepoRoot, sameGitPublication, sameRemoteUrl, canonicalRemoteUrl, repositoryUsesRemote, headSha, isolatedHeadSha, logSince, lastChangeDate, firstCommitForFile, stagedFiles, workingFiles, commitFiles, asOfDate, stagedGateDiff, workingGateDiff, commitGateDiff, rangeFiles, rangeGateDiff,
|
|
46
|
+
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, syncExistingHunch, gitUntrackCached, gitCommonDir, checkoutCommonDir, separateGitDirCandidate, separateGitDirLink, hooksDir, isLinkedWorktree, mainWorktreeRoot, gitMemoryLog, memoryMoveDiff, revertMemoryMove, pushCurrentBranch, commitChanges, commitRepairStatusAgainst, commitPatchIds, mergeRangeChanges, commitsExist } from "../extractors/git.js";
|
|
47
|
+
import { runbookDraft } from "../core/runbookDraft.js";
|
|
47
48
|
import { parseMemoryLog } from "../core/memorylog.js";
|
|
48
49
|
import { renamesOf, planRepair, repairDecision, repairConstraint } from "../core/repair.js";
|
|
49
50
|
import { orphanedCommitDecisions, planCommitRepair, repairDecisionCommit, pickRewrite, commitRepairReviewHash, mergeRewrites, firstFor, deadRewrites, resolvedRewriteIds, withoutDropped, addDropped, withheldForUnresolvableTo } from "../core/commitrepair.js";
|
|
@@ -61,6 +62,7 @@ import { flushCapture, flushMemoryHome, flushMemoryHomes, pinSharedRemote, share
|
|
|
61
62
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
62
63
|
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS, upgradeManagedGitignore, describeGitignoreUpgrade } from "../integrations/gitignore.js";
|
|
63
64
|
import { writeCiWorkflow } from "../integrations/ciAction.js";
|
|
65
|
+
import { applyUninstall, formatUninstallPlan, isOptedOut, OPT_OUT_FILE, optOutMessage, planUninstall } from "../integrations/uninstall.js";
|
|
64
66
|
import { updateClaudeMd, renderHunchSection } from "../integrations/claudemd.js";
|
|
65
67
|
import { HOOK_REMINDER } from "../core/hookText.js";
|
|
66
68
|
import { measureFootprint } from "../core/footprint.js";
|
|
@@ -91,12 +93,13 @@ import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpol
|
|
|
91
93
|
import { isHumanConfirmed } from "../core/strictgate.js";
|
|
92
94
|
import { appendEvent, readEvents } from "../core/events.js";
|
|
93
95
|
import { computeStats, formatStats } from "../core/stats.js";
|
|
94
|
-
import { injectionMode, resetSessionInjections } from "../core/hookcache.js";
|
|
96
|
+
import { clearTaskSelection, injectionMode, loadTaskSelection, resetSessionInjections, saveTaskSelection, taskSelectionEnabled } from "../core/hookcache.js";
|
|
97
|
+
import { isBareFollowUp, isFilterableSelectionId, liveSelectionRecords, selectForTask } from "../core/taskSelection.js";
|
|
95
98
|
import { recordServed, servedSummary } from "../core/served.js";
|
|
96
|
-
import { recordTaskDelivery, reportActivity, reportHash, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
|
|
99
|
+
import { readTaskReport, recordTaskDelivery, reportActivity, reportHash, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
|
|
97
100
|
import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
|
|
98
101
|
import { renderRecalledLine } from "../core/taskReportRender.js";
|
|
99
|
-
import { closeHookTask, hookReportTaskId, nativeHookCwd, settleHookSession, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
102
|
+
import { closeHookTask, hookReportTaskId, isNotificationPrompt, nativeHookCwd, settleHookSession, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
100
103
|
import { persistTaskRecord } from "../core/taskRecord.js";
|
|
101
104
|
import { recordHookObservation } from "../core/hookObservations.js";
|
|
102
105
|
import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
|
|
@@ -217,6 +220,26 @@ registerReviewMemoryCommands(program, (records, repository, privateOnly) => {
|
|
|
217
220
|
return { root, existing: store.captureHome(privateOnly) === "private"
|
|
218
221
|
? store.recs("constraints") : store.json.loadAll("constraints") };
|
|
219
222
|
});
|
|
223
|
+
// Opt-out gate (#449): a repository `hunch uninstall` opted out stays out. Setup
|
|
224
|
+
// and every other command refuse; the commands a leftover git or agent hook may
|
|
225
|
+
// still call exit 0 so they never break the user's commit, merge or agent turn
|
|
226
|
+
// (`hook` silently: its stdout is the agent's context). The merge drivers still
|
|
227
|
+
// run: a leftover driver that exits without writing %A would let git drop `theirs`.
|
|
228
|
+
const OPT_OUT_ALLOWED = new Set(["uninstall", "doctor", "help", "merge-driver", "merge-driver-grounding"]);
|
|
229
|
+
const OPT_OUT_QUIET = new Set(["hook", "sync", "check", "grounding", "repair-provenance", "workspaces"]);
|
|
230
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
231
|
+
let top = actionCommand;
|
|
232
|
+
while (top.parent && top.parent !== program)
|
|
233
|
+
top = top.parent;
|
|
234
|
+
const name = top.name();
|
|
235
|
+
if (OPT_OUT_ALLOWED.has(name))
|
|
236
|
+
return;
|
|
237
|
+
if (!isOptedOut(findRoot()))
|
|
238
|
+
return;
|
|
239
|
+
if (name !== "hook")
|
|
240
|
+
console.error(optOutMessage());
|
|
241
|
+
process.exit(OPT_OUT_QUIET.has(name) ? 0 : 1);
|
|
242
|
+
});
|
|
220
243
|
// Read an already-known update and schedule any registry refresh in a detached
|
|
221
244
|
// worker. No network handle is opened in this command's process, so the
|
|
222
245
|
// advisory cannot delay command completion or process exit.
|
|
@@ -509,6 +532,46 @@ program
|
|
|
509
532
|
console.log("Cold start? Seed from history: hunch backfill --since 90d");
|
|
510
533
|
console.log("\n⭐ If Hunch earns its keep, a star helps others find it → https://github.com/davesheffer/hunch");
|
|
511
534
|
});
|
|
535
|
+
// ---- uninstall ------------------------------------------------------------
|
|
536
|
+
program
|
|
537
|
+
.command("uninstall")
|
|
538
|
+
.description("Remove everything Hunch installed in this repository (instruction blocks, MCP entries, agent and git hooks, merge driver, slash commands, CI workflow) and opt the repository out so Hunch never sets itself up here again. Keeps .hunch/ (your engineering memory) unless --purge.")
|
|
539
|
+
.option("--dry-run", "print what would change and write nothing")
|
|
540
|
+
.option("--purge", "also delete .hunch/ (the engineering memory), the derived cache, Hunch's .gitignore blocks and this clone's pointer in the git dir")
|
|
541
|
+
.action((opts) => {
|
|
542
|
+
const root = findRoot();
|
|
543
|
+
const plan = planUninstall(root, { purge: !!opts.purge });
|
|
544
|
+
if (plan.refused.length) {
|
|
545
|
+
console.error(`Hunch uninstall at ${root}: nothing was changed — fix the file(s) below, then re-run.`);
|
|
546
|
+
for (const line of formatUninstallPlan(plan, !!opts.dryRun))
|
|
547
|
+
console.error(line);
|
|
548
|
+
process.exitCode = 1;
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const { applied, failed } = opts.dryRun ? { applied: plan.steps, failed: [] } : applyUninstall(plan);
|
|
552
|
+
// The header follows what actually ran: a failed opt-out marker stops
|
|
553
|
+
// everything, and saying "uninstalled" then would be a lie.
|
|
554
|
+
const header = opts.dryRun ? "Hunch uninstall (dry run — nothing written)"
|
|
555
|
+
: !failed.length ? "Hunch uninstalled"
|
|
556
|
+
: applied.some((s) => s.kind !== "keep") ? "Hunch partly uninstalled"
|
|
557
|
+
: "Hunch uninstall: nothing was changed";
|
|
558
|
+
console.log(`${header} at ${root}${opts.purge ? " (--purge)" : ""}`);
|
|
559
|
+
if (!plan.steps.some((s) => s.kind !== "keep"))
|
|
560
|
+
console.log(" nothing to remove — Hunch is not installed here");
|
|
561
|
+
for (const line of formatUninstallPlan({ ...plan, steps: applied }, !!opts.dryRun))
|
|
562
|
+
console.log(line);
|
|
563
|
+
if (failed.length) {
|
|
564
|
+
console.error(`
|
|
565
|
+
✗ ${failed.length} step(s) could not be applied — close running Hunch processes (\`hunch mcp\`, \`hunch serve\`, editors using them), then re-run \`hunch uninstall${opts.purge ? " --purge" : ""}\`:`);
|
|
566
|
+
for (const f of failed)
|
|
567
|
+
console.error(` ✗ ${f.path}: ${f.error}`);
|
|
568
|
+
process.exitCode = 1;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (!opts.dryRun && plan.steps.some((s) => s.path === OPT_OUT_FILE)) {
|
|
572
|
+
console.log(`\nCommit ${OPT_OUT_FILE} (and the removals) so every clone stays opted out. To reinstall later, delete ${OPT_OUT_FILE} and run \`hunch init\`.`);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
512
575
|
// ---- index ----------------------------------------------------------------
|
|
513
576
|
program
|
|
514
577
|
.command("index")
|
|
@@ -1974,11 +2037,14 @@ program
|
|
|
1974
2037
|
store.close();
|
|
1975
2038
|
return fail(`base ref "${base ?? ""}" not found (range: ${range})`);
|
|
1976
2039
|
}
|
|
1977
|
-
|
|
1978
|
-
const
|
|
2040
|
+
// Hunch's own capture commits and store/grounding files are bookkeeping, not steps (#425).
|
|
2041
|
+
const commits = rangeCommits(base, root, head);
|
|
2042
|
+
const { steps, files } = runbookDraft(commits, rangeFiles(base, root, head));
|
|
1979
2043
|
if (!steps.length && !files.length) {
|
|
1980
2044
|
store.close();
|
|
1981
|
-
return fail(
|
|
2045
|
+
return fail(commits.length
|
|
2046
|
+
? `range ${range} only holds Hunch's own capture commits and files; nothing to draft`
|
|
2047
|
+
: `no commits or changes in range ${range}`);
|
|
1982
2048
|
}
|
|
1983
2049
|
const now = new Date().toISOString();
|
|
1984
2050
|
const rec = {
|
|
@@ -4108,7 +4174,7 @@ program
|
|
|
4108
4174
|
.description("Flag changes that touch a do-not-break invariant — the local guardrail AND the CI/PR Constraint Guard. Also flags (advisory) symbols you add that already exist elsewhere — possible re-implementation/sprawl.")
|
|
4109
4175
|
.option("--staged", "check git staged files (default)")
|
|
4110
4176
|
.option("--working", "check all working-tree edits vs HEAD (staged, unstaged, and untracked files)")
|
|
4111
|
-
.option("--commit <sha>", "check a specific commit's files")
|
|
4177
|
+
.option("--commit <sha>", "check a specific commit's files (judged alone, against its parent; use --base to gate a whole branch)")
|
|
4112
4178
|
.option("--base <ref>", "check a PR/branch: files changed vs <ref> (e.g. origin/main) — for CI")
|
|
4113
4179
|
.option("--strict", "exit non-zero ONLY on a direct, high-confidence, non-stale blocking invariant (near/stale/low-confidence stay advisory)")
|
|
4114
4180
|
.option("--format <fmt>", "output: text (default) | markdown (a PR comment) | sarif (SARIF 2.1.0 for code scanning)", "text")
|
|
@@ -4124,11 +4190,14 @@ program
|
|
|
4124
4190
|
const sarif = opts.format === "sarif";
|
|
4125
4191
|
const emptyReport = { fileCount: 0, strict: !!opts.strict, direct: [], near: [], regressions: [], vetoes: [], redundant: [], strictBlockers: 0, regBlocking: 0, vetoBlocking: 0 };
|
|
4126
4192
|
const { store, root, teamPullStatus } = storeFor({ requireFreshTeamMemory: !!opts.strict && !opts.publicOnly });
|
|
4127
|
-
const teamFreshnessFailure = teamPullStatus !== null
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
? `strict check could not refresh the advertised team memory (${teamPullStatus}); refusing a stale pass`
|
|
4193
|
+
const teamFreshnessFailure = teamPullStatus !== null && !teamPullIsFresh(teamPullStatus)
|
|
4194
|
+
? `strict check could not refresh the advertised team memory (${teamPullStatus}); refusing a stale pass` +
|
|
4195
|
+
(teamPullStatus === "unconfigured" ? " — the shared memory remote has no published branch while this clone has memory; publish it with `hunch shared --sync`" : "")
|
|
4131
4196
|
: null;
|
|
4197
|
+
// An empty team remote is in sync with a never-committed clone, but it is also what
|
|
4198
|
+
// lost team memory looks like: never let that strict pass be silent (#427).
|
|
4199
|
+
if (teamPullStatus === "empty")
|
|
4200
|
+
console.error("· the advertised team memory has no published branch yet; this strict check ran with no team rules (publish with `hunch shared --sync` after the first capture)");
|
|
4132
4201
|
// Fail loudly on an unresolvable --base (e.g. CI forgot to fetch the base
|
|
4133
4202
|
// branch) — otherwise the diff is empty and the guard passes vacuously.
|
|
4134
4203
|
if (opts.base && !revExists(opts.base, root)) {
|
|
@@ -4194,13 +4263,14 @@ program
|
|
|
4194
4263
|
// blocking conformance predicate or an active policy guards.
|
|
4195
4264
|
// The COMPLETE diff (no synthesis byte budget); an incomplete one is flagged so
|
|
4196
4265
|
// content-matched blocking invariants fail closed instead of passing unseen.
|
|
4266
|
+
const freshAt = checkFreshnessRevision({ base: opts.base, commit: exactCommit }, root);
|
|
4197
4267
|
const gate = files.length
|
|
4198
4268
|
? (exactCommit ? commitGateDiff(exactCommit, root) : opts.base ? rangeGateDiff(opts.base, root) : opts.working ? workingGateDiff(root) : stagedGateDiff(root))
|
|
4199
4269
|
: { diff: "" };
|
|
4200
4270
|
const report = files.length
|
|
4201
4271
|
? store.buildCheckReport(files, gate.diff, {
|
|
4202
4272
|
strict: !!opts.strict,
|
|
4203
|
-
lastChange: (f) => lastChangeDate(f, root),
|
|
4273
|
+
lastChange: (f) => lastChangeDate(f, root, freshAt),
|
|
4204
4274
|
publicOnly: !!opts.publicOnly,
|
|
4205
4275
|
diffStatus: gate,
|
|
4206
4276
|
})
|
|
@@ -4341,7 +4411,8 @@ const vetoCmd = program
|
|
|
4341
4411
|
return;
|
|
4342
4412
|
}
|
|
4343
4413
|
const gate = opts.commit ? commitGateDiff(opts.commit, root) : opts.base ? rangeGateDiff(opts.base, root) : stagedGateDiff(root);
|
|
4344
|
-
const
|
|
4414
|
+
const freshAt = checkFreshnessRevision({ base: opts.base, commit: opts.commit }, root);
|
|
4415
|
+
const full = store.buildCheckReport(files, gate.diff, { strict: !!opts.strict, lastChange: (f) => lastChangeDate(f, root, freshAt), diffStatus: gate });
|
|
4345
4416
|
if (!full.vetoes.length) {
|
|
4346
4417
|
console.log(`✓ ${files.length} changed file(s) reverse no decision you rejected.`);
|
|
4347
4418
|
store.close();
|
|
@@ -4983,8 +5054,7 @@ program
|
|
|
4983
5054
|
const opened = openTeamStore(root, { requireFreshTeamMemory: firmness === "strict" });
|
|
4984
5055
|
store = opened.store;
|
|
4985
5056
|
// Same rule as the pre-edit path: strict never grounds from stale team rules.
|
|
4986
|
-
const stale = firmness === "strict" && opened.teamPullStatus
|
|
4987
|
-
&& opened.teamPullStatus !== "updated" && opened.teamPullStatus !== "current";
|
|
5057
|
+
const stale = firmness === "strict" && opened.teamPullStatus && !teamPullIsFresh(opened.teamPullStatus);
|
|
4988
5058
|
if (!stale)
|
|
4989
5059
|
shellGround = shellWriteGrounding(root, store, provider, evt, written);
|
|
4990
5060
|
}
|
|
@@ -5100,8 +5170,13 @@ program
|
|
|
5100
5170
|
// Reporting failure must not suppress the existing correction/policy reminder.
|
|
5101
5171
|
try {
|
|
5102
5172
|
const report = startHookReport(root, provider, evt);
|
|
5103
|
-
if (report)
|
|
5173
|
+
if (report) {
|
|
5104
5174
|
parts.push(report);
|
|
5175
|
+
store ??= new HunchStore(paths);
|
|
5176
|
+
const selected = promptTaskSelection(root, store, provider, evt);
|
|
5177
|
+
if (selected)
|
|
5178
|
+
parts.push(selected);
|
|
5179
|
+
}
|
|
5105
5180
|
}
|
|
5106
5181
|
catch { /* passive reporting remains fail-open */ }
|
|
5107
5182
|
// A task an earlier prompt of this session left open (interrupted before
|
|
@@ -5275,7 +5350,7 @@ program
|
|
|
5275
5350
|
// in the overlay. Private mode stays public-only: session transcripts travel
|
|
5276
5351
|
// further than a terminal.
|
|
5277
5352
|
const decisions = s.advisoryRecs("decisions");
|
|
5278
|
-
const {
|
|
5353
|
+
const { pendingReview } = nowData(decisions, 3);
|
|
5279
5354
|
const escalations = pendingEscalations(decisions);
|
|
5280
5355
|
escalations.push(...premiseEscalations(decisions, { now: new Date().toISOString(), exists: (p) => existsSync(join(paths.root, p)) }));
|
|
5281
5356
|
// liveness checked against the full store even in private mode — a
|
|
@@ -5312,17 +5387,6 @@ program
|
|
|
5312
5387
|
}
|
|
5313
5388
|
const L = [];
|
|
5314
5389
|
L.push(`🧠 Hunch orientation — ${decisions.length} decision(s) in the graph.`);
|
|
5315
|
-
if (recent.length) {
|
|
5316
|
-
L.push("Recent:");
|
|
5317
|
-
for (const r of recent)
|
|
5318
|
-
L.push(` ${r.date} [${r.status}] ${r.title} (${r.id})`);
|
|
5319
|
-
}
|
|
5320
|
-
if (roadmap.length) {
|
|
5321
|
-
L.push(`Roadmap (${roadmap.length} live proposed): ${roadmap.slice(0, 3).map((r) => (r.unconfirmed ? `${r.title} [unconfirmed, ${r.id}]` : r.title)).join(" · ")}${roadmap.length > 3 ? " · …" : ""}`);
|
|
5322
|
-
const unconfirmed = roadmap.filter((r) => r.unconfirmed).length;
|
|
5323
|
-
if (unconfirmed)
|
|
5324
|
-
L.push(`${unconfirmed} roadmap item(s) are unconfirmed agent testimony — the human confirms each with \`hunch review --confirm <id>${s.unified ? " --private" : ""}\`.`);
|
|
5325
|
-
}
|
|
5326
5390
|
if (pendingReview > 0)
|
|
5327
5391
|
L.push(`${pendingReview} legacy un-vouched draft(s) — adopt as advisory memory with \`hunch adopt-drafts\` (new captures auto-trust).`);
|
|
5328
5392
|
if (actionableEsc.length) {
|
|
@@ -5350,6 +5414,13 @@ program
|
|
|
5350
5414
|
}
|
|
5351
5415
|
if (evt.hook_event_name !== "PreToolUse")
|
|
5352
5416
|
return;
|
|
5417
|
+
// A shell command's writes are measured from its own start: a file written
|
|
5418
|
+
// before it (a parallel tool call, another process, a host that skipped the
|
|
5419
|
+
// last PostToolUse) is not this command's to be blamed for.
|
|
5420
|
+
if (evt.tool_name === "Bash" || evt.tool_name === "PowerShell") {
|
|
5421
|
+
refreshShellBaseline(root, evt.session_id, evt.agent_id);
|
|
5422
|
+
return;
|
|
5423
|
+
}
|
|
5353
5424
|
const targets = editTargets(root, evt.tool_input);
|
|
5354
5425
|
// Nothing inside the repo → nothing for Hunch to say.
|
|
5355
5426
|
if (!targets.length)
|
|
@@ -5373,8 +5444,7 @@ program
|
|
|
5373
5444
|
// never grounding from public/stale memory (dec_77d99014e0).
|
|
5374
5445
|
const opened = openTeamStore(root, { requireFreshTeamMemory: firmness === "strict" });
|
|
5375
5446
|
store = opened.store;
|
|
5376
|
-
if (firmness === "strict" && opened.teamPullStatus
|
|
5377
|
-
&& opened.teamPullStatus !== "updated" && opened.teamPullStatus !== "current") {
|
|
5447
|
+
if (firmness === "strict" && opened.teamPullStatus && !teamPullIsFresh(opened.teamPullStatus)) {
|
|
5378
5448
|
// A strict deny is only trustworthy when it includes the latest team
|
|
5379
5449
|
// rules. Offline/busy/unconfigured team memory is unavailable, so the
|
|
5380
5450
|
// non-blocking hook says so instead of denying from stale state.
|
|
@@ -6321,8 +6391,11 @@ program
|
|
|
6321
6391
|
console.log(`Pruned ${deadEntries.length} dead queue entr${deadEntries.length === 1 ? "y" : "ies"} (decision moved on, has no commit on record, or was superseded/rejected): ${deadEntries.map((r) => r.id).join(", ")}`);
|
|
6322
6392
|
}
|
|
6323
6393
|
const candidates = rangeResolves && !opts.apply && !opts.drop ? mergeRangeChanges(oldRef, newRef, root) : [];
|
|
6324
|
-
|
|
6325
|
-
|
|
6394
|
+
// Orphaned means HEAD no longer reaches the commit either: a historical
|
|
6395
|
+
// --range tip alone would flag every newer, perfectly reachable commit (#450).
|
|
6396
|
+
const orphaned = candidates.length ? orphanedCommitDecisions(decisions, (sha) => commitRepairStatusAgainst(sha, [newRef, "HEAD"], root)) : [];
|
|
6397
|
+
const patchIds = orphaned.length ? commitPatchIds([...orphaned.map((d) => d.commit), ...candidates.map((c) => c.sha)], root) : new Map();
|
|
6398
|
+
const freshPlan = candidates.length ? planCommitRepair(orphaned, candidates, (sha) => patchIds.get(sha) ?? null) : { rewrites: [], records: [] };
|
|
6326
6399
|
// Detection always queues a fresh match — local-only, never committed, so
|
|
6327
6400
|
// it's safe to persist unconditionally. This is what makes running
|
|
6328
6401
|
// opportunistically (from the hook) worth anything: the match survives
|
|
@@ -6475,12 +6548,17 @@ program
|
|
|
6475
6548
|
console.log(unresolvedCount
|
|
6476
6549
|
? `Would repair ${toApply.length - unresolvedCount} of ${toApply.length} listed:`
|
|
6477
6550
|
: `Would repair ${toApply.length} commit reference(s):`);
|
|
6551
|
+
// Match strength, so a reviewer can tell an identical change from a
|
|
6552
|
+
// file-overlap guess before applying it into shared memory (#450).
|
|
6553
|
+
const previewPatchIds = commitPatchIds(toApply.flatMap((r) => [r.from, r.to]), root);
|
|
6478
6554
|
for (const r of toApply) {
|
|
6479
6555
|
const reason = reasonQueued(r);
|
|
6556
|
+
const samePatch = !!previewPatchIds.get(r.from) && previewPatchIds.get(r.from) === previewPatchIds.get(r.to);
|
|
6480
6557
|
const label = reason === "invisible" ? "not visible this run — --apply would leave it queued"
|
|
6481
6558
|
: reason === "withheld" ? "proposed replacement commit doesn't resolve in this repository — --apply would leave it queued"
|
|
6482
6559
|
: reason === "duplicate" ? "another queued entry for this same decision would be applied instead — --apply would leave this one queued"
|
|
6483
|
-
:
|
|
6560
|
+
: samePatch ? "same patch-id: identical change"
|
|
6561
|
+
: "file match only — check the diff before applying";
|
|
6484
6562
|
console.log(` ${r.id} ${r.from} → ${r.to}${label ? ` (${label})` : ""}`);
|
|
6485
6563
|
}
|
|
6486
6564
|
const reviewHash = commitRepairReviewHash(queue, withheldRewrites(root, queue));
|
|
@@ -7126,6 +7204,13 @@ program
|
|
|
7126
7204
|
console.log(`· ${f.id} — ${f.detail}`);
|
|
7127
7205
|
console.log(`\nHeal: this is a HUMAN call — check \`hunch repair-provenance\` for a queued match (the post-merge hook detects these automatically while the commit is still resolvable, but never applies one unattended); run \`hunch repair-provenance --apply\` to confirm it, or if the commit is already gone, manually correct the decision's provenance or leave it as historical record.\n`);
|
|
7128
7206
|
}
|
|
7207
|
+
const fenceUnclosed = kind("doc-fence-unclosed");
|
|
7208
|
+
if (fenceUnclosed.length) {
|
|
7209
|
+
console.log(`${fenceUnclosed.length} markdown file(s) open a code fence that never closes, hiding the topic markers below it:\n`);
|
|
7210
|
+
for (const f of fenceUnclosed)
|
|
7211
|
+
console.log(`· ${f.id} — ${f.detail}`);
|
|
7212
|
+
console.log(`\nHeal: close the fence (a line of the same \`\`\` or ~~~ run) where the example ends; the markers after it become live again.\n`);
|
|
7213
|
+
}
|
|
7129
7214
|
console.log(`Hunch never rewrites prose for you; this is a read-only reconciliation report.`);
|
|
7130
7215
|
}
|
|
7131
7216
|
finally {
|
|
@@ -7248,7 +7333,19 @@ program
|
|
|
7248
7333
|
.command("doctor")
|
|
7249
7334
|
.description("Diagnose the environment (git, synthesis provider, index freshness).")
|
|
7250
7335
|
.action(async () => {
|
|
7251
|
-
const
|
|
7336
|
+
const doctorRoot = findRoot();
|
|
7337
|
+
if (isOptedOut(doctorRoot)) {
|
|
7338
|
+
console.log(`Hunch: not installed (opted out — ${OPT_OUT_FILE} is present).`);
|
|
7339
|
+
const left = planUninstall(doctorRoot).steps.filter((s) => s.kind === "edit" || s.kind === "delete");
|
|
7340
|
+
if (left.length) {
|
|
7341
|
+
console.log(` ⚠ ${left.length} Hunch leftover(s) remain; \`hunch uninstall\` removes them:`);
|
|
7342
|
+
for (const s of left)
|
|
7343
|
+
console.log(` - ${s.path}`);
|
|
7344
|
+
}
|
|
7345
|
+
console.log(` To reinstall: delete ${OPT_OUT_FILE}, then run \`hunch init\`.`);
|
|
7346
|
+
return;
|
|
7347
|
+
}
|
|
7348
|
+
const integrations = inspectIntegrations(doctorRoot);
|
|
7252
7349
|
console.log(formatIntegrationHealth(integrations));
|
|
7253
7350
|
// A pin npm cannot serve kills every npx launcher (hooks and MCP) before Hunch
|
|
7254
7351
|
// runs, and the hosts report nothing. Name it here; bounded, offline-safe.
|
|
@@ -7522,6 +7619,62 @@ function shellWriteGrounding(root, store, provider, evt, written) {
|
|
|
7522
7619
|
const sibling = parts.some((p) => p.startsWith(SIBLING_HEADING)) ? " It starts with a fix a same-shaped function elsewhere received and this file's copy never did: resolve it before you finish." : "";
|
|
7523
7620
|
return `Hunch: this shell command wrote ${grounded.join(", ")}${more}. Edits made outside the Edit/Write tools skip the pre-edit grounding, so it arrives now: re-check the change against it before relying on it.${sibling}\n\n${parts.join("\n\n")}`;
|
|
7524
7621
|
}
|
|
7622
|
+
/** Score the store against this prompt once per task (taskSelection.ts) and keep
|
|
7623
|
+
* the selected ids — never the prompt — for the task's file grounding. Returns
|
|
7624
|
+
* the short prompt-time list, or "" when nothing clears the threshold (silence).
|
|
7625
|
+
* A follow-up prompt widens the qualifying set rather than replacing it, so a
|
|
7626
|
+
* "go" cannot unselect the task's memory. */
|
|
7627
|
+
const TASK_SELECTION_TITLE_CHARS = 80;
|
|
7628
|
+
function promptTaskSelection(root, store, provider, evt) {
|
|
7629
|
+
// A host notification is not a task prompt: the earlier selection stands untouched.
|
|
7630
|
+
if (!taskSelectionEnabled() || isNotificationPrompt(evt.prompt))
|
|
7631
|
+
return "";
|
|
7632
|
+
const taskId = hookReportTaskId(root, provider, evt);
|
|
7633
|
+
if (!taskId)
|
|
7634
|
+
return "";
|
|
7635
|
+
const selection = selectForTask(liveSelectionRecords({
|
|
7636
|
+
decisions: store.recs("decisions"),
|
|
7637
|
+
bugs: store.recs("bugs"),
|
|
7638
|
+
constraints: store.recs("constraints"),
|
|
7639
|
+
findings: store.recs("findings"),
|
|
7640
|
+
}), evt.prompt ?? "", { root, pathExists: (p) => existsSync(join(root, p)) });
|
|
7641
|
+
// Same task id again (a notification alias) keeps its own earlier selection; a
|
|
7642
|
+
// new task that CONTINUES the session's previous one inherits that task's
|
|
7643
|
+
// selection only on a bare follow-up ("continue", "go on"), so it is not left
|
|
7644
|
+
// ungrounded. A substantive prompt names its own task: a same-session prompt
|
|
7645
|
+
// inside the continuation window must not hide memory behind an old selection.
|
|
7646
|
+
let previous = loadTaskSelection(taskId);
|
|
7647
|
+
if (!previous && isBareFollowUp(evt.prompt ?? "")) {
|
|
7648
|
+
try {
|
|
7649
|
+
const continues = readTaskReport(root, taskId).task.continues;
|
|
7650
|
+
previous = continues ? loadTaskSelection(continues) : null;
|
|
7651
|
+
}
|
|
7652
|
+
catch { /* no continuity; the prompt's own selection stands */ }
|
|
7653
|
+
}
|
|
7654
|
+
const qualifying = [...new Set([...(previous?.qualifying ?? []), ...selection.qualifying])];
|
|
7655
|
+
// An empty selection is no selection: the task keeps the unfiltered grounding.
|
|
7656
|
+
// Emptiness counts only the kinds grounding filters (decisions, bugs,
|
|
7657
|
+
// findings): constraints always pass, so a constraint-only set would hide every
|
|
7658
|
+
// other record anchored to the file. The prompt-time list may still print.
|
|
7659
|
+
if (!qualifying.some(isFilterableSelectionId)) {
|
|
7660
|
+
clearTaskSelection(taskId);
|
|
7661
|
+
}
|
|
7662
|
+
else {
|
|
7663
|
+
saveTaskSelection({
|
|
7664
|
+
task_id: taskId,
|
|
7665
|
+
qualifying,
|
|
7666
|
+
top: selection.qualifying.length ? selection.top.map((item) => item.id) : previous?.top ?? [],
|
|
7667
|
+
});
|
|
7668
|
+
}
|
|
7669
|
+
// An inherited selection was listed when its own prompt ran: not again.
|
|
7670
|
+
if (!selection.top.length)
|
|
7671
|
+
return "";
|
|
7672
|
+
const clip = (text) => {
|
|
7673
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
7674
|
+
return flat.length > TASK_SELECTION_TITLE_CHARS ? `${flat.slice(0, TASK_SELECTION_TITLE_CHARS - 1).trimEnd()}…` : flat;
|
|
7675
|
+
};
|
|
7676
|
+
return `Hunch memory for this task: ${selection.top.map((item) => `${item.id} — ${clip(item.title)}`).join(" · ")} (hunch_why(id) for detail)`;
|
|
7677
|
+
}
|
|
7525
7678
|
/** Memory budget beside a sibling lesson (the default is 1500 tokens). */
|
|
7526
7679
|
const SIBLING_MEMORY_BUDGET_TOKENS = 800;
|
|
7527
7680
|
/** The advisory grounding for one repo-relative file: the ranked memory slice,
|
|
@@ -7540,7 +7693,21 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7540
7693
|
}
|
|
7541
7694
|
catch { /* unreadable / not yet created — no doc grounding */ }
|
|
7542
7695
|
}
|
|
7543
|
-
|
|
7696
|
+
// A task whose prompt was scored (promptTaskSelection) gets the file's
|
|
7697
|
+
// decisions, bugs and findings conditioned on it. Constraints always pass (any
|
|
7698
|
+
// severity): they are scoped rules, including agent-recorded corrections capped
|
|
7699
|
+
// at warning, not relevance guesses. No selection (a host without a prompt
|
|
7700
|
+
// hook, a legacy session, a prompt that selected nothing) keeps the unfiltered
|
|
7701
|
+
// grounding.
|
|
7702
|
+
const selection = loadTaskSelection(hookReportTaskId(root, provider, evt));
|
|
7703
|
+
const assembled = store.assembleContext(target);
|
|
7704
|
+
const selected = selection ? new Set(selection.qualifying) : null;
|
|
7705
|
+
const ctx = selected ? {
|
|
7706
|
+
...assembled,
|
|
7707
|
+
decisions: assembled.decisions.filter((d) => selected.has(d.id)),
|
|
7708
|
+
bugs: assembled.bugs.filter((b) => selected.has(b.id)),
|
|
7709
|
+
findings: assembled.findings.filter((f) => selected.has(f.id)),
|
|
7710
|
+
} : assembled;
|
|
7544
7711
|
// Sibling fixes: a same-shaped function elsewhere was fixed and this copy
|
|
7545
7712
|
// never was — the concrete lesson a scoped constraint cannot carry.
|
|
7546
7713
|
const siblings = siblingGrounding(root, target, store.recs("symbols"), ctx.constraints.filter((c) => c.severity === "blocking" && c.scope.some((g) => pathMatchesGlob(target, g))).map((c) => ({ id: c.id, statement: c.statement })));
|
|
@@ -7565,7 +7732,9 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7565
7732
|
// from this file. No diff exists yet, so this is context — "don't re-add X" —
|
|
7566
7733
|
// not a block; the commit-time `hunch check` does the actual gating.
|
|
7567
7734
|
const retired = store.retiredForFile(target).filter((r) => r.symbols.length || r.deps.length);
|
|
7568
|
-
|
|
7735
|
+
// Recent-task history is advisory; under a task selection it is dropped
|
|
7736
|
+
// (`hunch task list` still has it).
|
|
7737
|
+
const recentTasks = selected ? [] : taskSelectionSupplements(store.selectTasksAuto(target, buildTaskRankingQuery(root, hookReportTaskId(root, provider, evt), target, { excludeTargetDeliveries: true })), target);
|
|
7569
7738
|
const hasContent = ctx.constraints.length ||
|
|
7570
7739
|
ctx.decisions.length ||
|
|
7571
7740
|
ctx.bugs.length ||
|
package/dist/core/changeProof.js
CHANGED
|
@@ -313,7 +313,8 @@ export function deriveChangeProof(root, store, baseRef, resultRef = "HEAD", opti
|
|
|
313
313
|
// A diff cut at MAX_DIFF_BYTES is a prefix: content-matched blocking rules over the
|
|
314
314
|
// files it omits fail closed rather than read as compliant (dec_20db57c576).
|
|
315
315
|
diffStatus: diff.gaps.length ? { incomplete: `the guard diff exceeded ${MAX_DIFF_BYTES} bytes and was cut`, truncated: true } : undefined,
|
|
316
|
-
|
|
316
|
+
// Freshness as of the base: the change under proof must not make its own guard stale (#426).
|
|
317
|
+
lastChange: (path) => lastChangeAt(root, change.base_revision, path),
|
|
317
318
|
});
|
|
318
319
|
const allStrictBlockerIds = sortedUnique([
|
|
319
320
|
...guardReport.direct.filter((item) => item.strictBlocks).map((item) => item.id),
|
|
@@ -30,9 +30,15 @@ export interface CommitRepairPlan {
|
|
|
30
30
|
* doesn't resolve at all ("unresolvable") is out of repair scope, and a git
|
|
31
31
|
* failure ("unknown") must never be mistaken for repair-eligible. */
|
|
32
32
|
export declare function orphanedCommitDecisions(decisions: readonly Decision[], status: (sha: string) => CommitRepairStatus): Decision[];
|
|
33
|
-
/** Match each orphaned decision to exactly one candidate
|
|
34
|
-
*
|
|
35
|
-
|
|
33
|
+
/** Match each orphaned decision to exactly one candidate. A candidate with the
|
|
34
|
+
* same `git patch-id --stable` as the orphaned commit (a rebase- or
|
|
35
|
+
* single-commit squash-merge of it) is the strong match and wins. Only when
|
|
36
|
+
* no candidate shares the patch-id does it fall back to the one candidate
|
|
37
|
+
* whose changed files are a superset of the decision's related_files
|
|
38
|
+
* (non-empty, no globs) -- needed for a squash of several commits, whose
|
|
39
|
+
* patch-id never equals any one of them, but weak: shared files like
|
|
40
|
+
* CHANGELOG.md often have one wrong superset (#450). */
|
|
41
|
+
export declare function planCommitRepair(orphaned: readonly Decision[], candidates: readonly CommitCandidate[], patchIdOf?: (sha: string) => string | null): CommitRepairPlan;
|
|
36
42
|
/** Whether a queued rewrite is still worth ASKING about: the decision it
|
|
37
43
|
* names still exists, still cites the commit the entry expects to replace (a
|
|
38
44
|
* decision that already moved on would make repairDecisionCommit's own
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* A squash-merge produces one new commit whose parent is the pre-merge target
|
|
5
5
|
* tip — the original per-commit SHAs it squashed are never ancestors of it.
|
|
6
6
|
* This module matches an orphaned decision to the ONE newly-merged commit
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* repair.ts's
|
|
7
|
+
* with the same patch-id, or failing that the ONE whose changed files are a
|
|
8
|
+
* superset of the decision's related_files. Zero or multiple qualifying
|
|
9
|
+
* candidates means "don't guess" — same discipline as repair.ts's
|
|
10
|
+
* rename-repair.
|
|
10
11
|
*/
|
|
11
12
|
import { createHash } from "node:crypto";
|
|
12
13
|
import { replaceExact } from "./refrepair.js";
|
|
@@ -33,11 +34,27 @@ export function orphanedCommitDecisions(decisions, status) {
|
|
|
33
34
|
return status(d.commit) === "orphaned";
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
|
-
/** Match each orphaned decision to exactly one candidate
|
|
37
|
-
*
|
|
38
|
-
|
|
37
|
+
/** Match each orphaned decision to exactly one candidate. A candidate with the
|
|
38
|
+
* same `git patch-id --stable` as the orphaned commit (a rebase- or
|
|
39
|
+
* single-commit squash-merge of it) is the strong match and wins. Only when
|
|
40
|
+
* no candidate shares the patch-id does it fall back to the one candidate
|
|
41
|
+
* whose changed files are a superset of the decision's related_files
|
|
42
|
+
* (non-empty, no globs) -- needed for a squash of several commits, whose
|
|
43
|
+
* patch-id never equals any one of them, but weak: shared files like
|
|
44
|
+
* CHANGELOG.md often have one wrong superset (#450). */
|
|
45
|
+
export function planCommitRepair(orphaned, candidates, patchIdOf = () => null) {
|
|
39
46
|
const rewrites = [];
|
|
40
47
|
for (const d of orphaned) {
|
|
48
|
+
const patchId = patchIdOf(d.commit);
|
|
49
|
+
if (patchId) {
|
|
50
|
+
const same = candidates.filter((c) => patchIdOf(c.sha) === patchId);
|
|
51
|
+
if (same.length === 1) {
|
|
52
|
+
rewrites.push({ id: d.id, from: d.commit, to: same[0].sha });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (same.length > 1)
|
|
56
|
+
continue; // ambiguous: never guess
|
|
57
|
+
}
|
|
41
58
|
const files = (d.related_files ?? []).filter(isExactPath);
|
|
42
59
|
if (!files.length)
|
|
43
60
|
continue;
|
|
@@ -30,7 +30,7 @@ export interface CorrectionInput {
|
|
|
30
30
|
* enough — any agent can mint and consume one. Determines the TIER, never whether
|
|
31
31
|
* the write lands: an un-vouched correction still records immediately and still
|
|
32
32
|
* surfaces at edit time and in CI. Only the authority to DENY waits for a countersign
|
|
33
|
-
* (`hunch review --confirm <id>`). */
|
|
33
|
+
* (`hunch review --confirm <id>`). Omitted means false. */
|
|
34
34
|
vouched?: boolean;
|
|
35
35
|
}
|
|
36
36
|
/** Default rationales, exported so a later human countersign can replace the testimony
|
package/dist/core/correction.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* agent hook so they are unit-testable without spinning either up:
|
|
6
6
|
* - looksLikeCorrection(): does a user prompt read like "no / that's wrong /
|
|
7
7
|
* never do X" — the cue to nudge the agent to persist it.
|
|
8
|
-
* - buildCorrectionConstraint(): mint the Constraint record (human-confirmed
|
|
9
|
-
* scoped conservatively) that the pre-edit hook + CI guard then enforce.
|
|
8
|
+
* - buildCorrectionConstraint(): mint the Constraint record (human-confirmed
|
|
9
|
+
* only when vouched, scoped conservatively) that the pre-edit hook + CI guard then enforce.
|
|
10
10
|
*/
|
|
11
11
|
import { isAbsolute } from "node:path";
|
|
12
12
|
import { constraintId } from "./ids.js";
|
|
@@ -108,7 +108,8 @@ export function buildCorrectionConstraint(input, now) {
|
|
|
108
108
|
// still recorded immediately and still held against every assistant at edit time and
|
|
109
109
|
// in CI — Never Twice keeps its promise. What waits for a countersign is only the
|
|
110
110
|
// authority to DENY.
|
|
111
|
-
|
|
111
|
+
// Fails closed: an omitted flag is testimony, never a signature.
|
|
112
|
+
const vouched = input.vouched === true;
|
|
112
113
|
if (!vouched && severity === "blocking")
|
|
113
114
|
severity = "warning";
|
|
114
115
|
return {
|
package/dist/core/delivery.d.ts
CHANGED
|
@@ -112,6 +112,18 @@ export interface DeliveryOptions {
|
|
|
112
112
|
/** Injectable for deterministic tests. Omit to use the local Git graph. */
|
|
113
113
|
commitReachability?: (commit: string) => CommitReachability;
|
|
114
114
|
}
|
|
115
|
+
export declare const SEVERITY: {
|
|
116
|
+
readonly advisory: 1;
|
|
117
|
+
readonly warning: 2;
|
|
118
|
+
readonly blocking: 3;
|
|
119
|
+
readonly low: 1;
|
|
120
|
+
readonly medium: 2;
|
|
121
|
+
readonly high: 3;
|
|
122
|
+
readonly critical: 4;
|
|
123
|
+
};
|
|
124
|
+
export declare const PROFILE_BASE_SCORE: Record<DeliveryProfile, Record<DeliveryKind, number>>;
|
|
125
|
+
export declare const TASK_STOP_WORDS: Set<string>;
|
|
126
|
+
export declare function lexicalTokens(value: string): Set<string>;
|
|
115
127
|
/** Validate the public receipt without trusting a caller-supplied identity. */
|
|
116
128
|
export declare function assertDeliveryEnvelope(envelope: DeliveryEnvelope): void;
|
|
117
129
|
/** The string a hash-based injection dedup (`injectionMode`'s `hashInput`) must
|
package/dist/core/delivery.js
CHANGED
|
@@ -16,7 +16,7 @@ import { LANDSCAPE_FRAGMENT_SCHEMA_VERSION, assertLandscapeDeliveryFragment, cre
|
|
|
16
16
|
export const DELIVERY_ENVELOPE_SCHEMA_VERSION = "hunch.delivery-envelope/1";
|
|
17
17
|
export const DELIVERY_PROFILE_POLICY_VERSION = "hunch.delivery-profile/1";
|
|
18
18
|
export const DELIVERY_PROFILES = ["builder", "reviewer", "architect"];
|
|
19
|
-
const SEVERITY = { advisory: 1, warning: 2, blocking: 3, low: 1, medium: 2, high: 3, critical: 4 };
|
|
19
|
+
export const SEVERITY = { advisory: 1, warning: 2, blocking: 3, low: 1, medium: 2, high: 3, critical: 4 };
|
|
20
20
|
const MIN_ADVISORY_CONFIDENCE = 0.5;
|
|
21
21
|
const MIN_UNCONDITIONED_CONFIDENCE = 0.7;
|
|
22
22
|
const MAX_ACTIONABLE_HYPOTHESES = 2;
|
|
@@ -24,7 +24,7 @@ const MAX_PROFILE_HEADLINES = 8;
|
|
|
24
24
|
/** How far a supplement's text is clipped in the rendered line. Shared so
|
|
25
25
|
* `deliveryDedupeInput` can reconstruct that exact line to project over it. */
|
|
26
26
|
const SUPPLEMENT_HEADLINE_CHARS = 700;
|
|
27
|
-
const PROFILE_BASE_SCORE = {
|
|
27
|
+
export const PROFILE_BASE_SCORE = {
|
|
28
28
|
builder: {
|
|
29
29
|
constraints: 900,
|
|
30
30
|
decisions: 800,
|
|
@@ -50,7 +50,7 @@ const PROFILE_BASE_SCORE = {
|
|
|
50
50
|
relationships: 825,
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
|
-
const TASK_STOP_WORDS = new Set([
|
|
53
|
+
export const TASK_STOP_WORDS = new Set([
|
|
54
54
|
"a", "an", "and", "are", "as", "at", "be", "been", "but", "by", "can", "does", "for", "from",
|
|
55
55
|
"has", "have", "in", "into", "is", "it", "its", "of", "on", "or", "that", "the", "this", "to",
|
|
56
56
|
"use", "uses", "using", "was", "when", "where", "which", "while", "with", "without",
|
|
@@ -91,7 +91,7 @@ function stemToken(token) {
|
|
|
91
91
|
return token.slice(0, -1);
|
|
92
92
|
return token;
|
|
93
93
|
}
|
|
94
|
-
function lexicalTokens(value) {
|
|
94
|
+
export function lexicalTokens(value) {
|
|
95
95
|
const expanded = value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase();
|
|
96
96
|
const words = expanded.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
97
97
|
return new Set(words.map(stemToken).filter((token) => token.length >= 3 && !TASK_STOP_WORDS.has(token)));
|