@davesheffer/hunch 1.39.2 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +154 -64
- package/dist/cli/serve.d.ts +14 -0
- package/dist/cli/serve.js +18 -4
- package/dist/constitution/g2BehaviorCandidates.js +6 -1
- package/dist/constitution/g2Candidates.js +11 -1
- package/dist/constitution/structural.js +10 -0
- package/dist/core/correction.js +12 -7
- package/dist/core/delivery.d.ts +34 -0
- package/dist/core/delivery.js +60 -1
- package/dist/core/docanchors.js +195 -11
- package/dist/core/format.js +6 -1
- package/dist/core/glob.d.ts +25 -0
- package/dist/core/glob.js +43 -0
- package/dist/core/hookcache.d.ts +9 -2
- package/dist/core/hookcache.js +10 -3
- package/dist/core/paths.d.ts +48 -5
- package/dist/core/paths.js +89 -6
- package/dist/core/procstart.d.ts +94 -0
- package/dist/core/procstart.js +244 -0
- package/dist/core/served.d.ts +6 -3
- package/dist/core/served.js +10 -6
- package/dist/core/taskDelivery.js +27 -1
- package/dist/core/taskReport.d.ts +2 -0
- package/dist/core/taskReport.js +46 -11
- package/dist/core/taskReportEvidence.js +17 -1
- package/dist/core/taskReportHook.d.ts +19 -0
- package/dist/core/taskReportHook.js +54 -6
- package/dist/core/verifyLauncher.d.ts +21 -0
- package/dist/core/verifyLauncher.js +37 -0
- package/dist/core/workspace.d.ts +9 -0
- package/dist/core/workspace.js +15 -0
- package/dist/extractors/git.d.ts +5 -0
- package/dist/extractors/git.js +87 -10
- package/dist/extractors/indexer.d.ts +10 -0
- package/dist/extractors/indexer.js +56 -10
- package/dist/extractors/k8sManifest.d.ts +13 -0
- package/dist/extractors/k8sManifest.js +207 -23
- package/dist/extractors/landscapeDiscovery.js +9 -1
- package/dist/extractors/nativeTreeSitter.d.ts +24 -0
- package/dist/extractors/nativeTreeSitter.js +54 -1
- package/dist/extractors/parse.js +6 -1
- package/dist/integrations/claudemd.js +3 -3
- package/dist/integrations/hookmatch.d.ts +25 -0
- package/dist/integrations/hookmatch.js +58 -0
- package/dist/integrations/hooks.d.ts +79 -9
- package/dist/integrations/hooks.js +559 -46
- package/dist/integrations/providers.js +2 -17
- package/dist/integrations/scaffold.d.ts +1 -1
- package/dist/integrations/scaffold.js +24 -21
- package/dist/integrations/workspaceLedger.d.ts +10 -0
- package/dist/integrations/workspaceLedger.js +9 -1
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +32 -16
- package/dist/mcp/taskReportTools.d.ts +5 -9
- package/dist/mcp/taskReportTools.js +9 -20
- package/dist/serve/writelock.d.ts +21 -1
- package/dist/serve/writelock.js +157 -16
- package/dist/store/changeLedger.d.ts +9 -3
- package/dist/store/changeLedger.js +36 -10
- package/dist/store/hunchStore.d.ts +42 -2
- package/dist/store/hunchStore.js +105 -17
- package/dist/store/jsonStore.d.ts +6 -4
- package/dist/store/jsonStore.js +225 -32
- package/dist/store/merge.d.ts +7 -4
- package/dist/store/merge.js +65 -5
- package/dist/store/privateMigrate.js +10 -0
- package/dist/wiki/adopt.js +13 -2
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* doctor environment diagnostics
|
|
15
15
|
*/
|
|
16
16
|
import "./preflight.js"; // MUST stay the first import — Node-version gate before node:sqlite loads
|
|
17
|
-
import { chmodSync, existsSync, lstatSync, readFileSync, readdirSync, readlinkSync, writeFileSync, mkdirSync, mkdtempSync,
|
|
17
|
+
import { chmodSync, existsSync, lstatSync, readFileSync, readdirSync, readlinkSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, rmdirSync, symlinkSync } from "node:fs";
|
|
18
18
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
19
19
|
import { join, relative, dirname, basename, resolve, isAbsolute } from "node:path";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { Command } from "commander";
|
|
23
|
-
import { hunchPaths, hunchPathsForDir, findRoot, toPosixTarget, isDir } from "../core/paths.js";
|
|
23
|
+
import { hunchPaths, hunchPathsForDir, findRoot, toPosixTarget, isDir, realpathNorm, repoRelativeTarget } from "../core/paths.js";
|
|
24
24
|
import { writeFileAtomic } from "../core/io.js";
|
|
25
25
|
import { looksLikeCorrection, CORRECTION_NUDGE } from "../core/correction.js";
|
|
26
26
|
import { HUNCH_VERSION } from "../core/version.js";
|
|
@@ -36,7 +36,10 @@ import { publishedStatus } from "../integrations/registry.js";
|
|
|
36
36
|
import { HunchStore } from "../store/hunchStore.js";
|
|
37
37
|
import { JsonStore } from "../store/jsonStore.js";
|
|
38
38
|
import { selectEmbedder } from "../store/embedder.js";
|
|
39
|
-
import { assertCompleteRepoScan, indexRepo, scanRepo } from "../extractors/indexer.js";
|
|
39
|
+
import { assertCompleteRepoScan, indexRepo, mergeScannedEdges, scanRepo } from "../extractors/indexer.js";
|
|
40
|
+
// Importing this module is free (the addon load is a function call, not a
|
|
41
|
+
// top-level side effect); only `doctor` below actually calls the loader.
|
|
42
|
+
import { isParserLoadError, loadNativeTreeSitter } from "../extractors/nativeTreeSitter.js";
|
|
40
43
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
41
44
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
42
45
|
import { readSynthesisPreference, resolveSynthesisProvider, selectProvider, SYNTH_PREFERENCES, writeSynthesisPreference, normalizeProviderName, } from "../synthesis/provider.js";
|
|
@@ -52,7 +55,7 @@ import { deriveForbids, effectiveForbids } from "../core/constraintmatch.js";
|
|
|
52
55
|
import { extractInlineIntent } from "../extractors/comments.js";
|
|
53
56
|
import { renderText, renderMarkdown, renderSarif, renderImpact, reportFailsStrict } from "../core/checkreport.js";
|
|
54
57
|
import { partitionReview, isReviewDraft, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
|
|
55
|
-
import { installPostCommitHook, installPreCommitHook, installPostMergeHook, installPostCheckoutHook, hookStatus, hookReport, formatHookInstall } from "../integrations/hooks.js";
|
|
58
|
+
import { installPostCommitHook, installPreCommitHook, installPostMergeHook, installPostCheckoutHook, hookStatus, hookReport, hookInvocationLines, formatHookInstall, sharedHooksNote } from "../integrations/hooks.js";
|
|
56
59
|
import { ensureSharedOverlayPointer } from "../integrations/worktree.js";
|
|
57
60
|
import { flushCapture, flushMemoryHome, flushMemoryHomes, pinSharedRemote, sharedRemoteFor } from "../integrations/sync.js";
|
|
58
61
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
@@ -72,7 +75,7 @@ import { rankingStatusLine, resolveTaskRankingMode } from "../core/taskRankingMo
|
|
|
72
75
|
import { diagnoseIssueCorrectionStage, formatCorrectionStageDiagnostic } from "../core/correctionStage.js";
|
|
73
76
|
import { compileVerifiedEvidenceMap, formatVerifiedEvidenceMap } from "../core/evidenceMap.js";
|
|
74
77
|
import { collectCorrectionStageSources } from "../extractors/correctionSources.js";
|
|
75
|
-
import { buildDeliveryEnvelope, DELIVERY_PROFILES } from "../core/delivery.js";
|
|
78
|
+
import { buildDeliveryEnvelope, deliveryDedupeInput, DELIVERY_PROFILES } from "../core/delivery.js";
|
|
76
79
|
import { deriveChangeIdentity } from "../core/changeIdentity.js";
|
|
77
80
|
import { deriveChangeProof } from "../core/changeProof.js";
|
|
78
81
|
import { discoverProjectDna, evaluateProjectDnaMatch } from "../core/projectDna.js";
|
|
@@ -80,7 +83,7 @@ import { diffProjectDna } from "../core/projectDnaDelta.js";
|
|
|
80
83
|
import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
|
|
81
84
|
import { readConfig, writeConfig, FIRMNESS_LEVELS, isFirmness, workspacesConfig } from "../core/config.js";
|
|
82
85
|
import { loadOrCreateMachine, setMachineLabel, machineFile, labelLeaksIdentity } from "../core/machine.js";
|
|
83
|
-
import { worktreeRows, branchRows } from "../core/workspace.js";
|
|
86
|
+
import { worktreeRows, branchRows, publicWorkspaceRemovalRecipe } from "../core/workspace.js";
|
|
84
87
|
import { workspaceLedgerView, recordWorkspaceSnapshot, renderWorktreeTable, renderBranchTable, workspaceSummaryLine, prunePlanFor, renderPrunePlan, applyPrune, confirmPrune, pruneConfirmQuestion } from "../integrations/workspaceLedger.js";
|
|
85
88
|
import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpolicy.js";
|
|
86
89
|
import { isHumanConfirmed } from "../core/strictgate.js";
|
|
@@ -88,7 +91,7 @@ import { appendEvent, readEvents } from "../core/events.js";
|
|
|
88
91
|
import { computeStats, formatStats } from "../core/stats.js";
|
|
89
92
|
import { injectionMode, resetSessionInjections } from "../core/hookcache.js";
|
|
90
93
|
import { recordServed, servedSummary } from "../core/served.js";
|
|
91
|
-
import { recordTaskDelivery, reportActivity, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
|
|
94
|
+
import { recordTaskDelivery, reportActivity, reportHash, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
|
|
92
95
|
import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
|
|
93
96
|
import { renderRecalledLine } from "../core/taskReportRender.js";
|
|
94
97
|
import { closeHookTask, hookReportTaskId, nativeHookCwd, settleHookSession, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
@@ -363,7 +366,7 @@ program
|
|
|
363
366
|
// any mutator. With no commit identity there is no safe public source,
|
|
364
367
|
// so leave no source-derived records behind for a future pump.
|
|
365
368
|
store.json.replaceAll("symbols", []);
|
|
366
|
-
store.json.replaceAll("edges", []);
|
|
369
|
+
store.json.replaceAll("edges", mergeScannedEdges(store.json.loadAll("edges"), []));
|
|
367
370
|
store.json.replaceAll("components", []);
|
|
368
371
|
store.reindex();
|
|
369
372
|
console.log(" ⚠ skipped code graph: no committed HEAD — commit code, then run `hunch index`");
|
|
@@ -395,15 +398,21 @@ program
|
|
|
395
398
|
console.log(" ✓ auto-commit OFF (captures stay uncommitted; commit .hunch/ yourself)");
|
|
396
399
|
}
|
|
397
400
|
}
|
|
401
|
+
// Collected so the linked-worktree note below can say what actually happened
|
|
402
|
+
// to the SHARED hooks dir (issue #316) rather than assuming.
|
|
403
|
+
const installs = [];
|
|
398
404
|
if (isGitRepo(root)) {
|
|
399
405
|
const syncToOverlay = !!(opts.privateSync || opts.sharedSync);
|
|
400
406
|
const h = installPostCommitHook(root, inv.shell, { private: syncToOverlay, commit: opts.autoCommit, localOnly: syncToOverlay });
|
|
407
|
+
installs.push(h);
|
|
401
408
|
for (const line of formatHookInstall(root, "post-commit hook", h, ` (learning loop)${syncToOverlay ? " — syncs to the shared overlay" : ""}${opts.autoCommit ? " — auto-commit on" : ""}`))
|
|
402
409
|
console.log(line);
|
|
403
410
|
const pm = installPostMergeHook(root, inv.shell);
|
|
411
|
+
installs.push(pm);
|
|
404
412
|
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
413
|
console.log(line);
|
|
406
414
|
const pc = installPostCheckoutHook(root, inv.shell);
|
|
415
|
+
installs.push(pc);
|
|
407
416
|
for (const line of formatHookInstall(root, "post-checkout hook", pc, " (workspace ledger: records this machine's branches + worktrees on checkout)"))
|
|
408
417
|
console.log(line);
|
|
409
418
|
const m = installMergeDriver(root, inv.shell);
|
|
@@ -414,6 +423,7 @@ program
|
|
|
414
423
|
if (opts.enforce !== false || opts.enforceStrict) {
|
|
415
424
|
const strict = !!opts.enforceStrict;
|
|
416
425
|
const p = installPreCommitHook(root, inv.shell, strict);
|
|
426
|
+
installs.push(p);
|
|
417
427
|
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
428
|
console.log(line);
|
|
419
429
|
}
|
|
@@ -469,9 +479,8 @@ program
|
|
|
469
479
|
if (ensureSharedOverlayPointer(root, store.privateDir, store.privateAutoCommit, store.mode === "shared" ? "shared" : "private")) {
|
|
470
480
|
console.log(` ✓ private overlay registered at the git common dir — shared by every worktree of this repo`);
|
|
471
481
|
}
|
|
472
|
-
|
|
473
|
-
console.log(
|
|
474
|
-
}
|
|
482
|
+
for (const line of sharedHooksNote(root, installs))
|
|
483
|
+
console.log(line);
|
|
475
484
|
store.close();
|
|
476
485
|
console.log("\n" + formatIntegrationHealth(inspectIntegrations(root)));
|
|
477
486
|
console.log("\nTask reporting is configured through Hunch's agent instructions; actual agent activity has not been verified by setup. Reconnect the agent, then work normally. Completed task reports are available with `hunch report`.");
|
|
@@ -497,8 +506,10 @@ program
|
|
|
497
506
|
// the same way; do the same for the hook so an upgrade doesn't require
|
|
498
507
|
// re-running init by hand. Gated on already having post-commit: `index`
|
|
499
508
|
// is not a setup command (it runs in CI, on any git repo), so it must
|
|
500
|
-
// never be what FIRST hooks a repo that never ran init at all.
|
|
501
|
-
|
|
509
|
+
// never be what FIRST hooks a repo that never ran init at all. A STALE
|
|
510
|
+
// post-commit block counts too (issue #315): it proves init ran, so the
|
|
511
|
+
// missing post-merge/post-checkout hooks are still this repo's to heal.
|
|
512
|
+
if (isGitRepo(root) && ["installed", "stale"].includes(hookReport(root).postCommit.state)) {
|
|
502
513
|
const inv = resolveInvocation().shell;
|
|
503
514
|
installPostMergeHook(root, inv);
|
|
504
515
|
installPostCheckoutHook(root, inv); // same upgrade path for the workspace-ledger hook
|
|
@@ -1434,6 +1445,20 @@ program
|
|
|
1434
1445
|
indexNote = `\n ✓ code graph present in the checkout — blast-radius ready`;
|
|
1435
1446
|
}
|
|
1436
1447
|
}
|
|
1448
|
+
catch (error) {
|
|
1449
|
+
// The code graph is this command's CONVENIENCE; the worktree + branch
|
|
1450
|
+
// are its promise, and git has already created them. A dead native
|
|
1451
|
+
// parser (loaded on first parse, so it surfaces here rather than at
|
|
1452
|
+
// import) used to abort between the two: no success line, the ledger
|
|
1453
|
+
// step skipped, and a re-run then hitting "path already exists" with no
|
|
1454
|
+
// way forward. Downgrade it to the same note channel the other optional
|
|
1455
|
+
// steps use — the worktree stands, the user is told the index was
|
|
1456
|
+
// skipped and why, and `hunch index` there fixes it once TMPDIR/the
|
|
1457
|
+
// install is sound. Anything else is still a real failure of this step.
|
|
1458
|
+
if (!isParserLoadError(error))
|
|
1459
|
+
throw error;
|
|
1460
|
+
indexNote = `\n · code graph skipped — ${error.message}\n (the worktree is ready; run \`hunch index\` there once the parser loads)`;
|
|
1461
|
+
}
|
|
1437
1462
|
finally {
|
|
1438
1463
|
wstore.close();
|
|
1439
1464
|
openStore = null;
|
|
@@ -1517,8 +1542,15 @@ workspacesCmd
|
|
|
1517
1542
|
return console.log(" · or set .hunch/config.json {\"workspaces\":{\"publish_public\":true}} to commit it into this repo's .hunch/");
|
|
1518
1543
|
case "unchanged":
|
|
1519
1544
|
return console.log(`✓ unchanged since ${out.previous.observed_at} (${out.record.id}) — nothing written`);
|
|
1545
|
+
case "deferred":
|
|
1546
|
+
return console.log(out.reason === "detached-head"
|
|
1547
|
+
? "deferred: HEAD is detached — the next checkout or ledger read records this machine"
|
|
1548
|
+
: "deferred: a git operation is in progress — the next checkout or ledger read records this machine");
|
|
1520
1549
|
case "collision":
|
|
1521
|
-
|
|
1550
|
+
// The capture is routed to the overlay, so the only home it can collide with is
|
|
1551
|
+
// this repo's committed `.hunch/`. `forget` refuses that record (an additive pump
|
|
1552
|
+
// never stages a deletion), so the recipe here is the manual git removal.
|
|
1553
|
+
return fail(`not written: ${out.reason}\n the stale copy lives in this repo's .hunch/ — remove it by hand, then snapshot again:\n${publicWorkspaceRemovalRecipe(out.record.id, out.record.machine.label)}`);
|
|
1522
1554
|
case "written":
|
|
1523
1555
|
return console.log(`✓ recorded ${out.record.worktrees.length} worktree(s), ${out.record.branches.length} branch(es) as ${out.record.machine.label} (${out.record.id}, publish=${out.record.publish}) → ${out.home === "private" ? "overlay" : "public .hunch/"}${out.flushed ? `, ${out.flushed}` : ""}`);
|
|
1524
1556
|
}
|
|
@@ -1577,7 +1609,7 @@ workspacesCmd
|
|
|
1577
1609
|
console.log(` ${r.outcome === "deleted" ? "✓" : r.outcome === "skipped" ? "–" : "✗"} ${r.step.branch}: ${r.detail}`);
|
|
1578
1610
|
const failed = results.filter((r) => r.outcome === "failed").length;
|
|
1579
1611
|
const skipped = results.filter((r) => r.outcome === "skipped").length;
|
|
1580
|
-
const ledger = recorded === "written" ? " · ledger updated" : recorded === "unchanged" || recorded === "no-home" || recorded === "off" ? "" : ` · ledger not updated (${recorded})`;
|
|
1612
|
+
const ledger = recorded === "written" ? " · ledger updated" : recorded === "unchanged" || recorded === "no-home" || recorded === "off" ? "" : recorded === "deferred" ? " · ledger deferred (git operation in progress or detached HEAD)" : ` · ledger not updated (${recorded})`;
|
|
1581
1613
|
console.log(`\n${results.length - failed - skipped} deleted, ${skipped ? `${skipped} skipped (nothing changed), ` : ""}${failed} refused by git${ledger}`);
|
|
1582
1614
|
if (failed || skipped)
|
|
1583
1615
|
process.exitCode = 1;
|
|
@@ -1605,12 +1637,25 @@ workspacesCmd
|
|
|
1605
1637
|
const victims = store.recs("workspaces").filter((r) => r.machine.label === machine || r.machine.id === machine || r.id === machine);
|
|
1606
1638
|
if (!victims.length)
|
|
1607
1639
|
return fail(`no workspace record for "${machine}" — \`hunch workspaces\` lists the machines in memory`);
|
|
1608
|
-
// Decide each record's home BEFORE deleting
|
|
1609
|
-
|
|
1610
|
-
|
|
1640
|
+
// Decide each record's home BEFORE deleting anything, then flush exactly those homes.
|
|
1641
|
+
// A PUBLIC record (workspaces.publish_public) is refused rather than deleted: the
|
|
1642
|
+
// memory pump never stages a tracked deletion, so removing the file here would strand
|
|
1643
|
+
// `D .hunch/workspaces/<id>.json` and wedge every later auto-commit — see
|
|
1644
|
+
// publicWorkspaceRemovalRecipe.
|
|
1645
|
+
const overlay = victims.filter((v) => store.getPrivateRec("workspaces", v.id));
|
|
1646
|
+
const refused = victims.filter((v) => !store.getPrivateRec("workspaces", v.id));
|
|
1647
|
+
for (const v of overlay)
|
|
1611
1648
|
store.deleteWhereItLives("workspaces", v.id);
|
|
1612
|
-
|
|
1613
|
-
|
|
1649
|
+
if (overlay.length) {
|
|
1650
|
+
pumpMemoryHome(store, root, "private", `hunch: forget workspace ${machine}`);
|
|
1651
|
+
console.log(`✓ forgot ${overlay.length} record(s) for ${machine}`);
|
|
1652
|
+
}
|
|
1653
|
+
for (const v of refused) {
|
|
1654
|
+
console.log(`refused: ${v.id} (${v.machine.label}) lives in this repo's .hunch/ — Hunch publication is additive and never stages a deletion, so it is removed by hand:\n`
|
|
1655
|
+
+ publicWorkspaceRemovalRecipe(v.id, v.machine.label));
|
|
1656
|
+
}
|
|
1657
|
+
if (refused.length && !overlay.length)
|
|
1658
|
+
process.exitCode = 1;
|
|
1614
1659
|
}
|
|
1615
1660
|
finally {
|
|
1616
1661
|
store.close();
|
|
@@ -4502,18 +4547,26 @@ program
|
|
|
4502
4547
|
.requiredOption("--by <new>", "decision id that supersedes it")
|
|
4503
4548
|
.action((oldId, opts) => {
|
|
4504
4549
|
const { store, root } = storeFor();
|
|
4505
|
-
|
|
4550
|
+
// `old`'s home decides which store the close is written to; `--by` must resolve in
|
|
4551
|
+
// that SAME store, or a private `by` linked against a public `old` would write the
|
|
4552
|
+
// private id straight into the committed public store.
|
|
4553
|
+
const home = decisionMemoryHome(store, oldId);
|
|
4554
|
+
if (!store.decisionInStore(oldId, home === "private")) {
|
|
4555
|
+
store.close();
|
|
4556
|
+
return fail(`decision "${oldId}" not found`);
|
|
4557
|
+
}
|
|
4558
|
+
const by = store.decisionInStore(opts.by, home === "private");
|
|
4506
4559
|
if (!by) {
|
|
4507
4560
|
store.close();
|
|
4508
|
-
return fail(`--by decision "${opts.by}" not found`);
|
|
4561
|
+
return fail(`--by decision "${opts.by}" not found in the ${home} store that holds "${oldId}"`);
|
|
4509
4562
|
}
|
|
4510
|
-
const closed = store.supersede(oldId, by);
|
|
4563
|
+
const closed = home === "private" ? store.supersedePrivate(oldId, by) : store.supersede(oldId, by);
|
|
4511
4564
|
if (!closed) {
|
|
4512
4565
|
store.close();
|
|
4513
|
-
return fail(`decision "${oldId}"
|
|
4566
|
+
return fail(`decision "${oldId}" cannot supersede itself`);
|
|
4514
4567
|
}
|
|
4515
4568
|
store.reindex();
|
|
4516
|
-
pumpMemoryHome(store, root,
|
|
4569
|
+
pumpMemoryHome(store, root, home, `hunch: supersede ${oldId} by ${opts.by}`);
|
|
4517
4570
|
console.log(`✓ ${oldId} superseded by ${opts.by} — window closed at ${closed.valid_to?.slice(0, 10)}.`);
|
|
4518
4571
|
store.close();
|
|
4519
4572
|
});
|
|
@@ -4757,10 +4810,12 @@ program
|
|
|
4757
4810
|
if (/^(Edit|Write|MultiEdit)$/.test(evt.tool_name ?? "")) {
|
|
4758
4811
|
// A Codex apply_patch touches every file it lists (and each Move-to
|
|
4759
4812
|
// destination); the Stop gate must see all of them, not only the first.
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4813
|
+
// Same in-repo filter as the pre-edit path: a scratch file outside the
|
|
4814
|
+
// repository is not a product edit, and recording it would block Stop on
|
|
4815
|
+
// a path no check can ever verify.
|
|
4816
|
+
const edited = editTargets(root, evt.tool_input);
|
|
4817
|
+
for (const e of edited)
|
|
4818
|
+
st = onEdit(st, e.target);
|
|
4764
4819
|
if (edited.length)
|
|
4765
4820
|
activity = { kind: "edit" };
|
|
4766
4821
|
}
|
|
@@ -5170,22 +5225,23 @@ program
|
|
|
5170
5225
|
docGround;
|
|
5171
5226
|
if (!hasContent)
|
|
5172
5227
|
return; // no noise on files Hunch hasn't learned yet
|
|
5228
|
+
const supplements = [
|
|
5229
|
+
...(retired.length ? [{
|
|
5230
|
+
id: "retired-code",
|
|
5231
|
+
kind: "retired-code",
|
|
5232
|
+
priority: 200,
|
|
5233
|
+
text: `⚠ Deliberately RETIRED from this file — do not re-introduce without cause: ${retired.map((r) => `${[...r.symbols, ...r.deps].join(", ")} (${r.decision})`).join("; ")}.`,
|
|
5234
|
+
}] : []),
|
|
5235
|
+
...(docGround ? [{ id: "doc-grounding", kind: "doc-grounding", priority: 100, text: docGround }] : []),
|
|
5236
|
+
...recentTasks,
|
|
5237
|
+
];
|
|
5173
5238
|
const envelope = buildDeliveryEnvelope(ctx, {
|
|
5174
5239
|
profile: "builder",
|
|
5175
5240
|
root,
|
|
5176
5241
|
symbols: store.recs("symbols"),
|
|
5177
5242
|
components: store.recs("components"),
|
|
5178
5243
|
decisionCorpus: store.recs("decisions"),
|
|
5179
|
-
supplements
|
|
5180
|
-
...(retired.length ? [{
|
|
5181
|
-
id: "retired-code",
|
|
5182
|
-
kind: "retired-code",
|
|
5183
|
-
priority: 200,
|
|
5184
|
-
text: `⚠ Deliberately RETIRED from this file — do not re-introduce without cause: ${retired.map((r) => `${[...r.symbols, ...r.deps].join(", ")} (${r.decision})`).join("; ")}.`,
|
|
5185
|
-
}] : []),
|
|
5186
|
-
...(docGround ? [{ id: "doc-grounding", kind: "doc-grounding", priority: 100, text: docGround }] : []),
|
|
5187
|
-
...recentTasks,
|
|
5188
|
-
],
|
|
5244
|
+
supplements,
|
|
5189
5245
|
});
|
|
5190
5246
|
const text = envelope.text.trim();
|
|
5191
5247
|
// Identical grounding already shown this session → one-line delta instead of
|
|
@@ -5218,7 +5274,16 @@ program
|
|
|
5218
5274
|
const reportTaskId = hookReportTaskId(root, provider, evt);
|
|
5219
5275
|
// A new authoritative prompt gets its own full delivery. An earlier
|
|
5220
5276
|
// prompt's session-level delta cannot establish this task's receipt.
|
|
5221
|
-
|
|
5277
|
+
// A subagent reports to the prompt's task but starts with FRESH context:
|
|
5278
|
+
// it never saw that grounding, so its dedup is scoped by its own agent
|
|
5279
|
+
// identity (hashed — the raw agent_id is never retained in the key).
|
|
5280
|
+
const agentKey = evt.agent_id ? `:${reportHash(evt.agent_id).slice(7, 19)}` : "";
|
|
5281
|
+
// Dedup on the envelope's stable IDENTITY projection, never on the
|
|
5282
|
+
// rendered block: serving the full text writes delivery receipts, and the
|
|
5283
|
+
// next call's task ranking reads them back and moves the wording ("today"
|
|
5284
|
+
// → "delivered today"), so hashing the presentation made this grounding
|
|
5285
|
+
// self-invalidating and re-sent the full block for unchanged records.
|
|
5286
|
+
if (injectionMode(evt.session_id, `pre:${target}${reportTaskId ? `:${reportTaskId}` : ""}${agentKey}`, text, deliveryDedupeInput(envelope, supplements)) === "delta") {
|
|
5222
5287
|
receipts("refreshed");
|
|
5223
5288
|
emitContext(provider, "PreToolUse", `Hunch grounding for ${target}: unchanged this session (${envelope.delivered.filter((item) => item.kind === "decisions").length} decision(s), ${envelope.delivered.filter((item) => item.kind === "constraints").length} invariant(s) shown earlier — still current; hunch_why("${target}") to re-expand).`);
|
|
5224
5289
|
return;
|
|
@@ -7080,9 +7145,23 @@ program
|
|
|
7080
7145
|
// hook manager's regenerated file — issue #311) is NOT installed.
|
|
7081
7146
|
const unreachable = [["post-commit", report.postCommit], ["post-merge", report.postMerge], ["pre-commit", report.preCommit], ["post-checkout", report.postCheckout]]
|
|
7082
7147
|
.filter(([, e]) => e.state === "unreachable");
|
|
7148
|
+
// Present where git runs it, but its command is not a Hunch launcher that
|
|
7149
|
+
// exists (hand-edited, or an absolute path gone after a reinstall) — issue #315.
|
|
7150
|
+
const stale = [["post-commit", report.postCommit], ["post-merge", report.postMerge], ["pre-commit", report.preCommit], ["post-checkout", report.postCheckout]]
|
|
7151
|
+
.filter(([, e]) => e.state === "stale");
|
|
7152
|
+
// A bare `hunch init` regenerates post-commit without --private/--commit
|
|
7153
|
+
// and pre-commit without --strict, so pointing at it without naming the
|
|
7154
|
+
// options that keep them silently DOWNGRADES the install. --commit is left
|
|
7155
|
+
// out: init's auto-commit is ON unless --no-auto-commit is passed, so a
|
|
7156
|
+
// bare re-run keeps it and naming it here would claim a loss that never happens.
|
|
7157
|
+
const carried = [...new Set(stale.flatMap(([, e]) => e.flags ?? []))].filter((f) => f !== "--commit");
|
|
7158
|
+
const keep = carried.map((f) => (f === "--strict" ? "--enforce-strict" : f === "--private" ? "--private-sync (or --shared-sync)" : "")).filter(Boolean);
|
|
7159
|
+
const staleKeep = carried.length
|
|
7160
|
+
? ` — the stale block carried ${carried.join(", ")}${keep.length ? `: pass ${keep.join(", ")} or they are dropped` : ""}`
|
|
7161
|
+
: "";
|
|
7083
7162
|
// `hunch index` only ever installs post-merge onto an existing
|
|
7084
|
-
// post-commit install (it never hooks an un-hooked repo — see
|
|
7085
|
-
//
|
|
7163
|
+
// post-commit install (it never hooks an un-hooked repo — see the
|
|
7164
|
+
// hookReport(root).postCommit.state gate above) and never writes
|
|
7086
7165
|
// into a manager-owned hook — so the fix hint must not point there when
|
|
7087
7166
|
// post-commit itself is missing or a hook manager is in play.
|
|
7088
7167
|
const managedMissing = [report.postCommit, report.postMerge].find((e) => e.state === "missing" && e.manager !== "none");
|
|
@@ -7092,6 +7171,7 @@ program
|
|
|
7092
7171
|
const problems = [
|
|
7093
7172
|
missing.length ? `⚠ missing ${missing.join(", ")} — ${fix}` : "",
|
|
7094
7173
|
unreachable.length ? `⚠ installed but unreachable: ${unreachable.map(([n]) => n).join(", ")} — never runs; run \`hunch init\` to print the snippet for your hook manager` : "",
|
|
7174
|
+
stale.length ? `⚠ stale: ${stale.map(([n]) => n).join(", ")} — the hook's command is not a working Hunch launcher; run \`hunch init\` to regenerate it${staleKeep}` : "",
|
|
7095
7175
|
].filter(Boolean);
|
|
7096
7176
|
console.log(`hooks: ${problems.length
|
|
7097
7177
|
? problems.join("\n ")
|
|
@@ -7099,6 +7179,13 @@ program
|
|
|
7099
7179
|
for (const [name, e] of unreachable) {
|
|
7100
7180
|
console.log(dim(` ↳ ${name}: ${e.reason ?? "the block sits where git never reaches it"} (${rel(root, e.path)})`));
|
|
7101
7181
|
}
|
|
7182
|
+
for (const [name, e] of stale) {
|
|
7183
|
+
console.log(dim(` ↳ ${name}: ${e.reason ?? "the block's command is not a Hunch launcher"} (${rel(root, e.path)})`));
|
|
7184
|
+
}
|
|
7185
|
+
// Where the live hooks point — informational (a hook may legitimately run
|
|
7186
|
+
// another install), but the fastest way to spot a stale/foreign launcher.
|
|
7187
|
+
for (const line of hookInvocationLines(report, resolveInvocation().shell))
|
|
7188
|
+
console.log(dim(` ↳ ${line}`));
|
|
7102
7189
|
// Workspace ledger: what this machine is called, whether its record is in memory,
|
|
7103
7190
|
// and whether the checkout hook that keeps it fresh is installed.
|
|
7104
7191
|
try {
|
|
@@ -7107,7 +7194,7 @@ program
|
|
|
7107
7194
|
const others = store.recs("workspaces").filter((r) => r.machine.id !== machine.id).length;
|
|
7108
7195
|
const leak = labelLeaksIdentity(machine.label);
|
|
7109
7196
|
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)` +
|
|
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)" : ""}`);
|
|
7197
|
+
`${hooks.postCheckout ? " · post-checkout hook installed" : report.postCheckout.state === "unreachable" ? " · ⚠ post-checkout hook unreachable" : report.postCheckout.state === "stale" ? " · ⚠ post-checkout hook stale" : hooks.postCommit ? " · post-checkout hook not installed (`hunch index` adds it)" : ""}`);
|
|
7111
7198
|
}
|
|
7112
7199
|
catch { /* no machine file writable: nothing to report */ }
|
|
7113
7200
|
}
|
|
@@ -7133,6 +7220,20 @@ program
|
|
|
7133
7220
|
const ctxWarning = await maybeWarnOllamaContext(provider.name, process.env);
|
|
7134
7221
|
if (ctxWarning)
|
|
7135
7222
|
console.log(ctxWarning);
|
|
7223
|
+
// The native addons load on first PARSE, not at import, so no other command
|
|
7224
|
+
// that merely starts up proves the parser works. Doctor is the one place
|
|
7225
|
+
// that should pay the ~1.5s load: without this line a broken parser (an
|
|
7226
|
+
// unwritable TMPDIR, a missing or wrong-arch prebuild, an addon preloaded
|
|
7227
|
+
// past the isolation guard) is invisible until an index run refuses.
|
|
7228
|
+
try {
|
|
7229
|
+
loadNativeTreeSitter();
|
|
7230
|
+
console.log(`parser: native tree-sitter addons load`);
|
|
7231
|
+
}
|
|
7232
|
+
catch (e) {
|
|
7233
|
+
console.log(`parser: ⛔ ${e.message}`);
|
|
7234
|
+
console.log(dim(` no file can be parsed — \`hunch index\` refuses rather than emptying the graph`));
|
|
7235
|
+
process.exitCode = 1;
|
|
7236
|
+
}
|
|
7136
7237
|
const c = store.reindex().counts;
|
|
7137
7238
|
console.log(`hunch: ${c.symbols} symbols, ${c.edges} edges, ${c.components} components, ${c.decisions} decisions, ${c.bugs} bugs, ${c.constraints} constraints`);
|
|
7138
7239
|
try {
|
|
@@ -7252,28 +7353,17 @@ function readStdin() {
|
|
|
7252
7353
|
});
|
|
7253
7354
|
}
|
|
7254
7355
|
/** Absolute edit path → repo-relative, forward-slash (constraint scopes are
|
|
7255
|
-
* forward-slash globs even on Windows)
|
|
7256
|
-
|
|
7257
|
-
*
|
|
7258
|
-
*
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
catch {
|
|
7264
|
-
const parent = dirname(p);
|
|
7265
|
-
if (parent === p)
|
|
7266
|
-
return p; // hit the root; nothing more to resolve
|
|
7267
|
-
return join(realpathNorm(parent), basename(p));
|
|
7268
|
-
}
|
|
7269
|
-
}
|
|
7270
|
-
/** Repo-relative POSIX path. BOTH ends are realpath-normalized first: on macOS
|
|
7271
|
-
* `process.cwd()` (hence findRoot) resolves /var→/private/var, but a hook event's
|
|
7272
|
-
* file_path arrives UN-resolved — so a naive relative() yields a bogus "../" path
|
|
7273
|
-
* under any symlinked root (/var, /tmp, symlinked $HOME) and the caller treats the
|
|
7274
|
-
* file as outside the repo, silently dropping all context (dec_e0a36efbf5). */
|
|
7356
|
+
* forward-slash globs even on Windows), or "" when `abs` isn't (resolvably)
|
|
7357
|
+
* inside `root`. Thin adapter over the shared `repoRelativeTarget` (core/paths.ts,
|
|
7358
|
+
* which realpath-normalizes both ends first — see its docstring for why, incl.
|
|
7359
|
+
* dec_e0a36efbf5): that function passes an unresolvable absolute path through
|
|
7360
|
+
* UNCHANGED (still absolute) rather than signaling failure directly, since other
|
|
7361
|
+
* callers want the original target back to fail their own match safely. This
|
|
7362
|
+
* adapter converts that "still absolute" signal to "" — the sentinel `onEdit`
|
|
7363
|
+
* and `editTargets` below already treat as "skip, not in this repo". */
|
|
7275
7364
|
function toRepoRel(root, abs) {
|
|
7276
|
-
|
|
7365
|
+
const rel = repoRelativeTarget(abs, root);
|
|
7366
|
+
return isAbsolute(rel) || /^[a-zA-Z]:/.test(rel) ? "" : rel;
|
|
7277
7367
|
}
|
|
7278
7368
|
/** The in-repo files a pre-edit event would change, each with the lines the edit
|
|
7279
7369
|
* ADDS to that file. A Codex apply_patch yields one entry per touched path (a
|
package/dist/cli/serve.d.ts
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
+
/** Compact one served partition's ledger. Compaction is read → slice → write, so it takes the
|
|
3
|
+
* partition write lock like every other writer (#286): without it a live server's append between
|
|
4
|
+
* the read and the write is overwritten, regressing head_seq and reusing a seq. The ledger lives
|
|
5
|
+
* in the store's RESOLVED state home (the overlay in shared mode), never `<root>/.hunch` blindly. */
|
|
6
|
+
export declare function compactPartition(root: string, scope: {
|
|
7
|
+
kind: "organization" | "team" | "user" | "repository";
|
|
8
|
+
id: string;
|
|
9
|
+
}, keep: number, lockOptions?: {
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
dropped: number;
|
|
13
|
+
floor_seq: number;
|
|
14
|
+
head_seq: number;
|
|
15
|
+
}>;
|
|
2
16
|
export declare function registerServeCommands(program: Command): void;
|
package/dist/cli/serve.js
CHANGED
|
@@ -6,9 +6,9 @@ import { initServeConfig, partitionFor, readServeConfig, writeServeConfig } from
|
|
|
6
6
|
import { compactLedger } from "../store/changeLedger.js";
|
|
7
7
|
import { HunchStore } from "../store/hunchStore.js";
|
|
8
8
|
import { hunchPaths } from "../core/paths.js";
|
|
9
|
-
import { partitionOf } from "../store/stateBinding.js";
|
|
9
|
+
import { partitionOf, stateHomeFor } from "../store/stateBinding.js";
|
|
10
10
|
import { formatReplayReport, verifyReplay } from "../store/replay.js";
|
|
11
|
-
import {
|
|
11
|
+
import { withWriteLock } from "../serve/writelock.js";
|
|
12
12
|
import { ScopeSchema, scopePath } from "../core/stateContract.js";
|
|
13
13
|
import { HUNCH_VERSION } from "../core/version.js";
|
|
14
14
|
function parseScopeArg(value) {
|
|
@@ -18,6 +18,20 @@ function parseScopeArg(value) {
|
|
|
18
18
|
throw new Error(`partition must be kind:id (organization|team|user|repository), got "${value}"`);
|
|
19
19
|
return parsed.data;
|
|
20
20
|
}
|
|
21
|
+
/** Compact one served partition's ledger. Compaction is read → slice → write, so it takes the
|
|
22
|
+
* partition write lock like every other writer (#286): without it a live server's append between
|
|
23
|
+
* the read and the write is overwritten, regressing head_seq and reusing a seq. The ledger lives
|
|
24
|
+
* in the store's RESOLVED state home (the overlay in shared mode), never `<root>/.hunch` blindly. */
|
|
25
|
+
export async function compactPartition(root, scope, keep, lockOptions = {}) {
|
|
26
|
+
const store = new HunchStore(hunchPaths(root));
|
|
27
|
+
try {
|
|
28
|
+
const { hunchDir } = stateHomeFor(store, scope);
|
|
29
|
+
return await withWriteLock(hunchDir, () => compactLedger(hunchDir, scope, { keep }), lockOptions);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
store.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
21
35
|
export function registerServeCommands(program) {
|
|
22
36
|
const DEFAULT_CONFIG = "hunch-serve.json";
|
|
23
37
|
const serve = program.command("serve")
|
|
@@ -57,7 +71,7 @@ export function registerServeCommands(program) {
|
|
|
57
71
|
.requiredOption("--partition <kind:id>", "the partition whose ledger to compact")
|
|
58
72
|
.option("--keep <n>", "events to keep", "1000")
|
|
59
73
|
.option("--json", "machine-readable output")
|
|
60
|
-
.action((opts) => {
|
|
74
|
+
.action(async (opts) => {
|
|
61
75
|
const parent = serve.opts();
|
|
62
76
|
const config = readServeConfig(resolve(parent.config ?? DEFAULT_CONFIG));
|
|
63
77
|
const scope = parseScopeArg(opts.partition);
|
|
@@ -67,7 +81,7 @@ export function registerServeCommands(program) {
|
|
|
67
81
|
const keep = Number(opts.keep);
|
|
68
82
|
if (!Number.isInteger(keep) || keep < 0)
|
|
69
83
|
throw new Error("--keep must be a non-negative integer");
|
|
70
|
-
const result =
|
|
84
|
+
const result = await compactPartition(partition.root, scope, keep);
|
|
71
85
|
if (opts.json) {
|
|
72
86
|
console.log(JSON.stringify({ partition: scopePath(scope), ...result }));
|
|
73
87
|
return;
|
|
@@ -25,7 +25,11 @@ const DIRECT_TEST_DELTA_LIMITATIONS = [
|
|
|
25
25
|
"Direct decision candidates cover newly named node:test test()/it() cases and existing literal-named cases whose body contains an added fixing-commit line.",
|
|
26
26
|
...LIMITATIONS.slice(1),
|
|
27
27
|
];
|
|
28
|
-
|
|
28
|
+
/** Resolved on first parse, not at import: loading the native addons copies six
|
|
29
|
+
* `.node` files into a temp dir and dlopens them (~1.5s+ cold), which every CLI
|
|
30
|
+
* command and every editor hook would otherwise pay just for importing this
|
|
31
|
+
* module. loadNativeTreeSitter() memoizes the runtime itself. */
|
|
32
|
+
const treeSitter = () => loadNativeTreeSitter();
|
|
29
33
|
function safeTestFile(file) {
|
|
30
34
|
return !!file
|
|
31
35
|
&& !isAbsolute(file)
|
|
@@ -175,6 +179,7 @@ function literalTestName(raw) {
|
|
|
175
179
|
return name.trim() ? name : null;
|
|
176
180
|
}
|
|
177
181
|
function literalNodeTestCases(file, source) {
|
|
182
|
+
const { Parser, typescript: tsLanguage, tsx: tsxLanguage } = treeSitter();
|
|
178
183
|
const language = /\.[cm]?[jt]sx$/.test(file) && /x$/.test(file) ? tsxLanguage : tsLanguage;
|
|
179
184
|
const parser = new Parser();
|
|
180
185
|
parser.setLanguage(language);
|
|
@@ -5,6 +5,7 @@ import { shortHash } from "../core/ids.js";
|
|
|
5
5
|
import { hunchPathsForDir } from "../core/paths.js";
|
|
6
6
|
import { commitMeta, fixCommits } from "../extractors/git.js";
|
|
7
7
|
import { indexRepo } from "../extractors/indexer.js";
|
|
8
|
+
import { isParserLoadError } from "../extractors/nativeTreeSitter.js";
|
|
8
9
|
import { HunchStore } from "../store/hunchStore.js";
|
|
9
10
|
import { canonicalHash } from "./canonical.js";
|
|
10
11
|
import { extractStructuralDelta } from "./delta.js";
|
|
@@ -62,10 +63,15 @@ function privateGrounding(decisionStore, graphStore, root) {
|
|
|
62
63
|
});
|
|
63
64
|
byCommit.set(meta.sha, list);
|
|
64
65
|
}
|
|
65
|
-
catch {
|
|
66
|
+
catch (error) {
|
|
66
67
|
// A human decision can be real while still lacking a supported structural
|
|
67
68
|
// binding. It contributes no candidate attestation rather than being
|
|
68
69
|
// stretched over a coincidental fact from the same commit.
|
|
70
|
+
// A dead native parser is not that: it makes EVERY decision look
|
|
71
|
+
// unbindable, so the review packet would silently downgrade exact human
|
|
72
|
+
// grounding to unattested coincidence. Fail instead of under-attesting.
|
|
73
|
+
if (isParserLoadError(error))
|
|
74
|
+
throw error;
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
return byCommit;
|
|
@@ -142,6 +148,10 @@ function buildFromIndexedGraph(decisionStore, graphStore, root, opts, resolution
|
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
catch (error) {
|
|
151
|
+
// A dead native parser would turn every commit into a per-commit failure
|
|
152
|
+
// entry instead of an honest abort, so it is never recorded as one.
|
|
153
|
+
if (isParserLoadError(error))
|
|
154
|
+
throw error;
|
|
145
155
|
failures.push({ commit, error: error.message });
|
|
146
156
|
}
|
|
147
157
|
}
|
|
@@ -5,6 +5,7 @@ import { pathMatchesGlob } from "../core/glob.js";
|
|
|
5
5
|
import { shortHash } from "../core/ids.js";
|
|
6
6
|
import { resolveRelativeImport } from "../core/relativeImports.js";
|
|
7
7
|
import { commitMeta } from "../extractors/git.js";
|
|
8
|
+
import { isParserLoadError } from "../extractors/nativeTreeSitter.js";
|
|
8
9
|
import { canonicalHash } from "./canonical.js";
|
|
9
10
|
import { clampCandidateLimit, durationCutoff } from "./bootstrap.js";
|
|
10
11
|
import { compileStructuralPolicy } from "./compiler.js";
|
|
@@ -645,6 +646,15 @@ export function bootstrapStructuralPolicies(store, root, repository, opts = {})
|
|
|
645
646
|
report.compiled.push({ evidence: event, policy });
|
|
646
647
|
}
|
|
647
648
|
catch (e) {
|
|
649
|
+
// "uncompilable" is a verdict ABOUT the decision, and it is written to
|
|
650
|
+
// evidence. A dead native parser (extractStructuralDelta parses the
|
|
651
|
+
// commit's sources) says nothing about the decision — recording it would
|
|
652
|
+
// stamp every eligible decision uncompilable with a TMPDIR error as the
|
|
653
|
+
// stated reason, and a later run with a working parser would only
|
|
654
|
+
// reclassify what `canReclassify` still allows. Fail the bootstrap
|
|
655
|
+
// instead, the way the import-time load did.
|
|
656
|
+
if (isParserLoadError(e))
|
|
657
|
+
throw e;
|
|
648
658
|
report.uncompilable++;
|
|
649
659
|
const reason = e.message;
|
|
650
660
|
if (canReclassify(prior) && (prior?.compiler?.status !== "uncompilable" || prior.compiler.reason !== reason)) {
|
package/dist/core/correction.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* - buildCorrectionConstraint(): mint the Constraint record (human-confirmed,
|
|
9
9
|
* scoped conservatively) that the pre-edit hook + CI guard then enforce.
|
|
10
10
|
*/
|
|
11
|
-
import { isAbsolute
|
|
11
|
+
import { isAbsolute } from "node:path";
|
|
12
12
|
import { constraintId } from "./ids.js";
|
|
13
|
-
import { toPosixTarget } from "./paths.js";
|
|
13
|
+
import { toPosixTarget, repoRelativeTarget } from "./paths.js";
|
|
14
14
|
import { deriveForbids } from "./constraintmatch.js";
|
|
15
15
|
/** Correction cues. Deliberately conservative — anchored to imperative/rebuke
|
|
16
16
|
* phrasing, not bare "no", so ordinary conversational negation ("no idea",
|
|
@@ -57,7 +57,13 @@ export const TESTIMONY_CORRECTION_RATIONALE = "Recorded by the agent as a correc
|
|
|
57
57
|
* Returns "" when the hint cannot be made repo-relative (no root, or a path outside the
|
|
58
58
|
* repo). The caller then falls back to "**", where the existing severity guard
|
|
59
59
|
* down-ranks a non-explicit blocking rule to a warning — fail-safe and honest, rather
|
|
60
|
-
* than a blocking rule enforced nowhere.
|
|
60
|
+
* than a blocking rule enforced nowhere.
|
|
61
|
+
*
|
|
62
|
+
* Thin adapter over the shared `repoRelativeTarget` (core/paths.ts): that function
|
|
63
|
+
* passes an unresolvable absolute hint through UNCHANGED (still absolute) rather
|
|
64
|
+
* than signaling failure directly, since other callers (checkConstraints, why())
|
|
65
|
+
* want the original target back to fail their own match safely. This adapter
|
|
66
|
+
* converts that "still absolute" signal to "" — the fail-safe this caller needs. */
|
|
61
67
|
function repoRelativeHint(rawHint, root) {
|
|
62
68
|
if (!rawHint)
|
|
63
69
|
return "";
|
|
@@ -66,10 +72,9 @@ function repoRelativeHint(rawHint, root) {
|
|
|
66
72
|
return rawHint;
|
|
67
73
|
if (!root)
|
|
68
74
|
return "";
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return rel;
|
|
75
|
+
const rewritten = repoRelativeTarget(rawHint, root);
|
|
76
|
+
const stillAbsolute = isAbsolute(rewritten) || /^[a-zA-Z]:/.test(rewritten);
|
|
77
|
+
return stillAbsolute ? "" : rewritten;
|
|
73
78
|
}
|
|
74
79
|
/**
|
|
75
80
|
* Build the Constraint a correction mints. Pure (caller passes `now`), so the
|