@davesheffer/hunch 1.39.0 → 1.39.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 +193 -65
- package/dist/cli/integrations.js +10 -0
- package/dist/client/state.d.ts +2 -1
- package/dist/client/state.js +1 -0
- package/dist/core/agenthook.d.ts +14 -0
- package/dist/core/agenthook.js +55 -8
- package/dist/core/capturetoken.d.ts +30 -3
- package/dist/core/capturetoken.js +29 -3
- package/dist/core/changeProof.js +5 -1
- package/dist/core/checkreport.d.ts +7 -0
- package/dist/core/checkreport.js +20 -3
- package/dist/core/compare.js +3 -2
- package/dist/core/correction.d.ts +10 -4
- package/dist/core/correction.js +7 -4
- package/dist/core/countersign.d.ts +28 -0
- package/dist/core/countersign.js +50 -0
- package/dist/core/reviewqueue.js +6 -1
- package/dist/core/spawnCommand.js +41 -9
- package/dist/core/stateHttp.d.ts +1 -1
- package/dist/core/stateHttp.js +3 -1
- package/dist/core/taskReportEvidence.js +31 -11
- package/dist/core/topics.js +1 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/core/workspace.d.ts +23 -1
- package/dist/core/workspace.js +31 -7
- package/dist/extractors/diff.d.ts +34 -0
- package/dist/extractors/diff.js +147 -5
- package/dist/extractors/git.d.ts +40 -11
- package/dist/extractors/git.js +147 -43
- package/dist/extractors/workspaces.d.ts +10 -0
- package/dist/extractors/workspaces.js +92 -15
- package/dist/integrations/gitignore.d.ts +27 -2
- package/dist/integrations/gitignore.js +103 -17
- package/dist/integrations/hooks.d.ts +61 -7
- package/dist/integrations/hooks.js +330 -43
- package/dist/integrations/scaffold.js +1 -1
- package/dist/integrations/workspaceLedger.d.ts +23 -3
- package/dist/integrations/workspaceLedger.js +114 -8
- package/dist/mcp/server.js +115 -56
- package/dist/serve/app.d.ts +4 -0
- package/dist/serve/app.js +56 -36
- package/dist/store/hunchStore.d.ts +4 -2
- package/dist/store/hunchStore.js +23 -6
- package/dist/store/stateBinding.js +111 -42
- package/dist/wiki/wiki.d.ts +7 -0
- package/dist/wiki/wiki.js +19 -8
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import { assertCompleteRepoScan, indexRepo, scanRepo } from "../extractors/index
|
|
|
40
40
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
41
41
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
42
42
|
import { readSynthesisPreference, resolveSynthesisProvider, selectProvider, SYNTH_PREFERENCES, writeSynthesisPreference, normalizeProviderName, } from "../synthesis/provider.js";
|
|
43
|
-
import { isGitRepo, isGitRepoRoot, sameGitPublication, sameRemoteUrl, canonicalRemoteUrl, repositoryUsesRemote, headSha, isolatedHeadSha, logSince, lastChangeDate, firstCommitForFile, stagedFiles, workingFiles, commitFiles, asOfDate,
|
|
43
|
+
import { isGitRepo, isGitRepoRoot, sameGitPublication, sameRemoteUrl, canonicalRemoteUrl, repositoryUsesRemote, headSha, isolatedHeadSha, logSince, lastChangeDate, firstCommitForFile, stagedFiles, workingFiles, commitFiles, asOfDate, stagedGateDiff, workingGateDiff, commitGateDiff, rangeFiles, rangeGateDiff, rangeSubjects, revExists, revParse, commitAndPushHunch, pullHunchStatus, syncExistingHunch, gitUntrackCached, gitCommonDir, hooksDir, isLinkedWorktree, mainWorktreeRoot, gitMemoryLog, memoryMoveDiff, revertMemoryMove, pushCurrentBranch, commitChanges, commitRepairStatus, mergeRangeChanges, commitsExist } from "../extractors/git.js";
|
|
44
44
|
import { parseMemoryLog } from "../core/memorylog.js";
|
|
45
45
|
import { renamesOf, planRepair, repairDecision, repairConstraint } from "../core/repair.js";
|
|
46
46
|
import { orphanedCommitDecisions, planCommitRepair, repairDecisionCommit, pickRewrite, commitRepairReviewHash, mergeRewrites, firstFor, deadRewrites, resolvedRewriteIds, withoutDropped, addDropped, withheldForUnresolvableTo } from "../core/commitrepair.js";
|
|
@@ -52,11 +52,11 @@ import { deriveForbids, effectiveForbids } from "../core/constraintmatch.js";
|
|
|
52
52
|
import { extractInlineIntent } from "../extractors/comments.js";
|
|
53
53
|
import { renderText, renderMarkdown, renderSarif, renderImpact, reportFailsStrict } from "../core/checkreport.js";
|
|
54
54
|
import { partitionReview, isReviewDraft, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
|
|
55
|
-
import { installPostCommitHook, installPreCommitHook, installPostMergeHook, installPostCheckoutHook, hookStatus } from "../integrations/hooks.js";
|
|
55
|
+
import { installPostCommitHook, installPreCommitHook, installPostMergeHook, installPostCheckoutHook, hookStatus, hookReport, formatHookInstall } from "../integrations/hooks.js";
|
|
56
56
|
import { ensureSharedOverlayPointer } from "../integrations/worktree.js";
|
|
57
57
|
import { flushCapture, flushMemoryHome, flushMemoryHomes, pinSharedRemote, sharedRemoteFor } from "../integrations/sync.js";
|
|
58
58
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
59
|
-
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS } from "../integrations/gitignore.js";
|
|
59
|
+
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS, upgradeManagedGitignore, describeGitignoreUpgrade } from "../integrations/gitignore.js";
|
|
60
60
|
import { writeCiWorkflow } from "../integrations/ciAction.js";
|
|
61
61
|
import { updateClaudeMd, renderHunchSection } from "../integrations/claudemd.js";
|
|
62
62
|
import { classifyGroundingBlock, describeGroundingFreshness } from "../core/groundingLag.js";
|
|
@@ -81,7 +81,7 @@ import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
|
|
|
81
81
|
import { readConfig, writeConfig, FIRMNESS_LEVELS, isFirmness, workspacesConfig } from "../core/config.js";
|
|
82
82
|
import { loadOrCreateMachine, setMachineLabel, machineFile, labelLeaksIdentity } from "../core/machine.js";
|
|
83
83
|
import { worktreeRows, branchRows } from "../core/workspace.js";
|
|
84
|
-
import { workspaceLedgerView, recordWorkspaceSnapshot, renderWorktreeTable, renderBranchTable, workspaceSummaryLine, prunePlanFor, renderPrunePlan, applyPrune, confirmPrune } from "../integrations/workspaceLedger.js";
|
|
84
|
+
import { workspaceLedgerView, recordWorkspaceSnapshot, renderWorktreeTable, renderBranchTable, workspaceSummaryLine, prunePlanFor, renderPrunePlan, applyPrune, confirmPrune, pruneConfirmQuestion } from "../integrations/workspaceLedger.js";
|
|
85
85
|
import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpolicy.js";
|
|
86
86
|
import { isHumanConfirmed } from "../core/strictgate.js";
|
|
87
87
|
import { appendEvent, readEvents } from "../core/events.js";
|
|
@@ -102,10 +102,11 @@ import { loadGoldenSet, evaluateRetrieval, evaluateTraversalLift } from "../eval
|
|
|
102
102
|
import { loadGuardCases, evalGuards, generateGuardCases } from "../eval/guards.js";
|
|
103
103
|
import { DRIFT_KINDS, computeDrift } from "../core/drift.js";
|
|
104
104
|
import { renderCompilerScorecard, scoreCompilerCaseBank } from "../constitution/scorecard.js";
|
|
105
|
-
import { generateWiki, wikiStatus, wikiPrompt, publicHome, privateHome, readWikiManifestAt, nowData } from "../wiki/wiki.js";
|
|
105
|
+
import { generateWiki, wikiStatus, wikiPrompt, publicHome, privateHome, readWikiManifestAt, nowData, unconfirmedRoadmapMarker } from "../wiki/wiki.js";
|
|
106
106
|
import { adoptProsePrompt } from "../wiki/adopt.js";
|
|
107
107
|
import { topicCollisions, isInForce, liveForTopic } from "../core/topics.js";
|
|
108
108
|
import { ADR_DIR_CANDIDATES, ADR_FILE_RE, mapAdrCorpus } from "../extractors/adrImport.js";
|
|
109
|
+
import { countersignConstraint, countersignDecision } from "../core/countersign.js";
|
|
109
110
|
import { applyImportedAdrReview, carryImportedAdrReview, importedAdrReviewHash, importedAdrSourceHash, isImportedAdrDecision, pendingImportedAdrReviews } from "../core/importReview.js";
|
|
110
111
|
import { exportMadrCorpus, isRegenerableMadr } from "../integrations/madrExport.js";
|
|
111
112
|
import { buildMadrManifest, writeMadrManifest, refreshMadrCorpus } from "../integrations/madrManifest.js";
|
|
@@ -341,6 +342,10 @@ program
|
|
|
341
342
|
const gi = ensureGitignore(root);
|
|
342
343
|
if (gi.action !== "unchanged")
|
|
343
344
|
console.log(` ✓ .gitignore ${gi.action} (Hunch runtime index excluded)`);
|
|
345
|
+
// A repo migrated to a private overlay by an older release: bring its private-only
|
|
346
|
+
// block up to date (and stop publishing newly ignored memory dirs).
|
|
347
|
+
for (const line of describeGitignoreUpgrade(upgradeManagedGitignore(root)))
|
|
348
|
+
console.log(` ✓ ${line}`);
|
|
344
349
|
if (opts.index !== false) {
|
|
345
350
|
if (isGitRepo(root) && revExists("HEAD", root)) {
|
|
346
351
|
const res = indexRepo(store, root, { source: { kind: "commit", ref: "HEAD" } });
|
|
@@ -393,11 +398,14 @@ program
|
|
|
393
398
|
if (isGitRepo(root)) {
|
|
394
399
|
const syncToOverlay = !!(opts.privateSync || opts.sharedSync);
|
|
395
400
|
const h = installPostCommitHook(root, inv.shell, { private: syncToOverlay, commit: opts.autoCommit, localOnly: syncToOverlay });
|
|
396
|
-
|
|
401
|
+
for (const line of formatHookInstall(root, "post-commit hook", h, ` (learning loop)${syncToOverlay ? " — syncs to the shared overlay" : ""}${opts.autoCommit ? " — auto-commit on" : ""}`))
|
|
402
|
+
console.log(line);
|
|
397
403
|
const pm = installPostMergeHook(root, inv.shell);
|
|
398
|
-
|
|
404
|
+
for (const line of formatHookInstall(root, "post-merge hook", pm, " (squash-merge provenance repair + re-syncs grounding docs after a merge that brought memory in)"))
|
|
405
|
+
console.log(line);
|
|
399
406
|
const pc = installPostCheckoutHook(root, inv.shell);
|
|
400
|
-
|
|
407
|
+
for (const line of formatHookInstall(root, "post-checkout hook", pc, " (workspace ledger: records this machine's branches + worktrees on checkout)"))
|
|
408
|
+
console.log(line);
|
|
401
409
|
const m = installMergeDriver(root, inv.shell);
|
|
402
410
|
console.log(` ✓ team merge driver ${m.action}`);
|
|
403
411
|
// Auto-install the pre-commit guard by default (advisory: flags invariants
|
|
@@ -406,7 +414,8 @@ program
|
|
|
406
414
|
if (opts.enforce !== false || opts.enforceStrict) {
|
|
407
415
|
const strict = !!opts.enforceStrict;
|
|
408
416
|
const p = installPreCommitHook(root, inv.shell, strict);
|
|
409
|
-
|
|
417
|
+
for (const line of 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"})`))
|
|
418
|
+
console.log(line);
|
|
410
419
|
}
|
|
411
420
|
}
|
|
412
421
|
else {
|
|
@@ -478,7 +487,10 @@ program
|
|
|
478
487
|
.action((opts) => {
|
|
479
488
|
const { store, root } = storeFor();
|
|
480
489
|
store.json.ensureDirs();
|
|
481
|
-
|
|
490
|
+
// Keep the derived SQLite index out of git (idempotent). Adds a missing block but
|
|
491
|
+
// never rewrites an existing one: `index` runs in CI/release gates on a clean
|
|
492
|
+
// checkout. Older blocks are upgraded by `hunch update` / init / private setup.
|
|
493
|
+
ensureGitignore(root, { upgradeExisting: false });
|
|
482
494
|
// The post-merge hook only ever got installed by `hunch init`/`hunch
|
|
483
495
|
// private`/`hunch shared` — a repo that already ran init before this hook
|
|
484
496
|
// existed never receives it. `hunch index` already self-heals gitignore
|
|
@@ -1250,11 +1262,12 @@ function configureOverlay(dir, opts, mode) {
|
|
|
1250
1262
|
if (opts.hook && isGitRepo(root)) {
|
|
1251
1263
|
freshSetup?.markHookWrite();
|
|
1252
1264
|
const h = installPostCommitHook(root, inv.shell, { private: true, commit: opts.autoCommit, localOnly: mode === "private" });
|
|
1253
|
-
|
|
1265
|
+
const note = (lines) => lines.map((l) => `${l}\n`).join("");
|
|
1266
|
+
hookNote = note(formatHookInstall(root, "post-commit hook", h, ` — captured decisions route here${opts.autoCommit ? " (auto-commit+push on)" : ""}`));
|
|
1254
1267
|
const pm = installPostMergeHook(root, inv.shell);
|
|
1255
|
-
hookNote +=
|
|
1268
|
+
hookNote += note(formatHookInstall(root, "post-merge hook", pm, " (squash-merge provenance repair + re-syncs grounding docs after a merge that brought memory in)"));
|
|
1256
1269
|
const pc = installPostCheckoutHook(root, inv.shell);
|
|
1257
|
-
hookNote +=
|
|
1270
|
+
hookNote += note(formatHookInstall(root, "post-checkout hook", pc, " (workspace ledger: this machine's branches + worktrees sync through the overlay)"));
|
|
1258
1271
|
}
|
|
1259
1272
|
// 5) one-time migration: MOVE existing public memory INTO the overlay, then make
|
|
1260
1273
|
// THIS repo code-only. Records are absorbed (union by id) BEFORE the public
|
|
@@ -1314,6 +1327,13 @@ function configureOverlay(dir, opts, mode) {
|
|
|
1314
1327
|
` next: review, then commit the PUBLIC repo:\n` +
|
|
1315
1328
|
` git add -A && git commit -m "chore: move engineering memory to a private overlay" && git push\n`;
|
|
1316
1329
|
}
|
|
1330
|
+
else {
|
|
1331
|
+
// Re-running setup on a repo migrated by an older release upgrades its private-only
|
|
1332
|
+
// block in place and stops publishing memory dirs that block did not yet list.
|
|
1333
|
+
const upgraded = describeGitignoreUpgrade(upgradeManagedGitignore(root));
|
|
1334
|
+
if (upgraded.length)
|
|
1335
|
+
migrateNote = upgraded.map((line) => ` ✓ ${line}\n`).join("");
|
|
1336
|
+
}
|
|
1317
1337
|
const lead = mode === "private"
|
|
1318
1338
|
? `✓ private overlay enabled → ${hunchDir}\n`
|
|
1319
1339
|
: `✓ shared overlay enabled → ${hunchDir}\n`;
|
|
@@ -1520,7 +1540,7 @@ workspacesCmd
|
|
|
1520
1540
|
if (!isGitRepo(root))
|
|
1521
1541
|
return fail("`hunch workspaces prune` needs a git repo");
|
|
1522
1542
|
const view = workspaceLedgerView(store, root, { fetch: opts.fetch });
|
|
1523
|
-
const plan = prunePlanFor(view);
|
|
1543
|
+
const plan = prunePlanFor(view, root);
|
|
1524
1544
|
if (!opts.apply) {
|
|
1525
1545
|
if (opts.json)
|
|
1526
1546
|
return console.log(JSON.stringify({ machine: view.machine.label, plan }, null, 2));
|
|
@@ -1536,8 +1556,7 @@ workspacesCmd
|
|
|
1536
1556
|
if (!opts.json)
|
|
1537
1557
|
console.log(renderPrunePlan(view, plan));
|
|
1538
1558
|
if (!opts.yes) {
|
|
1539
|
-
const
|
|
1540
|
-
const ok = await confirmPrune(`Delete ${plan.local.length} branch(es)${worktrees ? ` and remove ${worktrees} worktree(s)` : ""} on ${view.machine.label}?`);
|
|
1559
|
+
const ok = await confirmPrune(pruneConfirmQuestion(view, plan));
|
|
1541
1560
|
if (!ok)
|
|
1542
1561
|
return fail(process.stdin.isTTY ? "not confirmed — nothing was deleted" : "refusing to apply without confirmation: stdin is not a terminal; pass --yes to confirm explicitly");
|
|
1543
1562
|
}
|
|
@@ -1555,11 +1574,12 @@ workspacesCmd
|
|
|
1555
1574
|
return console.log(JSON.stringify({ machine: view.machine.label, plan, results, recorded }, null, 2));
|
|
1556
1575
|
console.log("");
|
|
1557
1576
|
for (const r of results)
|
|
1558
|
-
console.log(` ${r.outcome === "deleted" ? "✓" : "✗"} ${r.step.branch}: ${r.detail}`);
|
|
1577
|
+
console.log(` ${r.outcome === "deleted" ? "✓" : r.outcome === "skipped" ? "–" : "✗"} ${r.step.branch}: ${r.detail}`);
|
|
1559
1578
|
const failed = results.filter((r) => r.outcome === "failed").length;
|
|
1579
|
+
const skipped = results.filter((r) => r.outcome === "skipped").length;
|
|
1560
1580
|
const ledger = recorded === "written" ? " · ledger updated" : recorded === "unchanged" || recorded === "no-home" || recorded === "off" ? "" : ` · ledger not updated (${recorded})`;
|
|
1561
|
-
console.log(`\n${results.length - failed} deleted, ${failed} refused by git${ledger}`);
|
|
1562
|
-
if (failed)
|
|
1581
|
+
console.log(`\n${results.length - failed - skipped} deleted, ${skipped ? `${skipped} skipped (nothing changed), ` : ""}${failed} refused by git${ledger}`);
|
|
1582
|
+
if (failed || skipped)
|
|
1563
1583
|
process.exitCode = 1;
|
|
1564
1584
|
}
|
|
1565
1585
|
finally {
|
|
@@ -4025,14 +4045,17 @@ program
|
|
|
4025
4045
|
// vacuous green (deletions are excluded by the enumerators' --diff-filter=ACMR, so
|
|
4026
4046
|
// such a PR enumerates zero files) — including a deletion of the very symbol a
|
|
4027
4047
|
// blocking conformance predicate or an active policy guards.
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4048
|
+
// The COMPLETE diff (no synthesis byte budget); an incomplete one is flagged so
|
|
4049
|
+
// content-matched blocking invariants fail closed instead of passing unseen.
|
|
4050
|
+
const gate = files.length
|
|
4051
|
+
? (exactCommit ? commitGateDiff(exactCommit, root) : opts.base ? rangeGateDiff(opts.base, root) : opts.working ? workingGateDiff(root) : stagedGateDiff(root))
|
|
4052
|
+
: { diff: "" };
|
|
4031
4053
|
const report = files.length
|
|
4032
|
-
? store.buildCheckReport(files, diff, {
|
|
4054
|
+
? store.buildCheckReport(files, gate.diff, {
|
|
4033
4055
|
strict: !!opts.strict,
|
|
4034
4056
|
lastChange: (f) => lastChangeDate(f, root),
|
|
4035
4057
|
publicOnly: !!opts.publicOnly,
|
|
4058
|
+
diffStatus: gate,
|
|
4036
4059
|
})
|
|
4037
4060
|
: emptyReport;
|
|
4038
4061
|
if (opts.blast && !markdown && !sarif && files.length) {
|
|
@@ -4170,8 +4193,8 @@ const vetoCmd = program
|
|
|
4170
4193
|
store.close();
|
|
4171
4194
|
return;
|
|
4172
4195
|
}
|
|
4173
|
-
const
|
|
4174
|
-
const full = store.buildCheckReport(files, diff, { strict: !!opts.strict, lastChange: (f) => lastChangeDate(f, root) });
|
|
4196
|
+
const gate = opts.commit ? commitGateDiff(opts.commit, root) : opts.base ? rangeGateDiff(opts.base, root) : stagedGateDiff(root);
|
|
4197
|
+
const full = store.buildCheckReport(files, gate.diff, { strict: !!opts.strict, lastChange: (f) => lastChangeDate(f, root), diffStatus: gate });
|
|
4175
4198
|
if (!full.vetoes.length) {
|
|
4176
4199
|
console.log(`✓ ${files.length} changed file(s) reverse no decision you rejected.`);
|
|
4177
4200
|
store.close();
|
|
@@ -4732,11 +4755,14 @@ program
|
|
|
4732
4755
|
const before = st;
|
|
4733
4756
|
let activity = null;
|
|
4734
4757
|
if (/^(Edit|Write|MultiEdit)$/.test(evt.tool_name ?? "")) {
|
|
4735
|
-
|
|
4736
|
-
|
|
4758
|
+
// A Codex apply_patch touches every file it lists (and each Move-to
|
|
4759
|
+
// destination); the Stop gate must see all of them, not only the first.
|
|
4760
|
+
const patchPaths = evt.tool_input?.patch_files?.flatMap((f) => f.moved_to ? [f.path, f.moved_to] : [f.path]);
|
|
4761
|
+
const edited = patchPaths?.length ? patchPaths : evt.tool_input?.file_path ? [evt.tool_input.file_path] : [];
|
|
4762
|
+
for (const p of edited)
|
|
4737
4763
|
st = onEdit(st, toRepoRel(root, p));
|
|
4764
|
+
if (edited.length)
|
|
4738
4765
|
activity = { kind: "edit" };
|
|
4739
|
-
}
|
|
4740
4766
|
}
|
|
4741
4767
|
else if (evt.tool_name === "Bash" || evt.tool_name === "PowerShell") {
|
|
4742
4768
|
const command = String(evt.tool_input?.command ?? "");
|
|
@@ -5017,7 +5043,10 @@ program
|
|
|
5017
5043
|
L.push(` ${r.date} [${r.status}] ${r.title} (${r.id})`);
|
|
5018
5044
|
}
|
|
5019
5045
|
if (roadmap.length) {
|
|
5020
|
-
L.push(`Roadmap (${roadmap.length} live proposed): ${roadmap.slice(0, 3).map((r) => r.title).join(" · ")}${roadmap.length > 3 ? " · …" : ""}`);
|
|
5046
|
+
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 ? " · …" : ""}`);
|
|
5047
|
+
const unconfirmed = roadmap.filter((r) => r.unconfirmed).length;
|
|
5048
|
+
if (unconfirmed)
|
|
5049
|
+
L.push(`${unconfirmed} roadmap item(s) are unconfirmed agent testimony — the human confirms each with \`hunch review --confirm <id>${s.unified ? " --private" : ""}\`.`);
|
|
5021
5050
|
}
|
|
5022
5051
|
if (pendingReview > 0)
|
|
5023
5052
|
L.push(`${pendingReview} legacy un-vouched draft(s) — adopt as advisory memory with \`hunch adopt-drafts\` (new captures auto-trust).`);
|
|
@@ -5046,18 +5075,16 @@ program
|
|
|
5046
5075
|
}
|
|
5047
5076
|
if (evt.hook_event_name !== "PreToolUse")
|
|
5048
5077
|
return;
|
|
5049
|
-
const
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
const target = toRepoRel(root, abs);
|
|
5053
|
-
// Outside the repo (".." prefix) or on another drive (absolute, e.g. "D:/…")
|
|
5054
|
-
// → nothing for Hunch to say.
|
|
5055
|
-
if (!target || target.startsWith("..") || /^[a-zA-Z]:/.test(target))
|
|
5078
|
+
const targets = editTargets(root, evt.tool_input);
|
|
5079
|
+
// Nothing inside the repo → nothing for Hunch to say.
|
|
5080
|
+
if (!targets.length)
|
|
5056
5081
|
return;
|
|
5082
|
+
// Grounding below stays about the first in-repo file; the strict gate checks every target.
|
|
5083
|
+
const { abs, target } = targets[0];
|
|
5057
5084
|
// A compiled red→green probe is only meaningful if its red receipt exists
|
|
5058
5085
|
// before implementation. Firm/strict may deny two edits per prompt, then
|
|
5059
5086
|
// fail open so a malformed or unavailable probe can never deadlock work.
|
|
5060
|
-
if ((firmness === "firm" || firmness === "strict") && evt.session_id && pipelineEnabled() && isProductPath(target)) {
|
|
5087
|
+
if ((firmness === "firm" || firmness === "strict") && evt.session_id && pipelineEnabled() && targets.some((t) => isProductPath(t.target))) {
|
|
5061
5088
|
const baseline = beforeEditProbeVerdict(loadPipelineState(evt.session_id));
|
|
5062
5089
|
if (baseline.block) {
|
|
5063
5090
|
savePipelineState(evt.session_id, baseline.state);
|
|
@@ -5089,23 +5116,28 @@ program
|
|
|
5089
5116
|
store.reindex();
|
|
5090
5117
|
// The lines this edit would ADD — so a content-matched invariant denies only
|
|
5091
5118
|
// when the edit actually trips it (not on every edit in scope), and the Veto
|
|
5092
|
-
// Guard can test the proposed text. Covers Edit/Write/MultiEdit
|
|
5093
|
-
|
|
5094
|
-
const
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5119
|
+
// Guard can test the proposed text. Covers Edit/Write/MultiEdit, and each
|
|
5120
|
+
// file of a Codex apply_patch with that file's own added lines.
|
|
5121
|
+
const denials = [];
|
|
5122
|
+
for (const t of targets) {
|
|
5123
|
+
// Veto Guard (live): the proposed edit text re-introduces an approach an
|
|
5124
|
+
// in-force decision REJECTED. The agent self-corrects before staging;
|
|
5125
|
+
// only human-confirmed tripwires deny.
|
|
5126
|
+
const hit = blockingInScope(store, t.target, t.lines) ?? (t.lines.length ? vetoInScope(store, t.target, t.lines) : null);
|
|
5127
|
+
if (hit)
|
|
5128
|
+
denials.push({ target: t.target, hit });
|
|
5100
5129
|
}
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5130
|
+
if (denials.length) {
|
|
5131
|
+
for (const d of denials)
|
|
5132
|
+
appendEvent(paths, { at: new Date().toISOString(), file: d.target, ...d.hit.event });
|
|
5133
|
+
// One denied file keeps the single-file reason verbatim; several are
|
|
5134
|
+
// listed so the agent knows every file it must reconsider.
|
|
5135
|
+
const reason = denials.length === 1
|
|
5136
|
+
? denials[0].hit.reason
|
|
5137
|
+
: `Hunch: this patch is denied for ${denials.length} files (${denials.map((d) => d.target).join(", ")}).\n${denials.map((d) => d.hit.reason).join("\n")}`;
|
|
5138
|
+
emitDeny(provider, reason);
|
|
5139
|
+
for (const d of denials)
|
|
5140
|
+
observeHookDenial(root, provider, evt, d.target, d.hit);
|
|
5109
5141
|
return;
|
|
5110
5142
|
}
|
|
5111
5143
|
}
|
|
@@ -5330,6 +5362,8 @@ program
|
|
|
5330
5362
|
.description("Answer hash-bound imported-ADR questions, or triage deliberate proposed drafts.")
|
|
5331
5363
|
.option("--accept <id>", "promote a decision to accepted/human-confirmed (confirms its tripwires)")
|
|
5332
5364
|
.option("--reject <id>", "reject a draft decision with a durable lifecycle tombstone")
|
|
5365
|
+
.option("--confirm <id>", "countersign an agent-recorded decision (dec_…) or correction (con_…) as human-confirmed WITHOUT changing its status or content — the human act a capture token alone cannot stand in for")
|
|
5366
|
+
.option("--severity <s>", "with --confirm on a correction: the severity you grant (advisory | warning | blocking); default keeps its current severity")
|
|
5333
5367
|
.option("--approve-import <id>", "approve one exact imported ADR as human-confirmed authority")
|
|
5334
5368
|
.option("--decline-import <id>", "record that one exact imported ADR was reviewed and must stay advisory")
|
|
5335
5369
|
.option("--expected-source-hash <hash>", "exact sha256 source hash printed with the imported-ADR question")
|
|
@@ -5346,11 +5380,15 @@ program
|
|
|
5346
5380
|
store.close();
|
|
5347
5381
|
return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
5348
5382
|
}
|
|
5349
|
-
const actionCount = [opts.accept, opts.reject, opts.approveImport, opts.declineImport, opts.acceptVerified, opts.rejectDuplicates].filter(Boolean).length;
|
|
5383
|
+
const actionCount = [opts.accept, opts.reject, opts.confirm, opts.approveImport, opts.declineImport, opts.acceptVerified, opts.rejectDuplicates].filter(Boolean).length;
|
|
5350
5384
|
if (actionCount > 1) {
|
|
5351
5385
|
store.close();
|
|
5352
5386
|
return fail("choose exactly one review action at a time");
|
|
5353
5387
|
}
|
|
5388
|
+
if (opts.severity && !opts.confirm) {
|
|
5389
|
+
store.close();
|
|
5390
|
+
return fail("--severity applies only with --confirm on a correction");
|
|
5391
|
+
}
|
|
5354
5392
|
const decisions = () => opts.private ? store.recs("decisions") : store.json.loadAll("decisions");
|
|
5355
5393
|
let publicGroundingChanged = false;
|
|
5356
5394
|
const touchedHomes = new Set();
|
|
@@ -5390,6 +5428,58 @@ program
|
|
|
5390
5428
|
return fail(error instanceof Error ? error.message : String(error));
|
|
5391
5429
|
}
|
|
5392
5430
|
}
|
|
5431
|
+
else if (opts.confirm) {
|
|
5432
|
+
// Human countersign of agent testimony. A capture token proves only that the
|
|
5433
|
+
// interview tool was called (inside the agent's channel), so agent-written records
|
|
5434
|
+
// land as agent_recorded; this is the out-of-channel human act that signs them.
|
|
5435
|
+
// Content and status are untouched: confirming a proposed decision is not shipping it.
|
|
5436
|
+
const id = opts.confirm;
|
|
5437
|
+
const SEV = ["advisory", "warning", "blocking"];
|
|
5438
|
+
if (opts.severity && !SEV.includes(opts.severity)) {
|
|
5439
|
+
store.close();
|
|
5440
|
+
return fail(`--severity must be one of: ${SEV.join(", ")}`);
|
|
5441
|
+
}
|
|
5442
|
+
const now = new Date().toISOString();
|
|
5443
|
+
const d = opts.private ? store.getRec("decisions", id) : store.json.get("decisions", id);
|
|
5444
|
+
const c = d ? undefined : opts.private ? store.getRec("constraints", id) : store.json.get("constraints", id);
|
|
5445
|
+
if (!d && !c) {
|
|
5446
|
+
store.close();
|
|
5447
|
+
return fail(`no decision or constraint ${id} found${opts.private ? "" : " in the public home (add --private for overlay records)"}`);
|
|
5448
|
+
}
|
|
5449
|
+
let home;
|
|
5450
|
+
if (d) {
|
|
5451
|
+
if (opts.severity) {
|
|
5452
|
+
store.close();
|
|
5453
|
+
return fail("--severity applies only when confirming a correction (con_…), not a decision");
|
|
5454
|
+
}
|
|
5455
|
+
if (isImportedAdrDecision(d)) {
|
|
5456
|
+
store.close();
|
|
5457
|
+
return fail(`imported ADR ${d.id} requires the hash-bound --approve-import flow shown by hunch review`);
|
|
5458
|
+
}
|
|
5459
|
+
home = opts.private ? decisionMemoryHome(store, d.id) : "public";
|
|
5460
|
+
putDecisionInHome(store, countersignDecision(d, now), home);
|
|
5461
|
+
console.log(`✓ confirmed decision ${id} as human_confirmed (status ${d.status} unchanged)`);
|
|
5462
|
+
}
|
|
5463
|
+
else {
|
|
5464
|
+
home = opts.private && store.getPrivateRec("constraints", id) ? "private" : "public";
|
|
5465
|
+
const next = countersignConstraint(c, now, opts.severity);
|
|
5466
|
+
if (home === "private")
|
|
5467
|
+
store.putPrivate("constraints", next);
|
|
5468
|
+
else
|
|
5469
|
+
store.json.put("constraints", next);
|
|
5470
|
+
const repoWide = next.scope.length === 1 && next.scope[0] === "**";
|
|
5471
|
+
console.log(`✓ confirmed ${next.severity} constraint ${id} as human_confirmed (scope: ${next.scope.join(", ")})`);
|
|
5472
|
+
if (next.severity === "blocking" && repoWide)
|
|
5473
|
+
console.log(" ⚠ repo-wide blocking rule: it can deny any edit at strict firmness and fail hunch check --strict.");
|
|
5474
|
+
}
|
|
5475
|
+
touchedHomes.add(home);
|
|
5476
|
+
store.reindex();
|
|
5477
|
+
if (home === "public") {
|
|
5478
|
+
publicGroundingChanged = true;
|
|
5479
|
+
if (!store.autoCommit)
|
|
5480
|
+
refreshExistingGrounding(root, store);
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5393
5483
|
else if (opts.accept) {
|
|
5394
5484
|
const d = opts.private ? store.getRec("decisions", opts.accept) : store.json.get("decisions", opts.accept);
|
|
5395
5485
|
if (!d) {
|
|
@@ -5513,7 +5603,7 @@ program
|
|
|
5513
5603
|
if (dupCount)
|
|
5514
5604
|
console.log(`\n Batch-reject the ${dupCount} duplicate(s): hunch review --reject-duplicates`);
|
|
5515
5605
|
}
|
|
5516
|
-
console.log(`\nAccept: hunch review --accept <id> Reject: hunch review --reject <id>`);
|
|
5606
|
+
console.log(`\nAccept: hunch review --accept <id> Reject: hunch review --reject <id> Confirm (sign, keep status): hunch review --confirm <id>`);
|
|
5517
5607
|
}
|
|
5518
5608
|
}
|
|
5519
5609
|
if (touchedHomes.size)
|
|
@@ -6562,8 +6652,8 @@ program
|
|
|
6562
6652
|
console.log(`No changed files in ${scope}.`);
|
|
6563
6653
|
return;
|
|
6564
6654
|
}
|
|
6565
|
-
const
|
|
6566
|
-
console.log(renderImpact(store.prImpact(files, diff), scope));
|
|
6655
|
+
const gate = opts.commit ? commitGateDiff(opts.commit, root) : base ? rangeGateDiff(base, root) : opts.working ? workingGateDiff(root) : stagedGateDiff(root);
|
|
6656
|
+
console.log(renderImpact(store.prImpact(files, gate.diff, gate), scope));
|
|
6567
6657
|
}
|
|
6568
6658
|
finally {
|
|
6569
6659
|
store.close();
|
|
@@ -6713,7 +6803,7 @@ program
|
|
|
6713
6803
|
if (!roadmap.length)
|
|
6714
6804
|
console.log(" (empty — record what's next as a PROPOSED decision via /capture and it appears here)");
|
|
6715
6805
|
for (const r of roadmap)
|
|
6716
|
-
console.log(` • ${r.title} (${r.id}${r.topic ? `, ${r.topic}` : ""}, since ${r.date})\n ${r.note}`);
|
|
6806
|
+
console.log(` • ${r.title} (${r.id}${r.topic ? `, ${r.topic}` : ""}, since ${r.date})${r.unconfirmed ? `\n ${unconfirmedRoadmapMarker(r, { private: !!opts.private })}` : ""}\n ${r.note}`);
|
|
6717
6807
|
if (pendingReview > 0)
|
|
6718
6808
|
console.log(`\n (${pendingReview} legacy un-vouched draft(s) — \`hunch adopt-drafts\` to auto-trust them as advisory)`);
|
|
6719
6809
|
// Task-record ranking: evaluated automatically on every task write; the kill rule applies itself.
|
|
@@ -6983,16 +7073,32 @@ program
|
|
|
6983
7073
|
// squash-merge went unrepaired. pre-commit is opt-out (`--no-enforce`),
|
|
6984
7074
|
// so its absence is informational only, never a warning.
|
|
6985
7075
|
if (isGitRepo(root)) {
|
|
7076
|
+
const report = hookReport(root);
|
|
6986
7077
|
const hooks = hookStatus(root);
|
|
6987
|
-
const missing = [
|
|
7078
|
+
const missing = [report.postCommit.state === "missing" && "post-commit", report.postMerge.state === "missing" && "post-merge"].filter((h) => !!h);
|
|
7079
|
+
// A block that is present but never runs (after an exec/exit, or inside a
|
|
7080
|
+
// hook manager's regenerated file — issue #311) is NOT installed.
|
|
7081
|
+
const unreachable = [["post-commit", report.postCommit], ["post-merge", report.postMerge], ["pre-commit", report.preCommit], ["post-checkout", report.postCheckout]]
|
|
7082
|
+
.filter(([, e]) => e.state === "unreachable");
|
|
6988
7083
|
// `hunch index` only ever installs post-merge onto an existing
|
|
6989
7084
|
// post-commit install (it never hooks an un-hooked repo — see
|
|
6990
|
-
// isGitRepo(root) && hookStatus(root).postCommit above)
|
|
6991
|
-
// hint must not point there when
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
7085
|
+
// isGitRepo(root) && hookStatus(root).postCommit above) and never writes
|
|
7086
|
+
// into a manager-owned hook — so the fix hint must not point there when
|
|
7087
|
+
// post-commit itself is missing or a hook manager is in play.
|
|
7088
|
+
const managedMissing = [report.postCommit, report.postMerge].find((e) => e.state === "missing" && e.manager !== "none");
|
|
7089
|
+
const fix = managedMissing
|
|
7090
|
+
? `${managedMissing.manager === "exec-exit" ? "the existing hook exits before an appended block would run" : `a hook manager (${managedMissing.manager}) owns these hooks`}; run \`hunch init\` to print the snippet to add to ${rel(root, managedMissing.path)}`
|
|
7091
|
+
: !hooks.postCommit ? "run `hunch init` for the full setup" : "run `hunch index` to install it";
|
|
7092
|
+
const problems = [
|
|
7093
|
+
missing.length ? `⚠ missing ${missing.join(", ")} — ${fix}` : "",
|
|
7094
|
+
unreachable.length ? `⚠ installed but unreachable: ${unreachable.map(([n]) => n).join(", ")} — never runs; run \`hunch init\` to print the snippet for your hook manager` : "",
|
|
7095
|
+
].filter(Boolean);
|
|
7096
|
+
console.log(`hooks: ${problems.length
|
|
7097
|
+
? problems.join("\n ")
|
|
6995
7098
|
: `post-commit, post-merge installed${hooks.preCommit ? " (+ pre-commit)" : ""}`}`);
|
|
7099
|
+
for (const [name, e] of unreachable) {
|
|
7100
|
+
console.log(dim(` ↳ ${name}: ${e.reason ?? "the block sits where git never reaches it"} (${rel(root, e.path)})`));
|
|
7101
|
+
}
|
|
6996
7102
|
// Workspace ledger: what this machine is called, whether its record is in memory,
|
|
6997
7103
|
// and whether the checkout hook that keeps it fresh is installed.
|
|
6998
7104
|
try {
|
|
@@ -7001,7 +7107,7 @@ program
|
|
|
7001
7107
|
const others = store.recs("workspaces").filter((r) => r.machine.id !== machine.id).length;
|
|
7002
7108
|
const leak = labelLeaksIdentity(machine.label);
|
|
7003
7109
|
console.log(`workspaces: this machine is ${machine.label}${leak ? ` (⚠ label equals the ${leak})` : ""} · record in memory: ${stored ? `yes (${stored.observed_at})` : "no"} · ${others} other machine(s)` +
|
|
7004
|
-
`${hooks.postCheckout ? " · post-checkout hook installed" : hooks.postCommit ? " · post-checkout hook not installed (`hunch index` adds it)" : ""}`);
|
|
7110
|
+
`${hooks.postCheckout ? " · post-checkout hook installed" : report.postCheckout.state === "unreachable" ? " · ⚠ post-checkout hook unreachable" : hooks.postCommit ? " · post-checkout hook not installed (`hunch index` adds it)" : ""}`);
|
|
7005
7111
|
}
|
|
7006
7112
|
catch { /* no machine file writable: nothing to report */ }
|
|
7007
7113
|
}
|
|
@@ -7169,6 +7275,28 @@ function realpathNorm(p) {
|
|
|
7169
7275
|
function toRepoRel(root, abs) {
|
|
7170
7276
|
return relative(realpathNorm(root), realpathNorm(abs)).split("\\").join("/");
|
|
7171
7277
|
}
|
|
7278
|
+
/** The in-repo files a pre-edit event would change, each with the lines the edit
|
|
7279
|
+
* ADDS to that file. A Codex apply_patch yields one entry per touched path (a
|
|
7280
|
+
* Move-to destination is its own entry, carrying the section's added lines);
|
|
7281
|
+
* every other edit tool yields its single `file_path`. Paths outside the repo
|
|
7282
|
+
* ("..") or on another drive ("D:/…") are skipped. */
|
|
7283
|
+
function editTargets(root, input) {
|
|
7284
|
+
const raw = input?.patch_files?.length
|
|
7285
|
+
? input.patch_files.flatMap((f) => (f.moved_to ? [f.path, f.moved_to] : [f.path]).map((abs) => ({ abs, lines: f.added_lines })))
|
|
7286
|
+
: input?.file_path ? [{ abs: input.file_path, lines: proposedEditLines(input) }] : [];
|
|
7287
|
+
const byTarget = new Map();
|
|
7288
|
+
for (const { abs, lines } of raw) {
|
|
7289
|
+
const target = toRepoRel(root, abs);
|
|
7290
|
+
if (!target || target.startsWith("..") || /^[a-zA-Z]:/.test(target))
|
|
7291
|
+
continue;
|
|
7292
|
+
const seen = byTarget.get(target);
|
|
7293
|
+
if (seen)
|
|
7294
|
+
seen.lines = [...seen.lines, ...lines];
|
|
7295
|
+
else
|
|
7296
|
+
byTarget.set(target, { abs, target, lines });
|
|
7297
|
+
}
|
|
7298
|
+
return [...byTarget.values()];
|
|
7299
|
+
}
|
|
7172
7300
|
function emitContext(provider, event, text,
|
|
7173
7301
|
/** One user-facing line where the host shows hook messages (Claude Code's
|
|
7174
7302
|
* `systemMessage`); never a block, never a second model turn. */
|
package/dist/cli/integrations.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { findRoot } from "../core/paths.js";
|
|
2
2
|
import { CAPABILITIES, HARNESSES, inspectIntegrations, integrationHealthFails, formatIntegrationHealth, repairIntegrationPins } from "../integrations/health.js";
|
|
3
3
|
import { probeIntegration } from "../integrations/probe.js";
|
|
4
|
+
import { describeGitignoreUpgrade, upgradeManagedGitignore } from "../integrations/gitignore.js";
|
|
4
5
|
export function registerIntegrationCommands(program, refreshGrounding) {
|
|
5
6
|
const integrations = program.command("integrations").description("Check harness coverage and repair stale repository Hunch pins");
|
|
6
7
|
integrations.command("check")
|
|
@@ -34,6 +35,15 @@ export function registerIntegrationCommands(program, refreshGrounding) {
|
|
|
34
35
|
if (files.includes(".codex/hooks.json")) {
|
|
35
36
|
console.log("Codex: open /hooks to review and trust the changed commands, then start a new session. Command changes require renewed trust; Hunch does not grant it automatically.");
|
|
36
37
|
}
|
|
38
|
+
// `hunch update` runs this command with the NEW release, so existing repos pick up
|
|
39
|
+
// managed .gitignore entries added since they were set up (no re-init needed).
|
|
40
|
+
try {
|
|
41
|
+
for (const line of describeGitignoreUpgrade(upgradeManagedGitignore(root)))
|
|
42
|
+
console.log(line);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.log(`.gitignore not upgraded: ${error.message}`);
|
|
46
|
+
}
|
|
37
47
|
const grounding = refreshGrounding();
|
|
38
48
|
if (grounding.length)
|
|
39
49
|
console.log(`Updated Hunch instructions: ${grounding.join(", ")}. Reconnect the agent to load task reporting instructions.`);
|
package/dist/client/state.d.ts
CHANGED
|
@@ -306,11 +306,12 @@ export declare function createStateClient(opts: StateClientOptions): {
|
|
|
306
306
|
missing: string[];
|
|
307
307
|
denied: string[];
|
|
308
308
|
}>;
|
|
309
|
+
/** Liveness; `partitions` is present because this client sends its credential. */
|
|
309
310
|
health: () => Promise<{
|
|
310
311
|
ok: boolean;
|
|
311
312
|
version: string;
|
|
312
313
|
protocol: string;
|
|
313
|
-
partitions
|
|
314
|
+
partitions?: string[];
|
|
314
315
|
}>;
|
|
315
316
|
};
|
|
316
317
|
export type StateClient = ReturnType<typeof createStateClient>;
|
package/dist/client/state.js
CHANGED
|
@@ -59,6 +59,7 @@ export function createStateClient(opts) {
|
|
|
59
59
|
captureBatch: (request) => call("POST", "/nuryel/v1/capture-batch", request),
|
|
60
60
|
subscribe: (request) => call("POST", "/nuryel/v1/subscribe", request),
|
|
61
61
|
records: (request) => call("POST", "/nuryel/v1/records", request),
|
|
62
|
+
/** Liveness; `partitions` is present because this client sends its credential. */
|
|
62
63
|
health: () => call("GET", "/nuryel/v1/health"),
|
|
63
64
|
};
|
|
64
65
|
}
|
package/dist/core/agenthook.d.ts
CHANGED
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
export declare const HOOK_PROVIDERS: readonly ["claude", "codex", "vscode", "windsurf", "antigravity", "cursor"];
|
|
9
9
|
export type HookProvider = (typeof HOOK_PROVIDERS)[number];
|
|
10
10
|
export type HunchHookEvent = "PreToolUse" | "PostToolUse" | "PostToolUseFailure" | "UserPromptSubmit" | "SessionStart" | "SubagentStart" | "PreCompact" | "Stop";
|
|
11
|
+
/** One file section of a Codex `apply_patch` payload. */
|
|
12
|
+
export interface HunchPatchFile {
|
|
13
|
+
/** The path as written in the patch (may be absolute). */
|
|
14
|
+
path: string;
|
|
15
|
+
action: "update" | "add" | "delete";
|
|
16
|
+
/** `*** Move to:` destination, when the section renames the file. */
|
|
17
|
+
moved_to?: string;
|
|
18
|
+
/** Only the lines this section adds (`+` prefix stripped). */
|
|
19
|
+
added_lines: string[];
|
|
20
|
+
}
|
|
11
21
|
export interface HunchToolInput {
|
|
12
22
|
file_path?: string;
|
|
13
23
|
new_string?: string;
|
|
@@ -17,6 +27,9 @@ export interface HunchToolInput {
|
|
|
17
27
|
}>;
|
|
18
28
|
command?: string;
|
|
19
29
|
skill?: string;
|
|
30
|
+
/** Codex `apply_patch` only: every file the patch touches. `file_path` is the
|
|
31
|
+
* first entry's path and `content` the raw patch, for single-file consumers. */
|
|
32
|
+
patch_files?: HunchPatchFile[];
|
|
20
33
|
}
|
|
21
34
|
/** A provider-neutral observation of the tool result. Output is ephemeral: the
|
|
22
35
|
* pipeline compares it with bounded expectations but never persists it. */
|
|
@@ -41,6 +54,7 @@ export interface HunchHookInput {
|
|
|
41
54
|
/** SubagentStart: the delegated agent's type (e.g. "Explore", "Plan"). */
|
|
42
55
|
agent_type?: string;
|
|
43
56
|
}
|
|
57
|
+
export declare function parseApplyPatch(patch: string): HunchPatchFile[];
|
|
44
58
|
/** Parse a provider name supplied by a hook config. Unknown values intentionally
|
|
45
59
|
* return null so a bad config cannot make an edit fail. */
|
|
46
60
|
export declare function hookProvider(value: unknown): HookProvider | null;
|