@davesheffer/hunch 1.47.0 → 1.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +129 -37
- 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 +14 -1
- 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
|
@@ -46,7 +46,7 @@ import { isParserLoadError, loadNativeTreeSitter } from "../extractors/nativeTre
|
|
|
46
46
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
47
47
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
48
48
|
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";
|
|
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, 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
50
|
import { runbookDraft } from "../core/runbookDraft.js";
|
|
51
51
|
import { parseMemoryLog } from "../core/memorylog.js";
|
|
52
52
|
import { renamesOf, planRepair, repairDecision, repairConstraint } from "../core/repair.js";
|
|
@@ -61,7 +61,7 @@ import { renderText, renderMarkdown, renderSarif, renderImpact, reportFailsStric
|
|
|
61
61
|
import { partitionReview, isReviewDraft, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
|
|
62
62
|
import { installPostCommitHook, installPreCommitHook, installPostMergeHook, installPostCheckoutHook, hookStatus, hookReport, hookInvocationLines, formatHookInstall, sharedHooksNote } from "../integrations/hooks.js";
|
|
63
63
|
import { ensureSharedOverlayPointer } from "../integrations/worktree.js";
|
|
64
|
-
import { flushCapture, flushMemoryHome, flushMemoryHomes, pinSharedRemote, sharedRemoteFor } from "../integrations/sync.js";
|
|
64
|
+
import { flushCapture, flushMemoryHome, flushMemoryHomes, pinSharedRemote, sharedRemoteFor, holdsOnDefaultBranch, HELD_ON_DEFAULT_BRANCH } from "../integrations/sync.js";
|
|
65
65
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
66
66
|
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS, upgradeManagedGitignore, describeGitignoreUpgrade, memoryIgnoreRule, describeMemoryIgnoreRule } from "../integrations/gitignore.js";
|
|
67
67
|
import { writeCiWorkflow } from "../integrations/ciAction.js";
|
|
@@ -210,7 +210,7 @@ registerReviewMemoryCommands(program, (records, repository, privateOnly) => {
|
|
|
210
210
|
for (const record of records)
|
|
211
211
|
store.putCapture("constraints", record, privateOnly);
|
|
212
212
|
store.reindex();
|
|
213
|
-
if (home === "public" && !store
|
|
213
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
214
214
|
refreshExistingGrounding(root, store);
|
|
215
215
|
pumpMemoryHome(store, root, home, `hunch: capture ${records.length} sourced review rule(s)`);
|
|
216
216
|
}, (repository, privateOnly) => {
|
|
@@ -447,8 +447,25 @@ function storeFor(opts = {}) {
|
|
|
447
447
|
const { store, teamPullStatus, teamPullDetail } = openTeamStore(root, opts);
|
|
448
448
|
return { store, root, teamPullStatus, teamPullDetail };
|
|
449
449
|
}
|
|
450
|
+
/** Whether a public memory flush commits the tracked grounding docs itself. When it
|
|
451
|
+
* won't (auto-commit off, or held on the default branch), a by-hand command refreshes
|
|
452
|
+
* them on disk instead, so CLAUDE.md never keeps enforcing what the command just changed. */
|
|
453
|
+
function publicFlushCommitsGrounding(store, root) {
|
|
454
|
+
return store.autoCommit && !holdsOnDefaultBranch(hunchPaths(root).hunch);
|
|
455
|
+
}
|
|
456
|
+
// A held public flush says so once per command, on stderr: stdout may be a --json payload.
|
|
457
|
+
let heldNoticePrinted = false;
|
|
458
|
+
function noteHeld() {
|
|
459
|
+
if (heldNoticePrinted)
|
|
460
|
+
return;
|
|
461
|
+
heldNoticePrinted = true;
|
|
462
|
+
console.error(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
463
|
+
}
|
|
450
464
|
function pumpMemoryHome(store, root, home, message) {
|
|
451
|
-
flushMemoryHome(store, hunchPaths(root).hunch, home, message);
|
|
465
|
+
const flush = flushMemoryHome(store, hunchPaths(root).hunch, home, message);
|
|
466
|
+
if (flush === "held")
|
|
467
|
+
noteHeld();
|
|
468
|
+
return flush;
|
|
452
469
|
}
|
|
453
470
|
function pumpPolicyHome(store, root, service, policyId, message) {
|
|
454
471
|
const home = service.repository.homeOfPolicy(policyId);
|
|
@@ -457,7 +474,9 @@ function pumpPolicyHome(store, root, service, policyId, message) {
|
|
|
457
474
|
pumpMemoryHome(store, root, home, message);
|
|
458
475
|
}
|
|
459
476
|
function pumpMemoryHomes(store, root, homes, message) {
|
|
460
|
-
flushMemoryHomes(store, hunchPaths(root).hunch, homes, message);
|
|
477
|
+
const flushes = flushMemoryHomes(store, hunchPaths(root).hunch, homes, message);
|
|
478
|
+
if (flushes.public === "held")
|
|
479
|
+
noteHeld();
|
|
461
480
|
}
|
|
462
481
|
// ---- init -----------------------------------------------------------------
|
|
463
482
|
program
|
|
@@ -767,7 +786,7 @@ program
|
|
|
767
786
|
// while the docs are still Git-clean so it can stage them atomically with
|
|
768
787
|
// the graph. Pre-refreshing would make the safety filter treat generated
|
|
769
788
|
// docs as user-dirty and leave them stranded outside the memory commit.
|
|
770
|
-
const healed = shouldAutoCommit ? [] : refreshExistingGrounding(root, store);
|
|
789
|
+
const healed = shouldAutoCommit && publicFlushCommitsGrounding(store, root) ? [] : refreshExistingGrounding(root, store);
|
|
771
790
|
console.log(`Indexed ${res.files} files:`);
|
|
772
791
|
console.log(` ${counts.symbols} symbols, ${counts.edges} edges, ${counts.components} components`);
|
|
773
792
|
for (const item of res.coverage) {
|
|
@@ -995,7 +1014,13 @@ program
|
|
|
995
1014
|
// each home's artifacts into at most one memory commit.
|
|
996
1015
|
const publicCapture = r.status === "written" && !toOverlay;
|
|
997
1016
|
const privateCapture = r.status === "written" && toOverlay;
|
|
998
|
-
const
|
|
1017
|
+
const publicNeedsFlush = publicCapture || graphRefreshed || publicRepairTouched;
|
|
1018
|
+
// The default-branch hold covers the post-commit hook too. Its baked-in --commit is
|
|
1019
|
+
// the installed auto-commit default, so only a --commit typed by hand overrides it.
|
|
1020
|
+
// Decided before the grounding refresh: a held capture leaves tracked docs at HEAD,
|
|
1021
|
+
// so `git commit -a` cannot commit counts whose record it leaves untracked.
|
|
1022
|
+
const publicHeld = doCommit && publicNeedsFlush && (opts.fromHook || opts.commit === undefined) && holdsOnDefaultBranch(hunchPaths(root).hunch);
|
|
1023
|
+
const groundingToStage = publicNeedsFlush && !publicHeld ? refreshCommittableGrounding(root, store) : [];
|
|
999
1024
|
if (!opts.fromHook && (r.status === "written" || graphRefreshed || publicRepairTouched)) {
|
|
1000
1025
|
const healed = refreshExistingGrounding(root, store);
|
|
1001
1026
|
const refreshed = [...new Set([...groundingToStage.map((file) => relative(root, file)), ...healed])];
|
|
@@ -1003,9 +1028,10 @@ program
|
|
|
1003
1028
|
console.log(` ↳ grounding refreshed: ${refreshed.join(", ")}`);
|
|
1004
1029
|
}
|
|
1005
1030
|
if (doCommit) {
|
|
1006
|
-
const publicNeedsFlush = publicCapture || graphRefreshed || publicRepairTouched;
|
|
1007
1031
|
const privateNeedsFlush = privateCapture || (privateCorrectionQueued && graphRefreshed) || g2Recorded > 0 || privateRepairTouched;
|
|
1008
|
-
|
|
1032
|
+
if (publicHeld && !opts.quiet)
|
|
1033
|
+
console.log(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
1034
|
+
const publicResult = publicNeedsFlush && !publicHeld
|
|
1009
1035
|
? commitAndPushHunch(hunchPaths(root).hunch, publicCapture
|
|
1010
1036
|
? `hunch: capture ${r.decision?.id ?? "decision"}`
|
|
1011
1037
|
: graphRefreshed
|
|
@@ -2283,12 +2309,13 @@ program
|
|
|
2283
2309
|
}
|
|
2284
2310
|
}
|
|
2285
2311
|
store.reindex();
|
|
2286
|
-
|
|
2287
|
-
flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: capture ${dec + con} inline intent(s)`);
|
|
2312
|
+
const flush = dec || con ? flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: capture ${dec + con} inline intent(s)`) : null;
|
|
2288
2313
|
if (!intents.length)
|
|
2289
2314
|
console.log("No `hunch-why:` / `hunch-rule:` comments found.");
|
|
2290
2315
|
else
|
|
2291
2316
|
console.log(`✓ captured ${dec} decision(s) + ${con} constraint(s) from inline comments${opts.private ? " [private overlay]" : ""}`);
|
|
2317
|
+
if (flush === "held")
|
|
2318
|
+
console.log(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
2292
2319
|
store.close();
|
|
2293
2320
|
});
|
|
2294
2321
|
// ---- conform (Architectural Conformance: does the code still satisfy recorded intent) ----
|
|
@@ -2330,7 +2357,7 @@ program
|
|
|
2330
2357
|
valid_from: now,
|
|
2331
2358
|
});
|
|
2332
2359
|
store.reindex();
|
|
2333
|
-
if (home === "public" && !store
|
|
2360
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
2334
2361
|
refreshExistingGrounding(root, store);
|
|
2335
2362
|
pumpMemoryHome(store, root, home, `hunch: record architectural invariant ${d.id}`);
|
|
2336
2363
|
console.log(`✓ recorded architectural invariant ${d.id}: "${opts.add}"`);
|
|
@@ -4047,7 +4074,7 @@ program
|
|
|
4047
4074
|
}
|
|
4048
4075
|
store.reindex();
|
|
4049
4076
|
const home = store.captureHome(!!opts.private);
|
|
4050
|
-
if (home === "public" && !store
|
|
4077
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
4051
4078
|
refreshExistingGrounding(root, store);
|
|
4052
4079
|
const flush = flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: import ${decisions.length} ADR(s) from ${dir}`);
|
|
4053
4080
|
const live = decisions.filter((d) => d.status === "accepted").length;
|
|
@@ -4066,6 +4093,8 @@ program
|
|
|
4066
4093
|
}
|
|
4067
4094
|
if (flush === "pushed")
|
|
4068
4095
|
console.log(" ↳ private memory committed + pushed");
|
|
4096
|
+
if (flush === "held")
|
|
4097
|
+
console.log(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
4069
4098
|
}
|
|
4070
4099
|
finally {
|
|
4071
4100
|
store.close();
|
|
@@ -4203,7 +4232,7 @@ program
|
|
|
4203
4232
|
store.reindex();
|
|
4204
4233
|
// Public grounding is a publishable artifact. Private rules stay local and
|
|
4205
4234
|
// are surfaced by local checks/MCP, never copied into committed agent docs.
|
|
4206
|
-
if (home === "public" && !store
|
|
4235
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
4207
4236
|
refreshExistingGrounding(root, store);
|
|
4208
4237
|
const flush = flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: capture ${c.id}`);
|
|
4209
4238
|
console.log(`✓ recorded ${c.severity} constraint ${c.id}: "${c.statement}" (scope: ${scope.join(", ") || "repo"})${opts.private ? " [private overlay]" : ""}`);
|
|
@@ -4217,6 +4246,8 @@ program
|
|
|
4217
4246
|
}
|
|
4218
4247
|
if (flush === "pushed")
|
|
4219
4248
|
console.log(" ↳ private memory committed + pushed");
|
|
4249
|
+
if (flush === "held")
|
|
4250
|
+
console.log(` ↳ ${HELD_ON_DEFAULT_BRANCH}`);
|
|
4220
4251
|
store.close();
|
|
4221
4252
|
});
|
|
4222
4253
|
// ---- test (failure-learning loop) -----------------------------------------
|
|
@@ -4960,7 +4991,7 @@ program
|
|
|
4960
4991
|
// is on, but no commit happens to carry that refresh when it's off, so do it here
|
|
4961
4992
|
// too — otherwise a retired constraint keeps showing as enforced on disk (the same
|
|
4962
4993
|
// gap record-constraint and `conform --add` close for their own capture path).
|
|
4963
|
-
if (home === "public" && !store
|
|
4994
|
+
if (home === "public" && !publicFlushCommitsGrounding(store, root))
|
|
4964
4995
|
refreshExistingGrounding(root, store);
|
|
4965
4996
|
// `--reason` goes in the commit BODY, never the subject: `hunch log`'s move
|
|
4966
4997
|
// classifier (src/core/memorylog.ts) regexes the commit SUBJECT for keywords
|
|
@@ -4968,11 +4999,14 @@ program
|
|
|
4968
4999
|
// in the subject could accidentally match one and misclassify the move.
|
|
4969
5000
|
const willCommit = home === "private" ? store.privateAutoCommit : store.autoCommit;
|
|
4970
5001
|
const message = `hunch: retire constraint ${id}${opts.reason ? `\n\n${opts.reason}` : ""}`;
|
|
4971
|
-
pumpMemoryHome(store, root, home, message);
|
|
5002
|
+
const flush = pumpMemoryHome(store, root, home, message);
|
|
4972
5003
|
console.log(`✓ ${retired.id} retired — window closed at ${retired.valid_to?.slice(0, 10)}.`);
|
|
4973
5004
|
if (opts.reason && !willCommit) {
|
|
4974
5005
|
console.log(" ⚠ --reason is not recorded anywhere: auto-commit is off, so no commit message captured it.");
|
|
4975
5006
|
}
|
|
5007
|
+
else if (opts.reason && flush === "held") {
|
|
5008
|
+
console.log(" ⚠ --reason is not recorded anywhere: the commit was held on the default branch, so no commit message captured it.");
|
|
5009
|
+
}
|
|
4976
5010
|
store.close();
|
|
4977
5011
|
});
|
|
4978
5012
|
// ---- firmness (agent-hook enforcement level) ------------------------------
|
|
@@ -5198,10 +5232,14 @@ program
|
|
|
5198
5232
|
if (!evt)
|
|
5199
5233
|
return;
|
|
5200
5234
|
eventName = evt.hook_event_name;
|
|
5201
|
-
|
|
5235
|
+
// Grounding reads the store of the checkout the event's file is in; the prompt's
|
|
5236
|
+
// task and the hook evidence stay with the process root that opened them.
|
|
5237
|
+
const processRoot = findRoot();
|
|
5238
|
+
const routed = hookEventRoot(processRoot, evt);
|
|
5239
|
+
const root = routed.root;
|
|
5202
5240
|
// The host delivered this event: runtime evidence for `hunch integrations check`,
|
|
5203
5241
|
// recorded before any policy decision so firmness never hides delivery itself.
|
|
5204
|
-
recordHookObservation(
|
|
5242
|
+
recordHookObservation(processRoot, provider, evt.hook_event_name, evt.tool_outcome?.status);
|
|
5205
5243
|
const paths = hunchPaths(root);
|
|
5206
5244
|
const firmness = readConfig(paths).firmness;
|
|
5207
5245
|
if (firmness === "off")
|
|
@@ -5225,7 +5263,7 @@ program
|
|
|
5225
5263
|
// Same rule as the pre-edit path: strict never grounds from stale team rules.
|
|
5226
5264
|
const stale = firmness === "strict" && opened.teamPullStatus && !teamPullIsFresh(opened.teamPullStatus);
|
|
5227
5265
|
if (!stale)
|
|
5228
|
-
shellGround = shellWriteGrounding(root, store, provider, evt, written);
|
|
5266
|
+
shellGround = shellWriteGrounding(root, store, provider, evt, written, processRoot);
|
|
5229
5267
|
}
|
|
5230
5268
|
}
|
|
5231
5269
|
else {
|
|
@@ -5582,9 +5620,13 @@ program
|
|
|
5582
5620
|
return;
|
|
5583
5621
|
}
|
|
5584
5622
|
const targets = editTargets(root, evt.tool_input);
|
|
5585
|
-
// Nothing inside the repo → nothing for Hunch to say
|
|
5586
|
-
|
|
5623
|
+
// Nothing inside the repo → nothing for Hunch to say, except for a file in a
|
|
5624
|
+
// linked worktree of this repository Hunch could not ground (never silently).
|
|
5625
|
+
if (!targets.length) {
|
|
5626
|
+
if (routed.ungrounded)
|
|
5627
|
+
emitContext(provider, "PreToolUse", routed.ungrounded);
|
|
5587
5628
|
return;
|
|
5629
|
+
}
|
|
5588
5630
|
// Grounding below stays about the first in-repo file; the strict gate checks every target.
|
|
5589
5631
|
const { abs, target } = targets[0];
|
|
5590
5632
|
// A compiled red→green probe is only meaningful if its red receipt exists
|
|
@@ -5646,12 +5688,12 @@ program
|
|
|
5646
5688
|
: `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
5689
|
emitDeny(provider, reason);
|
|
5648
5690
|
for (const d of denials)
|
|
5649
|
-
observeHookDenial(
|
|
5691
|
+
observeHookDenial(processRoot, provider, evt, d.target, d.hit);
|
|
5650
5692
|
return;
|
|
5651
5693
|
}
|
|
5652
5694
|
}
|
|
5653
5695
|
// advisory / firm / strict(non-blocking): inject the relevant Hunch slice.
|
|
5654
|
-
const grounded = fileGrounding(root, store, provider, evt, target, abs);
|
|
5696
|
+
const grounded = fileGrounding(root, store, provider, evt, target, abs, processRoot);
|
|
5655
5697
|
if (!grounded)
|
|
5656
5698
|
return; // no noise on files Hunch hasn't learned yet
|
|
5657
5699
|
emitContext(provider, "PreToolUse", grounded.text, grounded.mode === "full" ? grounded.recalled ?? undefined : undefined);
|
|
@@ -5830,7 +5872,7 @@ program
|
|
|
5830
5872
|
store.reindex();
|
|
5831
5873
|
if (home === "public") {
|
|
5832
5874
|
publicGroundingChanged = true;
|
|
5833
|
-
if (!store
|
|
5875
|
+
if (!publicFlushCommitsGrounding(store, root))
|
|
5834
5876
|
refreshExistingGrounding(root, store);
|
|
5835
5877
|
}
|
|
5836
5878
|
console.log(opts.approveImport
|
|
@@ -5890,7 +5932,7 @@ program
|
|
|
5890
5932
|
store.reindex();
|
|
5891
5933
|
if (home === "public") {
|
|
5892
5934
|
publicGroundingChanged = true;
|
|
5893
|
-
if (!store
|
|
5935
|
+
if (!publicFlushCommitsGrounding(store, root))
|
|
5894
5936
|
refreshExistingGrounding(root, store);
|
|
5895
5937
|
}
|
|
5896
5938
|
}
|
|
@@ -5909,7 +5951,7 @@ program
|
|
|
5909
5951
|
store.reindex();
|
|
5910
5952
|
if (home === "public") {
|
|
5911
5953
|
publicGroundingChanged = true;
|
|
5912
|
-
if (!store
|
|
5954
|
+
if (!publicFlushCommitsGrounding(store, root))
|
|
5913
5955
|
refreshExistingGrounding(root, store);
|
|
5914
5956
|
}
|
|
5915
5957
|
console.log(`✓ accepted ${opts.accept} (now ${source}, confidence 0.95${armed ? `, ${armed} tripwire(s) now blocking` : ""})`);
|
|
@@ -5968,7 +6010,7 @@ program
|
|
|
5968
6010
|
armedTotal += accepted.armed;
|
|
5969
6011
|
}
|
|
5970
6012
|
store.reindex();
|
|
5971
|
-
if (publicGroundingChanged && !store
|
|
6013
|
+
if (publicGroundingChanged && !publicFlushCommitsGrounding(store, root))
|
|
5972
6014
|
refreshExistingGrounding(root, store);
|
|
5973
6015
|
console.log(`✓ accepted ${ready.length} verified draft(s); ${armedTotal} tripwire(s) now blocking.`);
|
|
5974
6016
|
for (const it of ready)
|
|
@@ -6128,7 +6170,7 @@ program
|
|
|
6128
6170
|
}
|
|
6129
6171
|
if (accepted || rejected) {
|
|
6130
6172
|
store.reindex();
|
|
6131
|
-
if (publicAccepted && !store
|
|
6173
|
+
if (publicAccepted && !publicFlushCommitsGrounding(store, root))
|
|
6132
6174
|
refreshExistingGrounding(root, store);
|
|
6133
6175
|
pumpMemoryHomes(store, root, touchedHomes, "hunch: auto-review decision lifecycle");
|
|
6134
6176
|
}
|
|
@@ -7599,15 +7641,64 @@ function toRepoRel(root, abs) {
|
|
|
7599
7641
|
return "";
|
|
7600
7642
|
return rel;
|
|
7601
7643
|
}
|
|
7644
|
+
/** The repository root a hook event belongs to. Normally the process root, but an
|
|
7645
|
+
* edit to a file in another LINKED worktree of the same repository (or a subagent
|
|
7646
|
+
* started in one) belongs to that worktree's memory: grounded against the process
|
|
7647
|
+
* root it was dropped as "outside the repo", with no output at all. Re-roots only
|
|
7648
|
+
* to a worktree sharing this repository's git common dir that carries its own
|
|
7649
|
+
* .hunch/; one without it is reported as ungrounded rather than skipped silently.
|
|
7650
|
+
* Checkouts are told apart by their `.git` entry, never by a .hunch/ hit, so a
|
|
7651
|
+
* nested package .hunch/ inside this checkout (a monorepo) never displaces the
|
|
7652
|
+
* process root and its blocking constraints. Git is spawned only for a file in a
|
|
7653
|
+
* different checkout than the process root's. */
|
|
7654
|
+
function hookEventRoot(processRoot, evt) {
|
|
7655
|
+
const toolEvent = evt.hook_event_name === "PreToolUse" || evt.hook_event_name === "PostToolUse" || evt.hook_event_name === "PostToolUseFailure";
|
|
7656
|
+
const subject = evt.hook_event_name === "SubagentStart" ? evt.cwd
|
|
7657
|
+
: toolEvent ? (evt.tool_input?.patch_files?.[0]?.path ?? evt.tool_input?.file_path) : undefined;
|
|
7658
|
+
if (!subject || !isAbsolute(subject))
|
|
7659
|
+
return { root: processRoot };
|
|
7660
|
+
try {
|
|
7661
|
+
const ownerTop = checkoutTop(evt.hook_event_name === "SubagentStart" ? subject : dirname(subject));
|
|
7662
|
+
const processTop = checkoutTop(processRoot);
|
|
7663
|
+
if (!ownerTop || !processTop || sameFilesystemEntry(ownerTop, processTop))
|
|
7664
|
+
return { root: processRoot };
|
|
7665
|
+
const common = checkoutCommonDir(processRoot);
|
|
7666
|
+
const ownerCommon = common ? checkoutCommonDir(ownerTop) : "";
|
|
7667
|
+
if (!common || !ownerCommon || !sameFilesystemEntry(common, ownerCommon))
|
|
7668
|
+
return { root: processRoot };
|
|
7669
|
+
// The same Hunch root inside the other worktree: its top, or the package dir a
|
|
7670
|
+
// subdirectory-rooted process uses.
|
|
7671
|
+
const owner = join(ownerTop, relative(processTop, processRoot));
|
|
7672
|
+
if (isDir(join(owner, ".hunch")))
|
|
7673
|
+
return { root: owner };
|
|
7674
|
+
return { root: processRoot, ungrounded: `Hunch: ${toPosixTarget(subject)} is in linked worktree ${toPosixTarget(ownerTop)}, which has no .hunch/ — this edit proceeds ungrounded.` };
|
|
7675
|
+
}
|
|
7676
|
+
catch {
|
|
7677
|
+
return { root: processRoot };
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7680
|
+
/** The nearest directory at or above `start` holding a `.git` entry (a checkout's top;
|
|
7681
|
+
* a linked worktree's `.git` is a file), or "" outside every checkout. Pure fs. */
|
|
7682
|
+
function checkoutTop(start) {
|
|
7683
|
+
let cur = resolve(start);
|
|
7684
|
+
for (;;) {
|
|
7685
|
+
if (existsSync(join(cur, ".git")))
|
|
7686
|
+
return cur;
|
|
7687
|
+
const parent = dirname(cur);
|
|
7688
|
+
if (parent === cur)
|
|
7689
|
+
return "";
|
|
7690
|
+
cur = parent;
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7602
7693
|
/** Files a shell command wrote get the grounding the edit tools would have
|
|
7603
7694
|
* delivered before the edit — late, but while the agent can still revise.
|
|
7604
7695
|
* Already-served grounding (a delta) is not repeated. */
|
|
7605
7696
|
const MAX_SHELL_GROUNDED = 3;
|
|
7606
|
-
function shellWriteGrounding(root, store, provider, evt, written) {
|
|
7697
|
+
function shellWriteGrounding(root, store, provider, evt, written, ledgerRoot = root) {
|
|
7607
7698
|
const parts = [];
|
|
7608
7699
|
const grounded = [];
|
|
7609
7700
|
for (const target of written.slice(0, MAX_SHELL_GROUNDED)) {
|
|
7610
|
-
const g = fileGrounding(root, store, provider, evt, target, join(root, target));
|
|
7701
|
+
const g = fileGrounding(root, store, provider, evt, target, join(root, target), ledgerRoot);
|
|
7611
7702
|
if (g?.mode === "full") {
|
|
7612
7703
|
parts.push(g.text);
|
|
7613
7704
|
grounded.push(target);
|
|
@@ -7726,8 +7817,9 @@ const SIBLING_MEMORY_BUDGET_TOKENS = 800;
|
|
|
7726
7817
|
* retired code, doc anchors, recent tasks and sibling-fix lessons. `delta` when
|
|
7727
7818
|
* identical grounding was already served this session, null when Hunch knows
|
|
7728
7819
|
* nothing about the file. Serves the pre-edit hook and files a shell command
|
|
7729
|
-
* wrote (which never pass through the edit tools).
|
|
7730
|
-
|
|
7820
|
+
* wrote (which never pass through the edit tools). `ledgerRoot` is where the prompt's
|
|
7821
|
+
* task lives — the process root when the event was re-rooted to a linked worktree. */
|
|
7822
|
+
function fileGrounding(root, store, provider, evt, target, abs, ledgerRoot = root) {
|
|
7731
7823
|
// Decision-grounding for PROSE (doc≠graph): a markdown target that declares
|
|
7732
7824
|
// <!-- hunch:topic … --> anchors gets each topic's CURRENT decision — the
|
|
7733
7825
|
// graph outranks the prose being edited, and a stale pin is called out inline.
|
|
@@ -7744,7 +7836,7 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7744
7836
|
// at warning, not relevance guesses. No selection (a host without a prompt
|
|
7745
7837
|
// hook, a legacy session, a prompt that selected nothing) keeps the unfiltered
|
|
7746
7838
|
// grounding.
|
|
7747
|
-
const selection = loadTaskSelection(hookReportTaskId(
|
|
7839
|
+
const selection = loadTaskSelection(hookReportTaskId(ledgerRoot, provider, evt));
|
|
7748
7840
|
const assembled = store.assembleContext(target);
|
|
7749
7841
|
const selected = selection ? new Set(selection.qualifying) : null;
|
|
7750
7842
|
const ctx = selected ? {
|
|
@@ -7779,7 +7871,7 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7779
7871
|
const retired = store.retiredForFile(target).filter((r) => r.symbols.length || r.deps.length);
|
|
7780
7872
|
// Recent-task history is advisory; under a task selection it is dropped
|
|
7781
7873
|
// (`hunch task list` still has it).
|
|
7782
|
-
const recentTasks = selected ? [] : taskSelectionSupplements(store.selectTasksAuto(target, buildTaskRankingQuery(
|
|
7874
|
+
const recentTasks = selected ? [] : taskSelectionSupplements(store.selectTasksAuto(target, buildTaskRankingQuery(ledgerRoot, hookReportTaskId(ledgerRoot, provider, evt), target, { excludeTargetDeliveries: true })), target);
|
|
7783
7875
|
const hasContent = ctx.constraints.length ||
|
|
7784
7876
|
ctx.decisions.length ||
|
|
7785
7877
|
ctx.bugs.length ||
|
|
@@ -7843,7 +7935,7 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7843
7935
|
delivery_profile: envelope.profile, ranking_policy: envelope.ranking_policy,
|
|
7844
7936
|
})),
|
|
7845
7937
|
]);
|
|
7846
|
-
const reportTaskId = hookReportTaskId(
|
|
7938
|
+
const reportTaskId = hookReportTaskId(ledgerRoot, provider, evt);
|
|
7847
7939
|
// A new authoritative prompt gets its own full delivery. An earlier
|
|
7848
7940
|
// prompt's session-level delta cannot establish this task's receipt.
|
|
7849
7941
|
// A subagent reports to the prompt's task but starts with FRESH context:
|
|
@@ -7870,8 +7962,8 @@ function fileGrounding(root, store, provider, evt, target, abs) {
|
|
|
7870
7962
|
const snapshots = snapshotDeliveredRecords(store, envelope);
|
|
7871
7963
|
// The first time a lesson reaches this prompt's task, tell the USER in one
|
|
7872
7964
|
// line (systemMessage); repeats of the same revision stay silent.
|
|
7873
|
-
recalled = reportPresentationEnabled(
|
|
7874
|
-
const occurrence = recordTaskDelivery(
|
|
7965
|
+
recalled = reportPresentationEnabled(ledgerRoot) ? renderRecalledLine(unseenLessons(ledgerRoot, reportTaskId, snapshots)) : null;
|
|
7966
|
+
const occurrence = recordTaskDelivery(ledgerRoot, reportTaskId, envelope, snapshots, undefined, target);
|
|
7875
7967
|
reportNotice = `\n\nHunch task ${reportTaskId} · delivery ${occurrence}. Inspect exact application references with hunch_report(task_id).`;
|
|
7876
7968
|
}
|
|
7877
7969
|
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,23 @@ 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) and
|
|
34
|
+
* injected context (`<system-reminder>`, hook output). Their words are not the
|
|
35
|
+
* human's, so "never use X" quoted from a constraint is not a correction. A tag
|
|
36
|
+
* left unclosed drops only the rest of its own line, never the human's text below. */
|
|
37
|
+
const HOST_TAGS = "task-notification|system-reminder|local-command-stdout|local-command-stderr|command-message|command-name|command-args";
|
|
38
|
+
const HOST_BLOCK = new RegExp(`<(${HOST_TAGS})>[\\s\\S]*?<\\/\\1>`, "gi");
|
|
39
|
+
const HOST_OPEN_LINE = new RegExp(`<(?:${HOST_TAGS})>[^\\n]*`, "gi");
|
|
40
|
+
/** The prompt with host-generated blocks removed: what the human actually typed. */
|
|
41
|
+
export function stripHostBlocks(prompt) {
|
|
42
|
+
return prompt.replace(HOST_BLOCK, " ").replace(HOST_OPEN_LINE, " ");
|
|
43
|
+
}
|
|
32
44
|
export function looksLikeCorrection(prompt) {
|
|
33
45
|
if (!prompt || typeof prompt !== "string")
|
|
34
46
|
return false;
|
|
35
|
-
|
|
47
|
+
const human = stripHostBlocks(prompt);
|
|
48
|
+
return CORRECTION_PATTERNS.some((re) => re.test(human));
|
|
36
49
|
}
|
|
37
50
|
/** One-line nudge appended to the UserPromptSubmit hook context when a prompt
|
|
38
51
|
* reads like a correction — surfaces the write tool so the rule gets ENFORCED,
|
|
@@ -35,7 +35,7 @@ export declare function computeSupersedes(record: TaskRecord, others: readonly T
|
|
|
35
35
|
export interface PersistedTaskRecord {
|
|
36
36
|
record: TaskRecord;
|
|
37
37
|
home: TaskRecordHome;
|
|
38
|
-
flushed: "pushed" | "committed" | null;
|
|
38
|
+
flushed: "pushed" | "committed" | "held" | null;
|
|
39
39
|
/** False when the same report revision was already in the graph. */
|
|
40
40
|
changed: boolean;
|
|
41
41
|
}
|
package/dist/core/taskRecord.js
CHANGED
|
@@ -15,7 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { flushCapture } from "../integrations/sync.js";
|
|
16
16
|
import { hunchPaths } from "./paths.js";
|
|
17
17
|
import { episodeTasks, isEmptyTaskReport, readTaskReport, reportHash, sessionsOverlap } from "./taskReport.js";
|
|
18
|
-
import {
|
|
18
|
+
import { currentSource } from "./taskReportEvidence.js";
|
|
19
19
|
import { ENTITY_KINDS, TaskRecordSchema } from "./types.js";
|
|
20
20
|
import { refreshRankEval } from "./taskRankingMode.js";
|
|
21
21
|
import { gitTouchedFiles } from "./taskTouched.js";
|
|
@@ -172,7 +172,7 @@ export function computeSupersedes(record, others, limit = 20) {
|
|
|
172
172
|
export function persistTaskRecord(root, store, taskId, options = {}) {
|
|
173
173
|
if (!taskRecordsEnabled(root))
|
|
174
174
|
return null;
|
|
175
|
-
const snapshot =
|
|
175
|
+
const snapshot = currentSource(root);
|
|
176
176
|
const own = readTaskReport(root, taskId, snapshot);
|
|
177
177
|
if (own.task.state === "open")
|
|
178
178
|
return null;
|
|
@@ -30,6 +30,7 @@ export declare const ReportCheckSchema: z.ZodObject<{
|
|
|
30
30
|
snapshot_limitations: z.ZodArray<z.ZodString>;
|
|
31
31
|
timed_out: z.ZodBoolean;
|
|
32
32
|
cancelled: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
work_root: z.ZodOptional<z.ZodString>;
|
|
33
34
|
source: z.ZodLiteral<"local-command-runner">;
|
|
34
35
|
}, z.core.$strict>;
|
|
35
36
|
export type ReportCheck = z.infer<typeof ReportCheckSchema>;
|
|
@@ -321,8 +322,8 @@ export interface TaskSummary {
|
|
|
321
322
|
}
|
|
322
323
|
/** One bounded summary per recent task for status lines and host views; the
|
|
323
324
|
* card and evidence view remain the authoritative renderings. */
|
|
324
|
-
export declare function summarizeTaskReport(root: string, taskId: string, currentSnapshot?:
|
|
325
|
-
export declare function listTaskSummaries(root: string, limit?: number, currentSnapshot?:
|
|
325
|
+
export declare function summarizeTaskReport(root: string, taskId: string, currentSnapshot?: CurrentSnapshot): TaskSummary;
|
|
326
|
+
export declare function listTaskSummaries(root: string, limit?: number, currentSnapshot?: CurrentSnapshot): TaskSummary[];
|
|
326
327
|
/** One line for a terminal status line. Empty string when nothing was observed
|
|
327
328
|
* for the task, so a bare prompt shows no Hunch noise at all. */
|
|
328
329
|
export declare function renderTaskStatusLine(summary: TaskSummary | null): string;
|
|
@@ -348,5 +349,9 @@ export declare function reportActivity(root: string): string;
|
|
|
348
349
|
export declare function forgetReportTask(root: string, taskId: string): void;
|
|
349
350
|
export declare function pruneReportHistory(root: string, olderThanDays?: number): number;
|
|
350
351
|
export declare function reportPresentationEnabled(root: string): boolean;
|
|
351
|
-
|
|
352
|
+
/** What a reader compares evidence snapshots against: the report root's current source
|
|
353
|
+
* hash, or a resolver that also hashes another worktree a check ran in. A plain hash
|
|
354
|
+
* never vouches for a check that ran elsewhere. */
|
|
355
|
+
export type CurrentSnapshot = string | null | ((dir?: string) => string | null);
|
|
356
|
+
export declare function readTaskReport(root: string, taskId: string, currentSnapshot?: CurrentSnapshot): TaskReport;
|
|
352
357
|
export {};
|
package/dist/core/taskReport.js
CHANGED
|
@@ -43,6 +43,9 @@ export const ReportCheckSchema = z.object({
|
|
|
43
43
|
snapshot_limitations: z.array(z.string().max(300)).max(16),
|
|
44
44
|
timed_out: z.boolean(),
|
|
45
45
|
cancelled: z.boolean().optional(),
|
|
46
|
+
/** Where the command ran, recorded only when that is not this ledger's root (a
|
|
47
|
+
* sibling worktree): its currentness is judged against that tree's source. */
|
|
48
|
+
work_root: z.string().min(1).max(4_096).optional(),
|
|
46
49
|
source: z.literal("local-command-runner"),
|
|
47
50
|
}).strict();
|
|
48
51
|
export const ReportSaveSchema = z.object({
|
|
@@ -717,6 +720,7 @@ export function reportPresentationEnabled(root) {
|
|
|
717
720
|
}
|
|
718
721
|
}
|
|
719
722
|
export function readTaskReport(root, taskId, currentSnapshot = null) {
|
|
723
|
+
const snapshotAt = (dir) => typeof currentSnapshot === "function" ? currentSnapshot(dir) : dir === undefined ? currentSnapshot : null;
|
|
720
724
|
const { task, events } = taskDb(root, db => transaction(db, () => {
|
|
721
725
|
const task = readTask(db, root, taskId);
|
|
722
726
|
const { bytes } = db.prepare("SELECT COALESCE(SUM(length(CAST(body AS BLOB))), 0) AS bytes FROM report_events WHERE task_id = ?").get(taskId);
|
|
@@ -743,7 +747,7 @@ export function readTaskReport(root, taskId, currentSnapshot = null) {
|
|
|
743
747
|
claims.push({ ...ReportClaimSchema.parse(value), at: event.at, attribution: "agent-reported", supported_by: null });
|
|
744
748
|
else if (event.kind === "conformance") {
|
|
745
749
|
const rule = ReportConformanceSchema.parse(value);
|
|
746
|
-
conformance.push({ ...rule, event_id: event.event_id, at: event.at, current:
|
|
750
|
+
conformance.push({ ...rule, event_id: event.event_id, at: event.at, current: snapshotAt() !== null && rule.snapshot === snapshotAt() });
|
|
747
751
|
}
|
|
748
752
|
else if (event.kind === "save")
|
|
749
753
|
saves.push({ ...ReportSaveSchema.parse(value), event_id: event.event_id, at: event.at, durability: "local", proofs: [] });
|
|
@@ -760,7 +764,8 @@ export function readTaskReport(root, taskId, currentSnapshot = null) {
|
|
|
760
764
|
refusals.push({ ...ReportRefusalSchema.parse(value), event_id: event.event_id, at: event.at });
|
|
761
765
|
else if (event.kind === "check") {
|
|
762
766
|
const check = ReportCheckSchema.parse(value);
|
|
763
|
-
|
|
767
|
+
const now = snapshotAt(check.work_root);
|
|
768
|
+
checks.push({ ...check, at: event.at, current: now !== null && check.before_snapshot === now && check.after_snapshot === now });
|
|
764
769
|
}
|
|
765
770
|
else if (event.kind !== "check-start")
|
|
766
771
|
throw new Error("unsupported report event kind; update Hunch before reading this report");
|
|
@@ -9,6 +9,9 @@ export interface ReportSnapshot {
|
|
|
9
9
|
/** Bounded source snapshot, not a committed change proof. Ignored files and
|
|
10
10
|
* Hunch's own memory/cache are excluded and explicitly disclosed. */
|
|
11
11
|
export declare function reportSourceSnapshot(root: string): ReportSnapshot;
|
|
12
|
+
/** The current source of `root` for report readers, as a resolver that also hashes the
|
|
13
|
+
* sibling worktree a check ran in (its `work_root`). Each tree is hashed at most once. */
|
|
14
|
+
export declare function currentSource(root: string): (dir?: string) => string | null;
|
|
12
15
|
/** Evaluate every delivered lesson's DECLARED rule against the files this task
|
|
13
16
|
* changed (working tree vs HEAD). Deterministic and Hunch-owned: the agent
|
|
14
17
|
* neither chooses the rule nor submits the verdict. A constraint is judged by its
|
|
@@ -28,4 +31,8 @@ export declare function runReportCheck(root: string, taskId: string, command: st
|
|
|
28
31
|
signal?: AbortSignal;
|
|
29
32
|
onStdout?: (chunk: Buffer) => void;
|
|
30
33
|
onStderr?: (chunk: Buffer) => void;
|
|
34
|
+
/** Where the command runs and whose source is snapshotted, when the task's
|
|
35
|
+
* ledger lives in a sibling worktree of the same repository (the hook opened
|
|
36
|
+
* the task at the main checkout, the agent works in a linked one). */
|
|
37
|
+
workRoot?: string;
|
|
31
38
|
}): Promise<ReportCheck>;
|
|
@@ -87,6 +87,16 @@ export function reportSourceSnapshot(root) {
|
|
|
87
87
|
return { hash: null, limitations: [...limitations, "A complete bounded source snapshot was unavailable; no current-change verification is claimed."] };
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/** The current source of `root` for report readers, as a resolver that also hashes the
|
|
91
|
+
* sibling worktree a check ran in (its `work_root`). Each tree is hashed at most once. */
|
|
92
|
+
export function currentSource(root) {
|
|
93
|
+
const seen = new Map();
|
|
94
|
+
return (dir = root) => {
|
|
95
|
+
if (!seen.has(dir))
|
|
96
|
+
seen.set(dir, reportSourceSnapshot(dir).hash);
|
|
97
|
+
return seen.get(dir);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
90
100
|
/** Evaluate every delivered lesson's DECLARED rule against the files this task
|
|
91
101
|
* changed (working tree vs HEAD). Deterministic and Hunch-owned: the agent
|
|
92
102
|
* neither chooses the rule nor submits the verdict. A constraint is judged by its
|
|
@@ -210,7 +220,8 @@ export async function runReportCheck(root, taskId, command, label, timeoutMs = D
|
|
|
210
220
|
const task = readTaskReport(root, taskId).task;
|
|
211
221
|
if (task.state !== "open" && task.closed_by !== "host")
|
|
212
222
|
throw new Error("cannot verify a closed task");
|
|
213
|
-
const
|
|
223
|
+
const workRoot = options.workRoot ?? root;
|
|
224
|
+
const before = reportSourceSnapshot(workRoot);
|
|
214
225
|
// Validate sensitive arguments before executing or writing anything.
|
|
215
226
|
ReportCheckSchema.parse({ label, command, exit_code: null, output_hash: reportHash(""), before_snapshot: before.hash, after_snapshot: null, snapshot_limitations: before.limitations, timed_out: false, source: "local-command-runner" });
|
|
216
227
|
const checkId = beginReportCheck(root, taskId, label, timeoutMs);
|
|
@@ -228,7 +239,7 @@ export async function runReportCheck(root, taskId, command, label, timeoutMs = D
|
|
|
228
239
|
const started = (() => {
|
|
229
240
|
try {
|
|
230
241
|
const resolved = resolveSpawnCommand(command);
|
|
231
|
-
return spawn(resolved.file, resolved.args, { cwd:
|
|
242
|
+
return spawn(resolved.file, resolved.args, { cwd: workRoot, shell: false, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, detached: process.platform !== "win32", windowsVerbatimArguments: resolved.windowsVerbatimArguments === true });
|
|
232
243
|
}
|
|
233
244
|
catch (error) {
|
|
234
245
|
// An argument the launcher cannot carry, or a synchronous spawn refusal
|
|
@@ -319,9 +330,10 @@ export async function runReportCheck(root, taskId, command, label, timeoutMs = D
|
|
|
319
330
|
if (options.signal?.aborted)
|
|
320
331
|
cancel();
|
|
321
332
|
});
|
|
322
|
-
const after = reportSourceSnapshot(
|
|
333
|
+
const after = reportSourceSnapshot(workRoot);
|
|
323
334
|
const check = { check_id: checkId, label, command, exit_code: result.code, output_hash: result.hash, before_snapshot: before.hash, after_snapshot: after.hash,
|
|
324
|
-
snapshot_limitations: [...new Set([...before.limitations, ...after.limitations])], timed_out: result.timedOut, cancelled: result.cancelled,
|
|
335
|
+
snapshot_limitations: [...new Set([...before.limitations, ...after.limitations])], timed_out: result.timedOut, cancelled: result.cancelled,
|
|
336
|
+
...(workRoot !== root ? { work_root: workRoot } : {}), source: "local-command-runner" };
|
|
325
337
|
recordReportCheck(root, taskId, check);
|
|
326
338
|
return check;
|
|
327
339
|
}
|
|
@@ -6,7 +6,7 @@ import { findRoot } from "./paths.js";
|
|
|
6
6
|
import { canonicalReportRoot } from "./taskReportPaths.js";
|
|
7
7
|
import { isCredentialFreeText } from "./types.js";
|
|
8
8
|
import { aliasReportTask, continuationLinks, finishReportTask, isEmptyTaskReport, latestSessionTask, readTaskReport, recordReportRefusal, reportHash, reportPresentationEnabled, reportTaskExists, resolveReportTask, settleSessionTasks, startReportTask } from "./taskReport.js";
|
|
9
|
-
import {
|
|
9
|
+
import { currentSource } from "./taskReportEvidence.js";
|
|
10
10
|
import { renderTaskReport } from "./taskReportRender.js";
|
|
11
11
|
import { verificationLauncher } from "./verifyLauncher.js";
|
|
12
12
|
import { injectionMode } from "./hookcache.js";
|
|
@@ -290,7 +290,7 @@ export function stopHookReport(root, provider, event) {
|
|
|
290
290
|
if (id === "legacy")
|
|
291
291
|
return { systemMessage: "Hunch hook active. This host version does not provide an exact prompt identifier, so contribution for this response is unverified. Explicit task reports remain available with hunch report." };
|
|
292
292
|
try {
|
|
293
|
-
const report = readTaskReport(root, id,
|
|
293
|
+
const report = readTaskReport(root, id, currentSource(root));
|
|
294
294
|
if (isEmptyTaskReport(report))
|
|
295
295
|
return null;
|
|
296
296
|
// The HTML evidence view is a rendering of the local ledger, generated on
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { JsonStore } from "../store/jsonStore.js";
|
|
4
4
|
import { hunchPaths } from "./paths.js";
|
|
5
5
|
import { readTaskReport, reportHash } from "./taskReport.js";
|
|
6
|
-
import {
|
|
6
|
+
import { currentSource } from "./taskReportEvidence.js";
|
|
7
7
|
export function publicTaskReport(root, taskId) {
|
|
8
|
-
const report = readTaskReport(root, taskId,
|
|
8
|
+
const report = readTaskReport(root, taskId, currentSource(root));
|
|
9
9
|
const publicStore = new JsonStore(hunchPaths(root));
|
|
10
10
|
const records = new Map();
|
|
11
11
|
const deliveries = [];
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { writeFileAtomic } from "./io.js";
|
|
3
3
|
import { readTaskReport, readLessonHistory } from "./taskReport.js";
|
|
4
|
-
import {
|
|
4
|
+
import { currentSource } from "./taskReportEvidence.js";
|
|
5
5
|
import { assertReportPath } from "./taskReportPaths.js";
|
|
6
6
|
import { publicTaskReport, renderPublicTaskReportHtml } from "./taskReportPublic.js";
|
|
7
7
|
export function writeTaskReportHtml(root, taskId, publicOnly = false) {
|
|
8
|
-
const report = readTaskReport(root, taskId,
|
|
8
|
+
const report = readTaskReport(root, taskId, currentSource(root));
|
|
9
9
|
const directory = assertReportPath(root, ".hunch-cache", "reports");
|
|
10
10
|
mkdirSync(directory, { recursive: true });
|
|
11
11
|
const file = assertReportPath(root, ".hunch-cache", "reports", `${report.task.task_id}${publicOnly ? ".public" : ""}.html`);
|
package/dist/extractors/git.d.ts
CHANGED
|
@@ -266,6 +266,16 @@ export declare function pathKnownToHistory(root: string, file: string): boolean;
|
|
|
266
266
|
/** Current branch name (e.g. "main", "feat/x"), or "" in detached HEAD / non-repo.
|
|
267
267
|
* Stamped onto auto-captured decisions so branch-scoped work stays filterable. */
|
|
268
268
|
export declare function currentBranch(cwd: string): string;
|
|
269
|
+
/** Whether `cwd` has its repository's default branch checked out: origin/HEAD's branch
|
|
270
|
+
* when the remote names one, else `main` or `master`. False on a detached HEAD or
|
|
271
|
+
* outside a repository, so a caller keeps its pre-existing behavior there. */
|
|
272
|
+
export declare function onDefaultBranch(cwd: string): boolean;
|
|
273
|
+
/** Whether a memory commit of `hunchDir` would have anything to stage: a tracked file git
|
|
274
|
+
* sees as changed (after its own line-ending normalization) or an untracked, non-ignored
|
|
275
|
+
* file, skipping the derived artifacts and the per-machine local.json the commit never
|
|
276
|
+
* stages. A hook's inherited GIT_DIR/GIT_WORK_TREE is stripped, so the repository is
|
|
277
|
+
* resolved from `hunchDir` itself. False when git cannot answer. */
|
|
278
|
+
export declare function hunchDirHasChanges(hunchDir: string): boolean;
|
|
269
279
|
/** Whether HEAD is somewhere a write must not land: mid rebase/merge/cherry-pick/revert/bisect
|
|
270
280
|
* ("git-operation-in-progress", which wins when both hold), or detached ("detached-head").
|
|
271
281
|
* null when HEAD is a settled branch — and also when git cannot answer at all (not a repo,
|
package/dist/extractors/git.js
CHANGED
|
@@ -2283,6 +2283,33 @@ export function currentBranch(cwd) {
|
|
|
2283
2283
|
const b = gitSafe(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
2284
2284
|
return b === "HEAD" ? "" : b; // detached HEAD reports "HEAD" — treat as no branch
|
|
2285
2285
|
}
|
|
2286
|
+
/** Whether `cwd` has its repository's default branch checked out: origin/HEAD's branch
|
|
2287
|
+
* when the remote names one, else `main` or `master`. False on a detached HEAD or
|
|
2288
|
+
* outside a repository, so a caller keeps its pre-existing behavior there. */
|
|
2289
|
+
export function onDefaultBranch(cwd) {
|
|
2290
|
+
const branch = currentBranch(cwd);
|
|
2291
|
+
if (!branch)
|
|
2292
|
+
return false;
|
|
2293
|
+
const remoteHead = gitSafe(["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
2294
|
+
if (remoteHead)
|
|
2295
|
+
return branch === remoteHead.replace(/^origin\//, "");
|
|
2296
|
+
return branch === "main" || branch === "master";
|
|
2297
|
+
}
|
|
2298
|
+
/** Whether a memory commit of `hunchDir` would have anything to stage: a tracked file git
|
|
2299
|
+
* sees as changed (after its own line-ending normalization) or an untracked, non-ignored
|
|
2300
|
+
* file, skipping the derived artifacts and the per-machine local.json the commit never
|
|
2301
|
+
* stages. A hook's inherited GIT_DIR/GIT_WORK_TREE is stripped, so the repository is
|
|
2302
|
+
* resolved from `hunchDir` itself. False when git cannot answer. */
|
|
2303
|
+
export function hunchDirHasChanges(hunchDir) {
|
|
2304
|
+
const changed = [
|
|
2305
|
+
...gitSafeIsolated(["diff", "--name-only", "--relative", "--no-renames", "HEAD", "--", "."], hunchDir).split("\n"),
|
|
2306
|
+
...gitSafeIsolated(["ls-files", "--others", "--exclude-standard", "--", "."], hunchDir).split("\n"),
|
|
2307
|
+
];
|
|
2308
|
+
return changed.some((raw) => {
|
|
2309
|
+
const path = raw.trim().replace(/\\/g, "/");
|
|
2310
|
+
return path !== "" && path !== "local.json" && !isDerivedStoreArtifact(path);
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2286
2313
|
/** The state files git writes while it is replaying history. `git rev-parse --git-path`
|
|
2287
2314
|
* resolves each against THIS worktree's private git dir (a linked worktree keeps its own
|
|
2288
2315
|
* rebase-merge/MERGE_HEAD), so a rebase in one worktree never reads as one in another. */
|
|
@@ -197,7 +197,7 @@ export function inspectIntegrations(root, selected) {
|
|
|
197
197
|
try {
|
|
198
198
|
const launcher = readLauncher(root, harness);
|
|
199
199
|
recordPins(spec.mcp, [launcher.command, ...launcher.args]);
|
|
200
|
-
capabilities.mcp.detail =
|
|
200
|
+
capabilities.mcp.detail = `Configured locally; run \`hunch integrations check --probe --harness ${harness}\` to verify a fresh server, then reconnect the host`;
|
|
201
201
|
}
|
|
202
202
|
catch (e) {
|
|
203
203
|
// A harness can be detected here via its hooks file alone —
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { type Harness, type IntegrationHealth } from "./health.js";
|
|
2
|
+
/** A launcher that runs THIS Hunch build directly (`node <build>/cli/index.js mcp`) —
|
|
3
|
+
* a local checkout or an unpacked test build. It is exactly the code already
|
|
4
|
+
* running the probe, so starting it executes nothing new; any other custom
|
|
5
|
+
* command stays inspect-only. The server identity/version check still applies.
|
|
6
|
+
* The command is a bare `node` (PATH lookup) or this exact Node binary — a
|
|
7
|
+
* directory-qualified `./tools/node` in a cloned repo is not, whatever its name —
|
|
8
|
+
* and the probe spawns process.execPath itself, never the configured name. */
|
|
9
|
+
export declare function isSelfLauncher(launcher: {
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
}, entry?: string): boolean;
|
|
2
13
|
/** Opt-in: starts a fresh configured MCP process, never claims the current
|
|
3
14
|
* harness connection or hook delivery has been tested. No model is invoked. */
|
|
4
15
|
export declare function probeIntegration(root: string, harness: Harness, report: IntegrationHealth, timeoutMs?: number): Promise<void>;
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { sameFilesystemEntry } from "../extractors/git.js";
|
|
3
7
|
import { HARNESSES, readLauncher } from "./health.js";
|
|
8
|
+
/** A launcher that runs THIS Hunch build directly (`node <build>/cli/index.js mcp`) —
|
|
9
|
+
* a local checkout or an unpacked test build. It is exactly the code already
|
|
10
|
+
* running the probe, so starting it executes nothing new; any other custom
|
|
11
|
+
* command stays inspect-only. The server identity/version check still applies.
|
|
12
|
+
* The command is a bare `node` (PATH lookup) or this exact Node binary — a
|
|
13
|
+
* directory-qualified `./tools/node` in a cloned repo is not, whatever its name —
|
|
14
|
+
* and the probe spawns process.execPath itself, never the configured name. */
|
|
15
|
+
export function isSelfLauncher(launcher, entry = fileURLToPath(new URL("../cli/index.js", import.meta.url))) {
|
|
16
|
+
const bareNode = /^node(?:\.exe)?$/i.test(launcher.command);
|
|
17
|
+
const node = bareNode || (isAbsolute(launcher.command) && existsSync(launcher.command) && sameFilesystemEntry(launcher.command, process.execPath));
|
|
18
|
+
if (!node || launcher.args.length !== 2 || launcher.args[1] !== "mcp")
|
|
19
|
+
return false;
|
|
20
|
+
const script = launcher.args[0];
|
|
21
|
+
// Generated launchers name the build entry absolutely; a relative script would resolve
|
|
22
|
+
// against whatever cwd the host spawns in.
|
|
23
|
+
return isAbsolute(script) && existsSync(script) && existsSync(entry) && sameFilesystemEntry(script, entry);
|
|
24
|
+
}
|
|
4
25
|
/** Opt-in: starts a fresh configured MCP process, never claims the current
|
|
5
26
|
* harness connection or hook delivery has been tested. No model is invoked. */
|
|
6
27
|
export async function probeIntegration(root, harness, report, timeoutMs = 15_000) {
|
|
@@ -16,11 +37,15 @@ export async function probeIntegration(root, harness, report, timeoutMs = 15_000
|
|
|
16
37
|
throw new Error("custom MCP environment or working directory requires a host-side probe; no settings were ignored");
|
|
17
38
|
// Only execute the exact published launcher Hunch generates. Custom
|
|
18
39
|
// commands may need credentials or have unrelated effects; inspect only.
|
|
19
|
-
|
|
40
|
+
const generated = /^npx(?:\.cmd)?$/.test(launcher.command) && JSON.stringify(launcher.args) === JSON.stringify([
|
|
20
41
|
"-y", `--package=hunch-exact@npm:@davesheffer/hunch@${report.expectedVersion}`, "hunch", "mcp",
|
|
21
|
-
])
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
]);
|
|
43
|
+
const self = !generated && isSelfLauncher(launcher);
|
|
44
|
+
if (!generated && !self)
|
|
45
|
+
throw new Error("probe requires the generated exact-version npx launcher, or a `node <build>/cli/index.js mcp` launcher probed by that same build; repair stale pins first; other custom launchers remain untested");
|
|
46
|
+
// A self launcher runs this exact Node binary: no PATH/cwd lookup of `node` can
|
|
47
|
+
// resolve to an executable planted in the repository.
|
|
48
|
+
transport = new StdioClientTransport({ ...launcher, ...(self ? { command: process.execPath } : {}), cwd: root, stderr: "pipe" });
|
|
24
49
|
transport.stderr?.on("data", () => { });
|
|
25
50
|
await Promise.race([
|
|
26
51
|
(async () => {
|
|
@@ -11,11 +11,11 @@ export type MemoryHome = "public" | "private";
|
|
|
11
11
|
/** Flush one exact artifact home. Constitution repositories can explicitly
|
|
12
12
|
* choose public even in unified mode, so this must not infer routing through
|
|
13
13
|
* captureHome(isPrivate). The caller supplies the repository's actual home. */
|
|
14
|
-
export declare function flushMemoryHome(store: HunchStore, publicHunchDir: string, home: MemoryHome, message: string, remoteOverride?: HunchRemoteContract): "pushed" | "committed" | null;
|
|
14
|
+
export declare function flushMemoryHome(store: HunchStore, publicHunchDir: string, home: MemoryHome, message: string, remoteOverride?: HunchRemoteContract): "pushed" | "committed" | "held" | null;
|
|
15
15
|
/** One completion flush per touched home. A mixed ingest/bootstrap can write
|
|
16
16
|
* both homes; unchanged homes are cheap no-ops after the memory-only stage
|
|
17
17
|
* check, while each real home becomes durable exactly once. */
|
|
18
|
-
export declare function flushMemoryHomes(store: HunchStore, publicHunchDir: string, homes: Iterable<MemoryHome>, message: string, remoteOverride?: HunchRemoteContract): Partial<Record<MemoryHome, "pushed" | "committed" | null>>;
|
|
18
|
+
export declare function flushMemoryHomes(store: HunchStore, publicHunchDir: string, homes: Iterable<MemoryHome>, message: string, remoteOverride?: HunchRemoteContract): Partial<Record<MemoryHome, "pushed" | "committed" | "held" | null>>;
|
|
19
19
|
/** Auto-commit + push the overlay after a private write, when auto-commit is on. No-op
|
|
20
20
|
* otherwise (manual `hunch private --sync` still works). Never throws. */
|
|
21
21
|
export declare function flushPrivate(store: HunchStore, message: string): void;
|
|
@@ -24,7 +24,19 @@ export declare function flushPrivate(store: HunchStore, message: string): void;
|
|
|
24
24
|
* "committed" (commit created but not pushed — public .hunch/ rides the next push; an
|
|
25
25
|
* overlay commit whose merge/push failed retries on the next flush), or null (auto-commit
|
|
26
26
|
* off, no overlay for a private record, or the commit was skipped — lock held, safety
|
|
27
|
-
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up)
|
|
27
|
+
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up),
|
|
28
|
+
* or "held" (a public capture left uncommitted because the default branch is checked
|
|
29
|
+
* out; callers say so with HELD_ON_DEFAULT_BRANCH). */
|
|
28
30
|
export declare function flushCapture(store: HunchStore, publicHunchDir: string, isPrivate: boolean, message: string,
|
|
29
31
|
/** Long-lived callers can pin the route snapshot that admitted the write. */
|
|
30
|
-
remoteOverride?: HunchRemoteContract, observe?: GitMemoryObserver): "pushed" | "committed" | null;
|
|
32
|
+
remoteOverride?: HunchRemoteContract, observe?: GitMemoryObserver): "pushed" | "committed" | "held" | null;
|
|
33
|
+
/** Auto-commit a state-contract write (`nuryel_write`, `hunch serve`). A state root is a
|
|
34
|
+
* dedicated data repository whose writes commit on whatever branch it serves, so the
|
|
35
|
+
* default-branch hold for captures never applies; outcomes as flushCapture otherwise. */
|
|
36
|
+
export declare function flushStateWrite(store: HunchStore, publicHunchDir: string, isPrivate: boolean, message: string, remoteOverride?: HunchRemoteContract): "pushed" | "committed" | null;
|
|
37
|
+
/** What a caller prints for a "held" flush, so a solo user on main is never left
|
|
38
|
+
* wondering why the capture commit stopped appearing. */
|
|
39
|
+
export declare const HELD_ON_DEFAULT_BRANCH = "not auto-committed: the default branch is checked out \u2014 your next commit that adds .hunch/ carries it (to restore auto-commit there, set \"autoCommitDefaultBranch\": true in .hunch/local.json)";
|
|
40
|
+
/** Whether an automatic public memory commit must be held: the default branch is checked
|
|
41
|
+
* out and the user has not opted back in. Shared by flushStore and `hunch sync`. */
|
|
42
|
+
export declare function holdsOnDefaultBranch(publicHunchDir: string): boolean;
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* commits (bug_overlay_clobber); the memory commit rides their next push instead.
|
|
8
8
|
* Used by every CLI/MCP path that writes a record.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { commitAndPushHunch, hunchDirHasChanges, onDefaultBranch } from "../extractors/git.js";
|
|
12
13
|
import { refreshCommittableGrounding } from "./providers.js";
|
|
13
14
|
import { advertisedTeamRemoteContract } from "./team.js";
|
|
14
15
|
const pinnedSharedRoutes = new WeakMap();
|
|
@@ -41,6 +42,10 @@ export function flushMemoryHome(store, publicHunchDir, home, message, remoteOver
|
|
|
41
42
|
}
|
|
42
43
|
if (!store.autoCommit)
|
|
43
44
|
return null;
|
|
45
|
+
// Same hold as flushCapture: the public commit stages all of .hunch/, so it would
|
|
46
|
+
// otherwise sweep a capture that was just reported as held onto the default branch.
|
|
47
|
+
if (holdsOnDefaultBranch(publicHunchDir))
|
|
48
|
+
return heldOrNothing(publicHunchDir);
|
|
44
49
|
const grounding = refreshCommittableGrounding(dirname(publicHunchDir), store);
|
|
45
50
|
return commitAndPushHunch(publicHunchDir, message, { push: false, alsoStage: grounding });
|
|
46
51
|
}
|
|
@@ -70,10 +75,21 @@ export function flushPrivate(store, message) {
|
|
|
70
75
|
* "committed" (commit created but not pushed — public .hunch/ rides the next push; an
|
|
71
76
|
* overlay commit whose merge/push failed retries on the next flush), or null (auto-commit
|
|
72
77
|
* off, no overlay for a private record, or the commit was skipped — lock held, safety
|
|
73
|
-
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up)
|
|
78
|
+
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up),
|
|
79
|
+
* or "held" (a public capture left uncommitted because the default branch is checked
|
|
80
|
+
* out; callers say so with HELD_ON_DEFAULT_BRANCH). */
|
|
74
81
|
export function flushCapture(store, publicHunchDir, isPrivate, message,
|
|
75
82
|
/** Long-lived callers can pin the route snapshot that admitted the write. */
|
|
76
83
|
remoteOverride, observe) {
|
|
84
|
+
return flushStore(store, publicHunchDir, isPrivate, message, remoteOverride, observe, true);
|
|
85
|
+
}
|
|
86
|
+
/** Auto-commit a state-contract write (`nuryel_write`, `hunch serve`). A state root is a
|
|
87
|
+
* dedicated data repository whose writes commit on whatever branch it serves, so the
|
|
88
|
+
* default-branch hold for captures never applies; outcomes as flushCapture otherwise. */
|
|
89
|
+
export function flushStateWrite(store, publicHunchDir, isPrivate, message, remoteOverride) {
|
|
90
|
+
return flushStore(store, publicHunchDir, isPrivate, message, remoteOverride, undefined, false);
|
|
91
|
+
}
|
|
92
|
+
function flushStore(store, publicHunchDir, isPrivate, message, remoteOverride, observe, holdOnDefaultBranch) {
|
|
77
93
|
// Follow the same routing as HunchStore.captureHome: unified ("shared") mode homes
|
|
78
94
|
// EVERY capture in the overlay, so the flush must go there too — one source of truth.
|
|
79
95
|
if (store.captureHome(isPrivate) === "private") {
|
|
@@ -89,10 +105,45 @@ remoteOverride, observe) {
|
|
|
89
105
|
}
|
|
90
106
|
if (!store.autoCommit)
|
|
91
107
|
return null;
|
|
108
|
+
// A capture never commits onto the default branch by itself: task and capture commits
|
|
109
|
+
// landed on main between the user's own. The record stays on disk; the user's next
|
|
110
|
+
// commit carries it, or the next capture on a feature branch sweeps it up. Held before
|
|
111
|
+
// the grounding refresh, so an automatic capture (hook sync, MCP) leaves tracked docs at
|
|
112
|
+
// HEAD instead of counting a record that `git commit -a` would leave behind; by-hand CLI
|
|
113
|
+
// commands refresh them on disk themselves (publicFlushCommitsGrounding in the CLI).
|
|
114
|
+
if (holdOnDefaultBranch && holdsOnDefaultBranch(publicHunchDir))
|
|
115
|
+
return heldOrNothing(publicHunchDir);
|
|
92
116
|
// A public capture changes record counts, so refresh git-clean grounding docs and fold
|
|
93
117
|
// them into the SAME memory commit — otherwise every capture re-stales the committed
|
|
94
118
|
// counts and the release gate's clean-tree check fails on the next CI index.
|
|
95
119
|
const grounding = refreshCommittableGrounding(dirname(publicHunchDir), store);
|
|
96
120
|
return commitAndPushHunch(publicHunchDir, message, { push: false, alsoStage: grounding, observe });
|
|
97
121
|
}
|
|
122
|
+
/** What a caller prints for a "held" flush, so a solo user on main is never left
|
|
123
|
+
* wondering why the capture commit stopped appearing. */
|
|
124
|
+
export const HELD_ON_DEFAULT_BRANCH = "not auto-committed: the default branch is checked out — your next commit that adds .hunch/ carries it (to restore auto-commit there, set \"autoCommitDefaultBranch\": true in .hunch/local.json)";
|
|
125
|
+
/** A hold reports "held" only when there is something left for the next commit; a clean
|
|
126
|
+
* .hunch/ is null, as an unheld flush with nothing staged would be. */
|
|
127
|
+
function heldOrNothing(publicHunchDir) {
|
|
128
|
+
return hunchDirHasChanges(publicHunchDir) ? "held" : null;
|
|
129
|
+
}
|
|
130
|
+
/** Whether an automatic public memory commit must be held: the default branch is checked
|
|
131
|
+
* out and the user has not opted back in. Shared by flushStore and `hunch sync`. */
|
|
132
|
+
export function holdsOnDefaultBranch(publicHunchDir) {
|
|
133
|
+
return !defaultBranchCommitsAllowed(publicHunchDir) && onDefaultBranch(dirname(publicHunchDir));
|
|
134
|
+
}
|
|
135
|
+
/** Opt back in to capture commits on the default branch: `autoCommitDefaultBranch: true`
|
|
136
|
+
* in the per-machine .hunch/local.json, or HUNCH_AUTO_COMMIT_DEFAULT_BRANCH=1. */
|
|
137
|
+
function defaultBranchCommitsAllowed(publicHunchDir) {
|
|
138
|
+
if (process.env.HUNCH_AUTO_COMMIT_DEFAULT_BRANCH === "1")
|
|
139
|
+
return true;
|
|
140
|
+
try {
|
|
141
|
+
const raw = readFileSync(join(publicHunchDir, "local.json"), "utf8");
|
|
142
|
+
const local = JSON.parse(raw.startsWith("\uFEFF") ? raw.slice(1) : raw);
|
|
143
|
+
return !!local && typeof local === "object" && local.autoCommitDefaultBranch === true;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
98
149
|
//# sourceMappingURL=sync.js.map
|
|
@@ -47,7 +47,7 @@ export type SnapshotOutcome = {
|
|
|
47
47
|
status: "written";
|
|
48
48
|
record: Workspace;
|
|
49
49
|
home: "private" | "public";
|
|
50
|
-
flushed: "pushed" | "committed" | null;
|
|
50
|
+
flushed: "pushed" | "committed" | "held" | null;
|
|
51
51
|
};
|
|
52
52
|
/** Record this machine's snapshot. Honors `workspaces.publish`, skips a write when the
|
|
53
53
|
* content is unchanged and the stored record is under a day old (an idle machine's hooks
|
package/dist/mcp/server.js
CHANGED
|
@@ -34,7 +34,7 @@ import { refreshExistingGrounding } from "../integrations/providers.js";
|
|
|
34
34
|
import { workspaceLedgerView, renderWorktreeTable, renderBranchTable, workspaceSummaryLine, snapshotHasHome, recordWorkspaceSnapshot, branchRows, worktreeRows } from "../integrations/workspaceLedger.js";
|
|
35
35
|
import { workspacesConfig } from "../core/config.js";
|
|
36
36
|
import { revParse, asOfDate, revExists, lastChangeDate, checkFreshnessRevision, teamPullIsFresh, rangeFiles, rangeGateDiff, commitFiles, commitGateDiff, stagedFiles, stagedGateDiff, workingFiles, workingGateDiff, pullHunchStatusDetailed, describePullFailure, sameRemoteUrl, currentBranch, worktreePaths, pathKnownToHistory } from "../extractors/git.js";
|
|
37
|
-
import { flushCapture, flushMemoryHome, pinSharedRemote } from "../integrations/sync.js";
|
|
37
|
+
import { flushCapture, flushStateWrite, flushMemoryHome, pinSharedRemote, HELD_ON_DEFAULT_BRANCH } from "../integrations/sync.js";
|
|
38
38
|
import { withWriteLock } from "../serve/writelock.js";
|
|
39
39
|
import { advertisedTeamRemoteContract, ensureTeamOverlay, isTeamStoreTrusted, overlayMatchesTeamRemote, teamWiringConsented, untrustedTeamStoreMessage, readTeamConfig, teamRemoteContract, teamSharedRef } from "../integrations/team.js";
|
|
40
40
|
import { formatSearchHit, formatStructure } from "../core/format.js";
|
|
@@ -113,12 +113,13 @@ function cwdHintOf(input) {
|
|
|
113
113
|
/** Honest auto-commit suffix: reports only what flushCapture ACTUALLY did. A skipped
|
|
114
114
|
* commit (backstop/lock/nothing staged) says nothing — the record is on disk and the
|
|
115
115
|
* next flush sweeps it up; claiming "auto-committed" there would be a lie. */
|
|
116
|
-
const flushNote = (flush, home, mode) => flush === "
|
|
117
|
-
: flush === "committed"
|
|
118
|
-
|
|
119
|
-
?
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
const flushNote = (flush, home, mode) => flush === "held" ? ` (${HELD_ON_DEFAULT_BRANCH})`
|
|
117
|
+
: flush === "pushed" ? ` (committed + pushed to the ${mode === "shared" ? "shared team store" : "private repo"})`
|
|
118
|
+
: flush === "committed"
|
|
119
|
+
? home === "private"
|
|
120
|
+
? " (committed to the overlay repo — push deferred: offline, no upstream, or merge conflict; the next capture or `hunch private --sync` retries)"
|
|
121
|
+
: " (auto-committed to .hunch/ — rides your next push)"
|
|
122
|
+
: "";
|
|
122
123
|
/** When an overlay exists, a PUBLIC write deserves one visible line: a record that
|
|
123
124
|
* lands in the committed store publishes on the next push, and an agent writing
|
|
124
125
|
* strategy/competitive content there is a leak nobody notices until it ships
|
|
@@ -2767,7 +2768,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2767
2768
|
// not race the HTTP server between the ledger read and the record write.
|
|
2768
2769
|
const { hunchDir } = stateHomeFor(store, input.scope);
|
|
2769
2770
|
const result = await withWriteLock(hunchDir, () => writeState(store, { schema: STATE_WRITE_VERSION, ...input }, {
|
|
2770
|
-
flush: (isPrivate, message) =>
|
|
2771
|
+
flush: (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
2771
2772
|
}));
|
|
2772
2773
|
return stateResult(`${result.outcome} ${result.record_id} (${result.durability}) ${result.record_hash}`, result);
|
|
2773
2774
|
}
|
|
@@ -2784,7 +2785,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2784
2785
|
try {
|
|
2785
2786
|
const { hunchDir } = stateHomeFor(store, input.scope);
|
|
2786
2787
|
const result = await withWriteLock(hunchDir, () => captureState(store, { schema: STATE_CAPTURE_VERSION, ...input }, {
|
|
2787
|
-
flush: (isPrivate, message) =>
|
|
2788
|
+
flush: (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
2788
2789
|
}));
|
|
2789
2790
|
return stateResult(`${result.outcome} observation ${result.record_id} (${result.durability}); this does not assert currentness. ${result.record_hash}`, result);
|
|
2790
2791
|
}
|
|
@@ -2801,7 +2802,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2801
2802
|
try {
|
|
2802
2803
|
const { hunchDir } = stateHomeFor(store, input.scope);
|
|
2803
2804
|
const result = await withWriteLock(hunchDir, () => captureBatchState(store, { schema: STATE_CAPTURE_BATCH_VERSION, ...input }, {
|
|
2804
|
-
flush: (isPrivate, message) =>
|
|
2805
|
+
flush: (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
2805
2806
|
}));
|
|
2806
2807
|
return stateResult(`Capture batch: ${result.results.filter(r => r.status === "saved").length} saved/replayed, ${result.results.filter(r => r.status === "refused").length} refused.${result.reviews ? ` Reviews: ${result.reviews.filter(r => r.status === "saved").length} withdrawn/replayed, ${result.reviews.filter(r => r.status === "refused").length} refused.` : ''} Inspect each indexed result.`, result);
|
|
2807
2808
|
}
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { verificationLauncherFor } from "../core/verifyLauncher.js";
|
|
3
3
|
import { TaskIdSchema, ReportClaimSchema, LessonReferenceSchema, finishReportTask, listReportTasks, readTaskReport, readLessonHistory, recordReportClaim, reportPresentationEnabled, startReportTask } from "../core/taskReport.js";
|
|
4
4
|
import { persistTaskRecord } from "../core/taskRecord.js";
|
|
5
|
-
import {
|
|
5
|
+
import { currentSource, runReportConformance } from "../core/taskReportEvidence.js";
|
|
6
6
|
import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.js";
|
|
7
7
|
function applicationReferences(report) {
|
|
8
8
|
return report.deliveries.flatMap(d => d.records.map(r => ({ occurrence_id: d.occurrence_id, record_id: r.record_id, content_hash: r.content_hash, title: r.title }))).slice(-100);
|
|
@@ -85,7 +85,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
|
|
|
85
85
|
// from the model must not fork a second report or fail the start.
|
|
86
86
|
if (!task_id || !/different title/.test(error.message))
|
|
87
87
|
throw error;
|
|
88
|
-
task = readTaskReport(root, task_id,
|
|
88
|
+
task = readTaskReport(root, task_id, currentSource(root)).task;
|
|
89
89
|
}
|
|
90
90
|
const launcher = verificationLauncher();
|
|
91
91
|
// Compact by design (#370): the rules an agent needs to act, and exact
|
|
@@ -120,7 +120,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
|
|
|
120
120
|
catch (error) {
|
|
121
121
|
graphNote = `\nGraph not saved: ${error.message}`;
|
|
122
122
|
}
|
|
123
|
-
const report = readTaskReport(root, task_id,
|
|
123
|
+
const report = readTaskReport(root, task_id, currentSource(root));
|
|
124
124
|
const show = reportPresentationEnabled(root);
|
|
125
125
|
// The HTML evidence view is rendered on demand (hunch_report(html: true),
|
|
126
126
|
// `hunch report <id> --html`, or the VS Code view); finish writes no file.
|
|
@@ -165,7 +165,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
|
|
|
165
165
|
const tasks = listReportTasks(root);
|
|
166
166
|
return { content: [{ type: "text", text: tasks.length ? tasks.map(t => `${t.task_id} · ${t.state} · ${t.title}`).join("\n") : "No task reports yet. Start with hunch_task." }], structuredContent: { tasks } };
|
|
167
167
|
}
|
|
168
|
-
const report = readTaskReport(root, task_id,
|
|
168
|
+
const report = readTaskReport(root, task_id, currentSource(root));
|
|
169
169
|
const file = html ? writeTaskReportHtml(root, task_id) : null;
|
|
170
170
|
return { content: [{ type: "text", text: `${renderTaskReport(report)}${file ? `\nLocal evidence view: ${file}` : ""}` }], structuredContent: { ...boundedTaskReportForHost(report), application_references: applicationReferences(report), contribution_card: renderTaskReport(report), report_path: file } };
|
|
171
171
|
}
|
package/dist/serve/app.js
CHANGED
|
@@ -23,7 +23,7 @@ import { STATE_PROOF_CAPABILITY } from '../core/stateProof.js';
|
|
|
23
23
|
import { createServer } from "node:http";
|
|
24
24
|
import { HunchStore } from "../store/hunchStore.js";
|
|
25
25
|
import { hunchPaths } from "../core/paths.js";
|
|
26
|
-
import {
|
|
26
|
+
import { flushStateWrite } from "../integrations/sync.js";
|
|
27
27
|
import { StateRefusal, capabilities, mergeReadResponses, readState, recordsState, stateHomeFor, subscribeState, writeState } from "../store/stateBinding.js";
|
|
28
28
|
import { STATE_READ_VERSION, STATE_RECORDS_VERSION, STATE_SUBSCRIBE_VERSION, STATE_WRITE_VERSION, ReadScopesSchema, ScopeSchema, scopePath } from "../core/stateContract.js";
|
|
29
29
|
import { partitionFor, resolveCredential, readServeConfig } from "./config.js";
|
|
@@ -174,7 +174,7 @@ export function createServeApp(config, opts = {}) {
|
|
|
174
174
|
// Only authenticated grants select stores for cross-partition source visibility.
|
|
175
175
|
const accessOptions = { additionalStores: principal.grants.map(grant => requestStore(grant).store) };
|
|
176
176
|
const { store, root } = requestStore(scope);
|
|
177
|
-
const flush = (isPrivate, message) =>
|
|
177
|
+
const flush = (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message);
|
|
178
178
|
if (route === "read") {
|
|
179
179
|
if (body.observed_page !== undefined && body.scopes !== undefined)
|
|
180
180
|
throw problem(400, 'malformed', 'observation pages require a single partition without scopes');
|
package/dist/taskReports.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export declare function createTaskReporter(root: string): {
|
|
|
47
47
|
source: "local-command-runner";
|
|
48
48
|
check_id?: string | undefined;
|
|
49
49
|
cancelled?: boolean | undefined;
|
|
50
|
+
work_root?: string | undefined;
|
|
50
51
|
}>;
|
|
51
52
|
/** Hunch evaluates each delivered lesson's declared rule against the changed
|
|
52
53
|
* files. Deterministic and local; the harness supplies no verdict. */
|
package/dist/taskReports.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* lifecycle and display; neither operation depends on model compliance. */
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { finishReportTask, listReportTasks, readTaskReport, readLessonHistory, recordReportClaim, recordTaskDelivery, reportHash, reportPresentationEnabled, startReportTask } from "./core/taskReport.js";
|
|
5
|
-
import { DEFAULT_CHECK_TIMEOUT_MS,
|
|
5
|
+
import { DEFAULT_CHECK_TIMEOUT_MS, currentSource, runReportCheck, runReportConformance } from "./core/taskReportEvidence.js";
|
|
6
6
|
export { DEFAULT_CHECK_TIMEOUT_MS, MAX_CHECK_TIMEOUT_MS } from "./core/taskReportEvidence.js";
|
|
7
7
|
import { renderTaskReport, writeTaskReportHtml } from "./core/taskReportRender.js";
|
|
8
8
|
import { hunchPaths } from "./core/paths.js";
|
|
@@ -17,7 +17,7 @@ const IdentitySchema = z.object({ task: z.string().min(1).max(1024), attempt: z.
|
|
|
17
17
|
* their own principal/partition boundary before reaching this local API. */
|
|
18
18
|
export function createTaskReporter(root) {
|
|
19
19
|
const scope = canonicalReportRoot(root);
|
|
20
|
-
const report = (taskId) => readTaskReport(scope, taskId,
|
|
20
|
+
const report = (taskId) => readTaskReport(scope, taskId, currentSource(scope));
|
|
21
21
|
return {
|
|
22
22
|
/** Omit identity for a fresh task, or supply the harness's stable task AND
|
|
23
23
|
* execution attempt for idempotent retries. Raw identities are not retained. */
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://www.hunchmemory.com",
|
|
10
|
-
"version": "1.47.
|
|
10
|
+
"version": "1.47.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.47.
|
|
16
|
+
"version": "1.47.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|