@davesheffer/hunch 1.38.1 → 1.39.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 +355 -55
- package/dist/cli/integrations.js +10 -0
- package/dist/cli/serve.js +1 -0
- package/dist/client/readOrCompute.d.ts +77 -0
- package/dist/client/readOrCompute.js +85 -0
- package/dist/client/state.d.ts +1 -0
- package/dist/client/state.js +1 -0
- package/dist/constitution/g2.d.ts +1 -0
- package/dist/constitution/service.js +8 -0
- package/dist/constitution/sourceMutation.js +23 -18
- package/dist/core/agenthook.d.ts +14 -0
- package/dist/core/agenthook.js +48 -5
- package/dist/core/changeProof.js +5 -1
- package/dist/core/checkreport.d.ts +7 -0
- package/dist/core/checkreport.js +20 -3
- package/dist/core/compare.js +3 -2
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +13 -0
- package/dist/core/machine.d.ts +20 -0
- package/dist/core/machine.js +101 -0
- package/dist/core/taskReportEvidence.js +6 -6
- package/dist/core/types.d.ts +67 -1
- package/dist/core/types.js +3 -0
- package/dist/core/workspace.d.ts +256 -0
- package/dist/core/workspace.js +359 -0
- package/dist/extractors/diff.d.ts +34 -0
- package/dist/extractors/diff.js +147 -5
- package/dist/extractors/git.d.ts +40 -11
- package/dist/extractors/git.js +147 -43
- package/dist/extractors/helm.d.ts +17 -28
- package/dist/extractors/helm.js +12 -12
- package/dist/extractors/indexer.js +171 -7
- package/dist/extractors/k8sManifest.d.ts +59 -0
- package/dist/extractors/k8sManifest.js +507 -0
- package/dist/extractors/workspaces.d.ts +28 -0
- package/dist/extractors/workspaces.js +427 -0
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.d.ts +27 -2
- package/dist/integrations/gitignore.js +103 -17
- package/dist/integrations/hooks.d.ts +63 -7
- package/dist/integrations/hooks.js +350 -38
- package/dist/integrations/scaffold.js +11 -0
- package/dist/integrations/workspaceLedger.d.ts +93 -0
- package/dist/integrations/workspaceLedger.js +307 -0
- package/dist/mcp/server.js +59 -5
- package/dist/serve/app.d.ts +2 -0
- package/dist/serve/app.js +107 -92
- package/dist/serve/mcpHttp.d.ts +27 -0
- package/dist/serve/mcpHttp.js +95 -0
- package/dist/store/hunchStore.d.ts +4 -2
- package/dist/store/hunchStore.js +23 -6
- package/dist/store/stateBinding.js +83 -35
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import { assertCompleteRepoScan, indexRepo, scanRepo } from "../extractors/index
|
|
|
40
40
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
41
41
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
42
42
|
import { readSynthesisPreference, resolveSynthesisProvider, selectProvider, SYNTH_PREFERENCES, writeSynthesisPreference, normalizeProviderName, } from "../synthesis/provider.js";
|
|
43
|
-
import { isGitRepo, isGitRepoRoot, sameGitPublication, sameRemoteUrl, canonicalRemoteUrl, repositoryUsesRemote, headSha, isolatedHeadSha, logSince, lastChangeDate, firstCommitForFile, stagedFiles, workingFiles, commitFiles, asOfDate,
|
|
43
|
+
import { isGitRepo, isGitRepoRoot, sameGitPublication, sameRemoteUrl, canonicalRemoteUrl, repositoryUsesRemote, headSha, isolatedHeadSha, logSince, lastChangeDate, firstCommitForFile, stagedFiles, workingFiles, commitFiles, asOfDate, stagedGateDiff, workingGateDiff, commitGateDiff, rangeFiles, rangeGateDiff, rangeSubjects, revExists, revParse, commitAndPushHunch, pullHunchStatus, syncExistingHunch, gitUntrackCached, gitCommonDir, hooksDir, isLinkedWorktree, mainWorktreeRoot, gitMemoryLog, memoryMoveDiff, revertMemoryMove, pushCurrentBranch, commitChanges, commitRepairStatus, mergeRangeChanges, commitsExist } from "../extractors/git.js";
|
|
44
44
|
import { parseMemoryLog } from "../core/memorylog.js";
|
|
45
45
|
import { renamesOf, planRepair, repairDecision, repairConstraint } from "../core/repair.js";
|
|
46
46
|
import { orphanedCommitDecisions, planCommitRepair, repairDecisionCommit, pickRewrite, commitRepairReviewHash, mergeRewrites, firstFor, deadRewrites, resolvedRewriteIds, withoutDropped, addDropped, withheldForUnresolvableTo } from "../core/commitrepair.js";
|
|
@@ -52,11 +52,11 @@ import { deriveForbids, effectiveForbids } from "../core/constraintmatch.js";
|
|
|
52
52
|
import { extractInlineIntent } from "../extractors/comments.js";
|
|
53
53
|
import { renderText, renderMarkdown, renderSarif, renderImpact, reportFailsStrict } from "../core/checkreport.js";
|
|
54
54
|
import { partitionReview, isReviewDraft, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
|
|
55
|
-
import { installPostCommitHook, installPreCommitHook, installPostMergeHook, hookStatus } from "../integrations/hooks.js";
|
|
55
|
+
import { installPostCommitHook, installPreCommitHook, installPostMergeHook, installPostCheckoutHook, hookStatus, hookReport, formatHookInstall } from "../integrations/hooks.js";
|
|
56
56
|
import { ensureSharedOverlayPointer } from "../integrations/worktree.js";
|
|
57
57
|
import { flushCapture, flushMemoryHome, flushMemoryHomes, pinSharedRemote, sharedRemoteFor } from "../integrations/sync.js";
|
|
58
58
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
59
|
-
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS } from "../integrations/gitignore.js";
|
|
59
|
+
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS, upgradeManagedGitignore, describeGitignoreUpgrade } from "../integrations/gitignore.js";
|
|
60
60
|
import { writeCiWorkflow } from "../integrations/ciAction.js";
|
|
61
61
|
import { updateClaudeMd, renderHunchSection } from "../integrations/claudemd.js";
|
|
62
62
|
import { classifyGroundingBlock, describeGroundingFreshness } from "../core/groundingLag.js";
|
|
@@ -78,7 +78,10 @@ import { deriveChangeProof } from "../core/changeProof.js";
|
|
|
78
78
|
import { discoverProjectDna, evaluateProjectDnaMatch } from "../core/projectDna.js";
|
|
79
79
|
import { diffProjectDna } from "../core/projectDnaDelta.js";
|
|
80
80
|
import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
|
|
81
|
-
import { readConfig, writeConfig, FIRMNESS_LEVELS, isFirmness } from "../core/config.js";
|
|
81
|
+
import { readConfig, writeConfig, FIRMNESS_LEVELS, isFirmness, workspacesConfig } from "../core/config.js";
|
|
82
|
+
import { loadOrCreateMachine, setMachineLabel, machineFile, labelLeaksIdentity } from "../core/machine.js";
|
|
83
|
+
import { worktreeRows, branchRows } from "../core/workspace.js";
|
|
84
|
+
import { workspaceLedgerView, recordWorkspaceSnapshot, renderWorktreeTable, renderBranchTable, workspaceSummaryLine, prunePlanFor, renderPrunePlan, applyPrune, confirmPrune, pruneConfirmQuestion } from "../integrations/workspaceLedger.js";
|
|
82
85
|
import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpolicy.js";
|
|
83
86
|
import { isHumanConfirmed } from "../core/strictgate.js";
|
|
84
87
|
import { appendEvent, readEvents } from "../core/events.js";
|
|
@@ -338,6 +341,10 @@ program
|
|
|
338
341
|
const gi = ensureGitignore(root);
|
|
339
342
|
if (gi.action !== "unchanged")
|
|
340
343
|
console.log(` ✓ .gitignore ${gi.action} (Hunch runtime index excluded)`);
|
|
344
|
+
// A repo migrated to a private overlay by an older release: bring its private-only
|
|
345
|
+
// block up to date (and stop publishing newly ignored memory dirs).
|
|
346
|
+
for (const line of describeGitignoreUpgrade(upgradeManagedGitignore(root)))
|
|
347
|
+
console.log(` ✓ ${line}`);
|
|
341
348
|
if (opts.index !== false) {
|
|
342
349
|
if (isGitRepo(root) && revExists("HEAD", root)) {
|
|
343
350
|
const res = indexRepo(store, root, { source: { kind: "commit", ref: "HEAD" } });
|
|
@@ -390,9 +397,14 @@ program
|
|
|
390
397
|
if (isGitRepo(root)) {
|
|
391
398
|
const syncToOverlay = !!(opts.privateSync || opts.sharedSync);
|
|
392
399
|
const h = installPostCommitHook(root, inv.shell, { private: syncToOverlay, commit: opts.autoCommit, localOnly: syncToOverlay });
|
|
393
|
-
|
|
400
|
+
for (const line of formatHookInstall(root, "post-commit hook", h, ` (learning loop)${syncToOverlay ? " — syncs to the shared overlay" : ""}${opts.autoCommit ? " — auto-commit on" : ""}`))
|
|
401
|
+
console.log(line);
|
|
394
402
|
const pm = installPostMergeHook(root, inv.shell);
|
|
395
|
-
|
|
403
|
+
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)"))
|
|
404
|
+
console.log(line);
|
|
405
|
+
const pc = installPostCheckoutHook(root, inv.shell);
|
|
406
|
+
for (const line of formatHookInstall(root, "post-checkout hook", pc, " (workspace ledger: records this machine's branches + worktrees on checkout)"))
|
|
407
|
+
console.log(line);
|
|
396
408
|
const m = installMergeDriver(root, inv.shell);
|
|
397
409
|
console.log(` ✓ team merge driver ${m.action}`);
|
|
398
410
|
// Auto-install the pre-commit guard by default (advisory: flags invariants
|
|
@@ -401,7 +413,8 @@ program
|
|
|
401
413
|
if (opts.enforce !== false || opts.enforceStrict) {
|
|
402
414
|
const strict = !!opts.enforceStrict;
|
|
403
415
|
const p = installPreCommitHook(root, inv.shell, strict);
|
|
404
|
-
|
|
416
|
+
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"})`))
|
|
417
|
+
console.log(line);
|
|
405
418
|
}
|
|
406
419
|
}
|
|
407
420
|
else {
|
|
@@ -473,7 +486,10 @@ program
|
|
|
473
486
|
.action((opts) => {
|
|
474
487
|
const { store, root } = storeFor();
|
|
475
488
|
store.json.ensureDirs();
|
|
476
|
-
|
|
489
|
+
// Keep the derived SQLite index out of git (idempotent). Adds a missing block but
|
|
490
|
+
// never rewrites an existing one: `index` runs in CI/release gates on a clean
|
|
491
|
+
// checkout. Older blocks are upgraded by `hunch update` / init / private setup.
|
|
492
|
+
ensureGitignore(root, { upgradeExisting: false });
|
|
477
493
|
// The post-merge hook only ever got installed by `hunch init`/`hunch
|
|
478
494
|
// private`/`hunch shared` — a repo that already ran init before this hook
|
|
479
495
|
// existed never receives it. `hunch index` already self-heals gitignore
|
|
@@ -481,8 +497,11 @@ program
|
|
|
481
497
|
// re-running init by hand. Gated on already having post-commit: `index`
|
|
482
498
|
// is not a setup command (it runs in CI, on any git repo), so it must
|
|
483
499
|
// never be what FIRST hooks a repo that never ran init at all.
|
|
484
|
-
if (isGitRepo(root) && hookStatus(root).postCommit)
|
|
485
|
-
|
|
500
|
+
if (isGitRepo(root) && hookStatus(root).postCommit) {
|
|
501
|
+
const inv = resolveInvocation().shell;
|
|
502
|
+
installPostMergeHook(root, inv);
|
|
503
|
+
installPostCheckoutHook(root, inv); // same upgrade path for the workspace-ledger hook
|
|
504
|
+
}
|
|
486
505
|
const res = indexRepo(store, root, { requireClean: true });
|
|
487
506
|
const { counts } = store.reindex();
|
|
488
507
|
const correctionSweep = new ConstitutionService(store, root).upgradeCorrections();
|
|
@@ -707,7 +726,7 @@ program
|
|
|
707
726
|
const sweep = constitution.g2ShadowSweep();
|
|
708
727
|
g2Recorded = sweep.recorded.length;
|
|
709
728
|
if (!opts.quiet) {
|
|
710
|
-
console.log(` ↳ G2 shadow: ${sweep.recorded.length} recorded · ${sweep.existing.length} existing · ${sweep.failures.length} failed; authority none`);
|
|
729
|
+
console.log(` ↳ G2 shadow: ${sweep.recorded.length} recorded · ${sweep.existing.length} existing · ${sweep.failures.length} failed${sweep.retired.length ? ` · ${sweep.retired.length} retired (skipped)` : ""}; authority none`);
|
|
711
730
|
}
|
|
712
731
|
}
|
|
713
732
|
}
|
|
@@ -1242,9 +1261,12 @@ function configureOverlay(dir, opts, mode) {
|
|
|
1242
1261
|
if (opts.hook && isGitRepo(root)) {
|
|
1243
1262
|
freshSetup?.markHookWrite();
|
|
1244
1263
|
const h = installPostCommitHook(root, inv.shell, { private: true, commit: opts.autoCommit, localOnly: mode === "private" });
|
|
1245
|
-
|
|
1264
|
+
const note = (lines) => lines.map((l) => `${l}\n`).join("");
|
|
1265
|
+
hookNote = note(formatHookInstall(root, "post-commit hook", h, ` — captured decisions route here${opts.autoCommit ? " (auto-commit+push on)" : ""}`));
|
|
1246
1266
|
const pm = installPostMergeHook(root, inv.shell);
|
|
1247
|
-
hookNote +=
|
|
1267
|
+
hookNote += note(formatHookInstall(root, "post-merge hook", pm, " (squash-merge provenance repair + re-syncs grounding docs after a merge that brought memory in)"));
|
|
1268
|
+
const pc = installPostCheckoutHook(root, inv.shell);
|
|
1269
|
+
hookNote += note(formatHookInstall(root, "post-checkout hook", pc, " (workspace ledger: this machine's branches + worktrees sync through the overlay)"));
|
|
1248
1270
|
}
|
|
1249
1271
|
// 5) one-time migration: MOVE existing public memory INTO the overlay, then make
|
|
1250
1272
|
// THIS repo code-only. Records are absorbed (union by id) BEFORE the public
|
|
@@ -1304,6 +1326,13 @@ function configureOverlay(dir, opts, mode) {
|
|
|
1304
1326
|
` next: review, then commit the PUBLIC repo:\n` +
|
|
1305
1327
|
` git add -A && git commit -m "chore: move engineering memory to a private overlay" && git push\n`;
|
|
1306
1328
|
}
|
|
1329
|
+
else {
|
|
1330
|
+
// Re-running setup on a repo migrated by an older release upgrades its private-only
|
|
1331
|
+
// block in place and stops publishing memory dirs that block did not yet list.
|
|
1332
|
+
const upgraded = describeGitignoreUpgrade(upgradeManagedGitignore(root));
|
|
1333
|
+
if (upgraded.length)
|
|
1334
|
+
migrateNote = upgraded.map((line) => ` ✓ ${line}\n`).join("");
|
|
1335
|
+
}
|
|
1307
1336
|
const lead = mode === "private"
|
|
1308
1337
|
? `✓ private overlay enabled → ${hunchDir}\n`
|
|
1309
1338
|
: `✓ shared overlay enabled → ${hunchDir}\n`;
|
|
@@ -1409,11 +1438,220 @@ program
|
|
|
1409
1438
|
openStore = null;
|
|
1410
1439
|
}
|
|
1411
1440
|
}
|
|
1441
|
+
// 4) the workspace ledger: the new worktree is exactly what other machines want to know about.
|
|
1442
|
+
let ledgerNote = "";
|
|
1443
|
+
try {
|
|
1444
|
+
const lstore = openTeamStore(root).store;
|
|
1445
|
+
try {
|
|
1446
|
+
const out = recordWorkspaceSnapshot(lstore, root);
|
|
1447
|
+
if (out.status === "written")
|
|
1448
|
+
ledgerNote = `\n ✓ workspace ledger updated (${out.record.worktrees.length} worktree(s) on this machine → ${out.home === "private" ? "overlay" : "public .hunch/"})`;
|
|
1449
|
+
}
|
|
1450
|
+
finally {
|
|
1451
|
+
lstore.close();
|
|
1452
|
+
openStore = null;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
catch { /* the ledger is a side effect; the worktree itself is what this command promised */ }
|
|
1412
1456
|
console.log(`✓ worktree created → ${dest}${opts.branch ? ` (new branch ${opts.branch})` : ""}\n` +
|
|
1413
|
-
`${shareNote}${indexNote}\n` +
|
|
1457
|
+
`${shareNote}${indexNote}${ledgerNote}\n` +
|
|
1414
1458
|
` hooks + MCP server are shared (worktree-aware) — open your assistant in the new worktree to start.\n` +
|
|
1415
1459
|
` (needs \`hunch\` installed globally; a worktree has no node_modules of its own)`);
|
|
1416
1460
|
});
|
|
1461
|
+
// ---- workspaces / branches (workspace ledger — docs/workspace-ledger.md) ---------------
|
|
1462
|
+
// One shared code path (src/integrations/workspaceLedger.ts): this machine is always read
|
|
1463
|
+
// LIVE from git, other machines from the store, and stored records are display-only.
|
|
1464
|
+
const workspacesCmd = program
|
|
1465
|
+
.command("workspaces")
|
|
1466
|
+
.description("Workspace ledger: which worktrees are open on which machine (this machine live, other machines from memory). Read-only unless you run `snapshot`.");
|
|
1467
|
+
workspacesCmd
|
|
1468
|
+
.command("list", { isDefault: true })
|
|
1469
|
+
.description("Every worktree across machines: branch, dirty, last commit, when the machine last reported.")
|
|
1470
|
+
.option("--machine <label>", "only this machine")
|
|
1471
|
+
.option("--branch <name>", "only worktrees on this branch")
|
|
1472
|
+
.option("--fetch", "run `git fetch --prune` first (network; off by default)")
|
|
1473
|
+
.option("--json", "emit the rows as JSON")
|
|
1474
|
+
.action((opts) => {
|
|
1475
|
+
const { store, root } = storeFor();
|
|
1476
|
+
try {
|
|
1477
|
+
if (!isGitRepo(root))
|
|
1478
|
+
return fail("`hunch workspaces` needs a git repo");
|
|
1479
|
+
const view = workspaceLedgerView(store, root, { fetch: opts.fetch });
|
|
1480
|
+
let rows = worktreeRows(view.records, { staleAfterDays: view.config.stale_after_days });
|
|
1481
|
+
if (opts.machine)
|
|
1482
|
+
rows = rows.filter((r) => r.machine === opts.machine);
|
|
1483
|
+
if (opts.branch)
|
|
1484
|
+
rows = rows.filter((r) => r.branch === opts.branch);
|
|
1485
|
+
if (opts.json)
|
|
1486
|
+
return console.log(JSON.stringify({ machine: view.machine.label, worktrees: rows }, null, 2));
|
|
1487
|
+
console.log(renderWorktreeTable(view, rows));
|
|
1488
|
+
}
|
|
1489
|
+
finally {
|
|
1490
|
+
store.close();
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
workspacesCmd
|
|
1494
|
+
.command("snapshot")
|
|
1495
|
+
.description("Record this machine's worktrees and branches into memory (the overlay when one is configured). Offline unless --fetch. Also run by the post-checkout / post-commit hooks and at MCP session start.")
|
|
1496
|
+
.option("--fetch", "run `git fetch --prune` first (network; off by default)")
|
|
1497
|
+
.option("--dry-run", "print the record that WOULD be written and write nothing")
|
|
1498
|
+
.option("--json", "print the record as JSON")
|
|
1499
|
+
.option("--quiet", "no output on success (for hooks)")
|
|
1500
|
+
.action((opts) => {
|
|
1501
|
+
const { store, root } = storeFor();
|
|
1502
|
+
try {
|
|
1503
|
+
if (!isGitRepo(root))
|
|
1504
|
+
return fail("`hunch workspaces snapshot` needs a git repo");
|
|
1505
|
+
const out = recordWorkspaceSnapshot(store, root, { fetch: opts.fetch, dryRun: opts.dryRun });
|
|
1506
|
+
if (out.status !== "off" && (opts.dryRun || opts.json))
|
|
1507
|
+
console.log(JSON.stringify(out.record, null, 2));
|
|
1508
|
+
if (opts.quiet || out.status === "dry-run")
|
|
1509
|
+
return;
|
|
1510
|
+
switch (out.status) {
|
|
1511
|
+
case "off":
|
|
1512
|
+
return console.log("workspaces.publish is \"off\" in .hunch/config.json — nothing recorded.");
|
|
1513
|
+
case "no-home":
|
|
1514
|
+
console.log("No memory overlay is configured, so this machine's record is not written (queries read this machine live).");
|
|
1515
|
+
console.log(" · run `hunch private` or `hunch shared --repo <url>` to sync workspaces across machines");
|
|
1516
|
+
return console.log(" · or set .hunch/config.json {\"workspaces\":{\"publish_public\":true}} to commit it into this repo's .hunch/");
|
|
1517
|
+
case "unchanged":
|
|
1518
|
+
return console.log(`✓ unchanged since ${out.previous.observed_at} (${out.record.id}) — nothing written`);
|
|
1519
|
+
case "collision":
|
|
1520
|
+
return fail(`not written: ${out.reason}\n · \`hunch workspaces forget ${out.record.id}\` removes the stale copy (a normal, revertable memory move), then snapshot again`);
|
|
1521
|
+
case "written":
|
|
1522
|
+
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}` : ""}`);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
finally {
|
|
1526
|
+
store.close();
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
workspacesCmd
|
|
1530
|
+
.command("prune")
|
|
1531
|
+
.description("Branches and worktrees that are provably merged and safe to delete. Prints the exact git commands per machine (dry run). --apply runs them on THIS machine only — never a remote, never another machine — from a live snapshot, with `git branch -d` / `git worktree remove` (no force flags), after confirmation.")
|
|
1532
|
+
.option("--apply", "execute this machine's commands (asks for confirmation; --yes in a non-interactive shell)")
|
|
1533
|
+
.option("--yes", "skip the confirmation prompt (required with --apply when stdin is not a terminal)")
|
|
1534
|
+
.option("--fetch", "run `git fetch --prune` first (network; off by default)")
|
|
1535
|
+
.option("--json", "emit the plan (and results) as JSON")
|
|
1536
|
+
.action(async (opts) => {
|
|
1537
|
+
const { store, root } = storeFor();
|
|
1538
|
+
try {
|
|
1539
|
+
if (!isGitRepo(root))
|
|
1540
|
+
return fail("`hunch workspaces prune` needs a git repo");
|
|
1541
|
+
const view = workspaceLedgerView(store, root, { fetch: opts.fetch });
|
|
1542
|
+
const plan = prunePlanFor(view, root);
|
|
1543
|
+
if (!opts.apply) {
|
|
1544
|
+
if (opts.json)
|
|
1545
|
+
return console.log(JSON.stringify({ machine: view.machine.label, plan }, null, 2));
|
|
1546
|
+
console.log(renderPrunePlan(view, plan));
|
|
1547
|
+
return console.log(plan.local.length ? "\n(dry run — `hunch workspaces prune --apply` runs this machine's commands after confirmation)" : "\n(dry run — nothing to apply on this machine)");
|
|
1548
|
+
}
|
|
1549
|
+
if (!plan.local.length) {
|
|
1550
|
+
if (opts.json)
|
|
1551
|
+
return console.log(JSON.stringify({ machine: view.machine.label, plan, results: [] }, null, 2));
|
|
1552
|
+
console.log(renderPrunePlan(view, plan));
|
|
1553
|
+
return console.log("\nnothing to apply on this machine");
|
|
1554
|
+
}
|
|
1555
|
+
if (!opts.json)
|
|
1556
|
+
console.log(renderPrunePlan(view, plan));
|
|
1557
|
+
if (!opts.yes) {
|
|
1558
|
+
const ok = await confirmPrune(pruneConfirmQuestion(view, plan));
|
|
1559
|
+
if (!ok)
|
|
1560
|
+
return fail(process.stdin.isTTY ? "not confirmed — nothing was deleted" : "refusing to apply without confirmation: stdin is not a terminal; pass --yes to confirm explicitly");
|
|
1561
|
+
}
|
|
1562
|
+
const results = applyPrune(root, plan.local);
|
|
1563
|
+
// Memory follows what actually happened: re-snapshot so other machines see the change.
|
|
1564
|
+
// Best effort — the deletions above are real whatever the ledger write says.
|
|
1565
|
+
let recorded;
|
|
1566
|
+
try {
|
|
1567
|
+
recorded = recordWorkspaceSnapshot(store, root).status;
|
|
1568
|
+
}
|
|
1569
|
+
catch (error) {
|
|
1570
|
+
recorded = `failed: ${error.message}`;
|
|
1571
|
+
}
|
|
1572
|
+
if (opts.json)
|
|
1573
|
+
return console.log(JSON.stringify({ machine: view.machine.label, plan, results, recorded }, null, 2));
|
|
1574
|
+
console.log("");
|
|
1575
|
+
for (const r of results)
|
|
1576
|
+
console.log(` ${r.outcome === "deleted" ? "✓" : r.outcome === "skipped" ? "–" : "✗"} ${r.step.branch}: ${r.detail}`);
|
|
1577
|
+
const failed = results.filter((r) => r.outcome === "failed").length;
|
|
1578
|
+
const skipped = results.filter((r) => r.outcome === "skipped").length;
|
|
1579
|
+
const ledger = recorded === "written" ? " · ledger updated" : recorded === "unchanged" || recorded === "no-home" || recorded === "off" ? "" : ` · ledger not updated (${recorded})`;
|
|
1580
|
+
console.log(`\n${results.length - failed - skipped} deleted, ${skipped ? `${skipped} skipped (nothing changed), ` : ""}${failed} refused by git${ledger}`);
|
|
1581
|
+
if (failed || skipped)
|
|
1582
|
+
process.exitCode = 1;
|
|
1583
|
+
}
|
|
1584
|
+
finally {
|
|
1585
|
+
store.close();
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
workspacesCmd
|
|
1589
|
+
.command("label [label]")
|
|
1590
|
+
.description("Show or set this machine's label (what other machines see). Defaults to machine-<id>; never the hostname.")
|
|
1591
|
+
.action((label) => {
|
|
1592
|
+
const identity = label ? setMachineLabel(label) : loadOrCreateMachine();
|
|
1593
|
+
console.log(`${identity.label} (${identity.id}, ${machineFile()})`);
|
|
1594
|
+
const leak = labelLeaksIdentity(identity.label);
|
|
1595
|
+
if (leak)
|
|
1596
|
+
console.log(` ⚠ the label equals this machine's ${leak}; in a shared store every teammate sees it`);
|
|
1597
|
+
});
|
|
1598
|
+
workspacesCmd
|
|
1599
|
+
.command("forget <machine>")
|
|
1600
|
+
.description("Remove a machine's stored workspace record (by label or id) — e.g. a retired laptop. A memory move like any other: revertable via `hunch log`.")
|
|
1601
|
+
.action((machine) => {
|
|
1602
|
+
const { store, root } = storeFor();
|
|
1603
|
+
try {
|
|
1604
|
+
const victims = store.recs("workspaces").filter((r) => r.machine.label === machine || r.machine.id === machine || r.id === machine);
|
|
1605
|
+
if (!victims.length)
|
|
1606
|
+
return fail(`no workspace record for "${machine}" — \`hunch workspaces\` lists the machines in memory`);
|
|
1607
|
+
// Decide each record's home BEFORE deleting it, then flush exactly those homes.
|
|
1608
|
+
const homes = victims.map((v) => store.getPrivateRec("workspaces", v.id) ? "private" : "public");
|
|
1609
|
+
for (const v of victims)
|
|
1610
|
+
store.deleteWhereItLives("workspaces", v.id);
|
|
1611
|
+
pumpMemoryHomes(store, root, homes, `hunch: forget workspace ${machine}`);
|
|
1612
|
+
console.log(`✓ forgot ${victims.length} record(s) for ${machine}`);
|
|
1613
|
+
}
|
|
1614
|
+
finally {
|
|
1615
|
+
store.close();
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
program
|
|
1619
|
+
.command("branches")
|
|
1620
|
+
.description("Every local branch across machines with a deterministic verdict: merged (ancestry / squash / rebase), pushed, dirty worktree, and a recommended action. Read-only; never deletes anything.")
|
|
1621
|
+
.option("--merged", "only branches proven merged")
|
|
1622
|
+
.option("--unpushed", "only branches with no upstream")
|
|
1623
|
+
.option("--stale <days>", "only branches whose last commit is older than N days")
|
|
1624
|
+
.option("--machine <label>", "only branches present on this machine")
|
|
1625
|
+
.option("--fetch", "run `git fetch --prune` first (network; off by default)")
|
|
1626
|
+
.option("--json", "emit the rows as JSON")
|
|
1627
|
+
.action((opts) => {
|
|
1628
|
+
const { store, root } = storeFor();
|
|
1629
|
+
try {
|
|
1630
|
+
if (!isGitRepo(root))
|
|
1631
|
+
return fail("`hunch branches` needs a git repo");
|
|
1632
|
+
const view = workspaceLedgerView(store, root, { fetch: opts.fetch });
|
|
1633
|
+
const now = new Date();
|
|
1634
|
+
let rows = branchRows(view.records, { staleAfterDays: view.config.stale_after_days, now });
|
|
1635
|
+
if (opts.merged)
|
|
1636
|
+
rows = rows.filter((r) => r.merged.status === "merged");
|
|
1637
|
+
if (opts.unpushed)
|
|
1638
|
+
rows = rows.filter((r) => r.upstream === null);
|
|
1639
|
+
if (opts.machine)
|
|
1640
|
+
rows = rows.filter((r) => r.machines.includes(opts.machine));
|
|
1641
|
+
if (opts.stale) {
|
|
1642
|
+
const days = Number(opts.stale);
|
|
1643
|
+
if (!Number.isFinite(days) || days < 0)
|
|
1644
|
+
return fail("--stale takes a number of days");
|
|
1645
|
+
rows = rows.filter((r) => !r.last_commit_at || now.getTime() - Date.parse(r.last_commit_at) > days * 86_400_000);
|
|
1646
|
+
}
|
|
1647
|
+
if (opts.json)
|
|
1648
|
+
return console.log(JSON.stringify({ machine: view.machine.label, branches: rows }, null, 2));
|
|
1649
|
+
console.log(renderBranchTable(view, rows));
|
|
1650
|
+
}
|
|
1651
|
+
finally {
|
|
1652
|
+
store.close();
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1417
1655
|
// ---- query ----------------------------------------------------------------
|
|
1418
1656
|
program
|
|
1419
1657
|
.command("query")
|
|
@@ -3806,14 +4044,17 @@ program
|
|
|
3806
4044
|
// vacuous green (deletions are excluded by the enumerators' --diff-filter=ACMR, so
|
|
3807
4045
|
// such a PR enumerates zero files) — including a deletion of the very symbol a
|
|
3808
4046
|
// blocking conformance predicate or an active policy guards.
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
4047
|
+
// The COMPLETE diff (no synthesis byte budget); an incomplete one is flagged so
|
|
4048
|
+
// content-matched blocking invariants fail closed instead of passing unseen.
|
|
4049
|
+
const gate = files.length
|
|
4050
|
+
? (exactCommit ? commitGateDiff(exactCommit, root) : opts.base ? rangeGateDiff(opts.base, root) : opts.working ? workingGateDiff(root) : stagedGateDiff(root))
|
|
4051
|
+
: { diff: "" };
|
|
3812
4052
|
const report = files.length
|
|
3813
|
-
? store.buildCheckReport(files, diff, {
|
|
4053
|
+
? store.buildCheckReport(files, gate.diff, {
|
|
3814
4054
|
strict: !!opts.strict,
|
|
3815
4055
|
lastChange: (f) => lastChangeDate(f, root),
|
|
3816
4056
|
publicOnly: !!opts.publicOnly,
|
|
4057
|
+
diffStatus: gate,
|
|
3817
4058
|
})
|
|
3818
4059
|
: emptyReport;
|
|
3819
4060
|
if (opts.blast && !markdown && !sarif && files.length) {
|
|
@@ -3951,8 +4192,8 @@ const vetoCmd = program
|
|
|
3951
4192
|
store.close();
|
|
3952
4193
|
return;
|
|
3953
4194
|
}
|
|
3954
|
-
const
|
|
3955
|
-
const full = store.buildCheckReport(files, diff, { strict: !!opts.strict, lastChange: (f) => lastChangeDate(f, root) });
|
|
4195
|
+
const gate = opts.commit ? commitGateDiff(opts.commit, root) : opts.base ? rangeGateDiff(opts.base, root) : stagedGateDiff(root);
|
|
4196
|
+
const full = store.buildCheckReport(files, gate.diff, { strict: !!opts.strict, lastChange: (f) => lastChangeDate(f, root), diffStatus: gate });
|
|
3956
4197
|
if (!full.vetoes.length) {
|
|
3957
4198
|
console.log(`✓ ${files.length} changed file(s) reverse no decision you rejected.`);
|
|
3958
4199
|
store.close();
|
|
@@ -4513,11 +4754,14 @@ program
|
|
|
4513
4754
|
const before = st;
|
|
4514
4755
|
let activity = null;
|
|
4515
4756
|
if (/^(Edit|Write|MultiEdit)$/.test(evt.tool_name ?? "")) {
|
|
4516
|
-
|
|
4517
|
-
|
|
4757
|
+
// A Codex apply_patch touches every file it lists (and each Move-to
|
|
4758
|
+
// destination); the Stop gate must see all of them, not only the first.
|
|
4759
|
+
const patchPaths = evt.tool_input?.patch_files?.flatMap((f) => f.moved_to ? [f.path, f.moved_to] : [f.path]);
|
|
4760
|
+
const edited = patchPaths?.length ? patchPaths : evt.tool_input?.file_path ? [evt.tool_input.file_path] : [];
|
|
4761
|
+
for (const p of edited)
|
|
4518
4762
|
st = onEdit(st, toRepoRel(root, p));
|
|
4763
|
+
if (edited.length)
|
|
4519
4764
|
activity = { kind: "edit" };
|
|
4520
|
-
}
|
|
4521
4765
|
}
|
|
4522
4766
|
else if (evt.tool_name === "Bash" || evt.tool_name === "PowerShell") {
|
|
4523
4767
|
const command = String(evt.tool_input?.command ?? "");
|
|
@@ -4827,18 +5071,16 @@ program
|
|
|
4827
5071
|
}
|
|
4828
5072
|
if (evt.hook_event_name !== "PreToolUse")
|
|
4829
5073
|
return;
|
|
4830
|
-
const
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
const target = toRepoRel(root, abs);
|
|
4834
|
-
// Outside the repo (".." prefix) or on another drive (absolute, e.g. "D:/…")
|
|
4835
|
-
// → nothing for Hunch to say.
|
|
4836
|
-
if (!target || target.startsWith("..") || /^[a-zA-Z]:/.test(target))
|
|
5074
|
+
const targets = editTargets(root, evt.tool_input);
|
|
5075
|
+
// Nothing inside the repo → nothing for Hunch to say.
|
|
5076
|
+
if (!targets.length)
|
|
4837
5077
|
return;
|
|
5078
|
+
// Grounding below stays about the first in-repo file; the strict gate checks every target.
|
|
5079
|
+
const { abs, target } = targets[0];
|
|
4838
5080
|
// A compiled red→green probe is only meaningful if its red receipt exists
|
|
4839
5081
|
// before implementation. Firm/strict may deny two edits per prompt, then
|
|
4840
5082
|
// fail open so a malformed or unavailable probe can never deadlock work.
|
|
4841
|
-
if ((firmness === "firm" || firmness === "strict") && evt.session_id && pipelineEnabled() && isProductPath(target)) {
|
|
5083
|
+
if ((firmness === "firm" || firmness === "strict") && evt.session_id && pipelineEnabled() && targets.some((t) => isProductPath(t.target))) {
|
|
4842
5084
|
const baseline = beforeEditProbeVerdict(loadPipelineState(evt.session_id));
|
|
4843
5085
|
if (baseline.block) {
|
|
4844
5086
|
savePipelineState(evt.session_id, baseline.state);
|
|
@@ -4870,23 +5112,28 @@ program
|
|
|
4870
5112
|
store.reindex();
|
|
4871
5113
|
// The lines this edit would ADD — so a content-matched invariant denies only
|
|
4872
5114
|
// when the edit actually trips it (not on every edit in scope), and the Veto
|
|
4873
|
-
// Guard can test the proposed text. Covers Edit/Write/MultiEdit
|
|
4874
|
-
|
|
4875
|
-
const
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
5115
|
+
// Guard can test the proposed text. Covers Edit/Write/MultiEdit, and each
|
|
5116
|
+
// file of a Codex apply_patch with that file's own added lines.
|
|
5117
|
+
const denials = [];
|
|
5118
|
+
for (const t of targets) {
|
|
5119
|
+
// Veto Guard (live): the proposed edit text re-introduces an approach an
|
|
5120
|
+
// in-force decision REJECTED. The agent self-corrects before staging;
|
|
5121
|
+
// only human-confirmed tripwires deny.
|
|
5122
|
+
const hit = blockingInScope(store, t.target, t.lines) ?? (t.lines.length ? vetoInScope(store, t.target, t.lines) : null);
|
|
5123
|
+
if (hit)
|
|
5124
|
+
denials.push({ target: t.target, hit });
|
|
4881
5125
|
}
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
5126
|
+
if (denials.length) {
|
|
5127
|
+
for (const d of denials)
|
|
5128
|
+
appendEvent(paths, { at: new Date().toISOString(), file: d.target, ...d.hit.event });
|
|
5129
|
+
// One denied file keeps the single-file reason verbatim; several are
|
|
5130
|
+
// listed so the agent knows every file it must reconsider.
|
|
5131
|
+
const reason = denials.length === 1
|
|
5132
|
+
? denials[0].hit.reason
|
|
5133
|
+
: `Hunch: this patch is denied for ${denials.length} files (${denials.map((d) => d.target).join(", ")}).\n${denials.map((d) => d.hit.reason).join("\n")}`;
|
|
5134
|
+
emitDeny(provider, reason);
|
|
5135
|
+
for (const d of denials)
|
|
5136
|
+
observeHookDenial(root, provider, evt, d.target, d.hit);
|
|
4890
5137
|
return;
|
|
4891
5138
|
}
|
|
4892
5139
|
}
|
|
@@ -6343,8 +6590,8 @@ program
|
|
|
6343
6590
|
console.log(`No changed files in ${scope}.`);
|
|
6344
6591
|
return;
|
|
6345
6592
|
}
|
|
6346
|
-
const
|
|
6347
|
-
console.log(renderImpact(store.prImpact(files, diff), scope));
|
|
6593
|
+
const gate = opts.commit ? commitGateDiff(opts.commit, root) : base ? rangeGateDiff(base, root) : opts.working ? workingGateDiff(root) : stagedGateDiff(root);
|
|
6594
|
+
console.log(renderImpact(store.prImpact(files, gate.diff, gate), scope));
|
|
6348
6595
|
}
|
|
6349
6596
|
finally {
|
|
6350
6597
|
store.close();
|
|
@@ -6502,6 +6749,10 @@ program
|
|
|
6502
6749
|
console.log(`\n📊 ${rankingStatusLine(resolveTaskRankingMode(store.publicRoot, store))}`);
|
|
6503
6750
|
}
|
|
6504
6751
|
catch { /* no task records or no cache dir: nothing to say */ }
|
|
6752
|
+
// Workspace ledger, from stored records only (no git) so the hot view stays fast.
|
|
6753
|
+
const ws = workspaceSummaryLine(opts.private ? store.recs("workspaces") : store.json.loadAll("workspaces"), workspacesConfig(readConfig(hunchPaths(store.publicRoot))));
|
|
6754
|
+
if (ws)
|
|
6755
|
+
console.log(`\n${ws}`);
|
|
6505
6756
|
}
|
|
6506
6757
|
finally {
|
|
6507
6758
|
store.close();
|
|
@@ -6760,16 +7011,43 @@ program
|
|
|
6760
7011
|
// squash-merge went unrepaired. pre-commit is opt-out (`--no-enforce`),
|
|
6761
7012
|
// so its absence is informational only, never a warning.
|
|
6762
7013
|
if (isGitRepo(root)) {
|
|
7014
|
+
const report = hookReport(root);
|
|
6763
7015
|
const hooks = hookStatus(root);
|
|
6764
|
-
const missing = [
|
|
7016
|
+
const missing = [report.postCommit.state === "missing" && "post-commit", report.postMerge.state === "missing" && "post-merge"].filter((h) => !!h);
|
|
7017
|
+
// A block that is present but never runs (after an exec/exit, or inside a
|
|
7018
|
+
// hook manager's regenerated file — issue #311) is NOT installed.
|
|
7019
|
+
const unreachable = [["post-commit", report.postCommit], ["post-merge", report.postMerge], ["pre-commit", report.preCommit], ["post-checkout", report.postCheckout]]
|
|
7020
|
+
.filter(([, e]) => e.state === "unreachable");
|
|
6765
7021
|
// `hunch index` only ever installs post-merge onto an existing
|
|
6766
7022
|
// post-commit install (it never hooks an un-hooked repo — see
|
|
6767
|
-
// isGitRepo(root) && hookStatus(root).postCommit above)
|
|
6768
|
-
// hint must not point there when
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
7023
|
+
// isGitRepo(root) && hookStatus(root).postCommit above) and never writes
|
|
7024
|
+
// into a manager-owned hook — so the fix hint must not point there when
|
|
7025
|
+
// post-commit itself is missing or a hook manager is in play.
|
|
7026
|
+
const managedMissing = [report.postCommit, report.postMerge].find((e) => e.state === "missing" && e.manager !== "none");
|
|
7027
|
+
const fix = managedMissing
|
|
7028
|
+
? `${managedMissing.manager === "exec-exit" ? "the existing hook exits before an appended block would run" : `a hook manager (${managedMissing.manager}) owns these hooks`}; run \`hunch init\` to print the snippet to add to ${rel(root, managedMissing.path)}`
|
|
7029
|
+
: !hooks.postCommit ? "run `hunch init` for the full setup" : "run `hunch index` to install it";
|
|
7030
|
+
const problems = [
|
|
7031
|
+
missing.length ? `⚠ missing ${missing.join(", ")} — ${fix}` : "",
|
|
7032
|
+
unreachable.length ? `⚠ installed but unreachable: ${unreachable.map(([n]) => n).join(", ")} — never runs; run \`hunch init\` to print the snippet for your hook manager` : "",
|
|
7033
|
+
].filter(Boolean);
|
|
7034
|
+
console.log(`hooks: ${problems.length
|
|
7035
|
+
? problems.join("\n ")
|
|
6772
7036
|
: `post-commit, post-merge installed${hooks.preCommit ? " (+ pre-commit)" : ""}`}`);
|
|
7037
|
+
for (const [name, e] of unreachable) {
|
|
7038
|
+
console.log(dim(` ↳ ${name}: ${e.reason ?? "the block sits where git never reaches it"} (${rel(root, e.path)})`));
|
|
7039
|
+
}
|
|
7040
|
+
// Workspace ledger: what this machine is called, whether its record is in memory,
|
|
7041
|
+
// and whether the checkout hook that keeps it fresh is installed.
|
|
7042
|
+
try {
|
|
7043
|
+
const machine = loadOrCreateMachine();
|
|
7044
|
+
const stored = store.recs("workspaces").find((r) => r.machine.id === machine.id);
|
|
7045
|
+
const others = store.recs("workspaces").filter((r) => r.machine.id !== machine.id).length;
|
|
7046
|
+
const leak = labelLeaksIdentity(machine.label);
|
|
7047
|
+
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)` +
|
|
7048
|
+
`${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)" : ""}`);
|
|
7049
|
+
}
|
|
7050
|
+
catch { /* no machine file writable: nothing to report */ }
|
|
6773
7051
|
}
|
|
6774
7052
|
// In unified mode the public .hunch directory is only a routing shell.
|
|
6775
7053
|
// Report the same effective manifest that `hunch migrate` reads and stamps,
|
|
@@ -6935,6 +7213,28 @@ function realpathNorm(p) {
|
|
|
6935
7213
|
function toRepoRel(root, abs) {
|
|
6936
7214
|
return relative(realpathNorm(root), realpathNorm(abs)).split("\\").join("/");
|
|
6937
7215
|
}
|
|
7216
|
+
/** The in-repo files a pre-edit event would change, each with the lines the edit
|
|
7217
|
+
* ADDS to that file. A Codex apply_patch yields one entry per touched path (a
|
|
7218
|
+
* Move-to destination is its own entry, carrying the section's added lines);
|
|
7219
|
+
* every other edit tool yields its single `file_path`. Paths outside the repo
|
|
7220
|
+
* ("..") or on another drive ("D:/…") are skipped. */
|
|
7221
|
+
function editTargets(root, input) {
|
|
7222
|
+
const raw = input?.patch_files?.length
|
|
7223
|
+
? input.patch_files.flatMap((f) => (f.moved_to ? [f.path, f.moved_to] : [f.path]).map((abs) => ({ abs, lines: f.added_lines })))
|
|
7224
|
+
: input?.file_path ? [{ abs: input.file_path, lines: proposedEditLines(input) }] : [];
|
|
7225
|
+
const byTarget = new Map();
|
|
7226
|
+
for (const { abs, lines } of raw) {
|
|
7227
|
+
const target = toRepoRel(root, abs);
|
|
7228
|
+
if (!target || target.startsWith("..") || /^[a-zA-Z]:/.test(target))
|
|
7229
|
+
continue;
|
|
7230
|
+
const seen = byTarget.get(target);
|
|
7231
|
+
if (seen)
|
|
7232
|
+
seen.lines = [...seen.lines, ...lines];
|
|
7233
|
+
else
|
|
7234
|
+
byTarget.set(target, { abs, target, lines });
|
|
7235
|
+
}
|
|
7236
|
+
return [...byTarget.values()];
|
|
7237
|
+
}
|
|
6938
7238
|
function emitContext(provider, event, text,
|
|
6939
7239
|
/** One user-facing line where the host shows hook messages (Claude Code's
|
|
6940
7240
|
* `systemMessage`); never a block, never a second model turn. */
|
package/dist/cli/integrations.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { findRoot } from "../core/paths.js";
|
|
2
2
|
import { CAPABILITIES, HARNESSES, inspectIntegrations, integrationHealthFails, formatIntegrationHealth, repairIntegrationPins } from "../integrations/health.js";
|
|
3
3
|
import { probeIntegration } from "../integrations/probe.js";
|
|
4
|
+
import { describeGitignoreUpgrade, upgradeManagedGitignore } from "../integrations/gitignore.js";
|
|
4
5
|
export function registerIntegrationCommands(program, refreshGrounding) {
|
|
5
6
|
const integrations = program.command("integrations").description("Check harness coverage and repair stale repository Hunch pins");
|
|
6
7
|
integrations.command("check")
|
|
@@ -34,6 +35,15 @@ export function registerIntegrationCommands(program, refreshGrounding) {
|
|
|
34
35
|
if (files.includes(".codex/hooks.json")) {
|
|
35
36
|
console.log("Codex: open /hooks to review and trust the changed commands, then start a new session. Command changes require renewed trust; Hunch does not grant it automatically.");
|
|
36
37
|
}
|
|
38
|
+
// `hunch update` runs this command with the NEW release, so existing repos pick up
|
|
39
|
+
// managed .gitignore entries added since they were set up (no re-init needed).
|
|
40
|
+
try {
|
|
41
|
+
for (const line of describeGitignoreUpgrade(upgradeManagedGitignore(root)))
|
|
42
|
+
console.log(line);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.log(`.gitignore not upgraded: ${error.message}`);
|
|
46
|
+
}
|
|
37
47
|
const grounding = refreshGrounding();
|
|
38
48
|
if (grounding.length)
|
|
39
49
|
console.log(`Updated Hunch instructions: ${grounding.join(", ")}. Reconnect the agent to load task reporting instructions.`);
|
package/dist/cli/serve.js
CHANGED
|
@@ -35,6 +35,7 @@ export function registerServeCommands(program) {
|
|
|
35
35
|
app.listen(port, "127.0.0.1", () => {
|
|
36
36
|
console.log(`hunch ${HUNCH_VERSION} serving nuryel.state/1 on http://127.0.0.1:${port} — ${config.partitions.map((p) => scopePath(p.scope)).join(", ")} (${config.principals.length} principal(s))`);
|
|
37
37
|
console.log(`Shared state view: http://127.0.0.1:${port}/operator`);
|
|
38
|
+
console.log(`MCP (streamable HTTP): http://127.0.0.1:${port}/nuryel/v1/mcp`);
|
|
38
39
|
});
|
|
39
40
|
const stop = () => { app.close(() => { app.closeStores(); process.exit(0); }); };
|
|
40
41
|
process.on("SIGINT", stop);
|