@lore-co/cli 0.2.25 → 0.2.27

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.
Files changed (216) hide show
  1. package/README.md +69 -18
  2. package/dist/agents.d.ts +9 -0
  3. package/dist/agents.d.ts.map +1 -1
  4. package/dist/agents.js +19 -6
  5. package/dist/agents.js.map +1 -1
  6. package/dist/api.d.ts +2 -1
  7. package/dist/api.d.ts.map +1 -1
  8. package/dist/api.js +15 -2
  9. package/dist/api.js.map +1 -1
  10. package/dist/approach-guidance.d.ts +8 -1
  11. package/dist/approach-guidance.d.ts.map +1 -1
  12. package/dist/approach-guidance.js +13 -3
  13. package/dist/approach-guidance.js.map +1 -1
  14. package/dist/approach.d.ts.map +1 -1
  15. package/dist/approach.js +2 -2
  16. package/dist/approach.js.map +1 -1
  17. package/dist/ask.d.ts.map +1 -1
  18. package/dist/ask.js +2 -2
  19. package/dist/ask.js.map +1 -1
  20. package/dist/capture-scope.d.ts.map +1 -1
  21. package/dist/capture-scope.js +2 -1
  22. package/dist/capture-scope.js.map +1 -1
  23. package/dist/chat.d.ts.map +1 -1
  24. package/dist/chat.js +2 -2
  25. package/dist/chat.js.map +1 -1
  26. package/dist/cli.d.ts +66 -2
  27. package/dist/cli.d.ts.map +1 -1
  28. package/dist/cli.js +547 -91
  29. package/dist/cli.js.map +1 -1
  30. package/dist/codex-hook-approval.d.ts.map +1 -1
  31. package/dist/codex-hook-approval.js +9 -2
  32. package/dist/codex-hook-approval.js.map +1 -1
  33. package/dist/command-help.d.ts.map +1 -1
  34. package/dist/command-help.js +7 -3
  35. package/dist/command-help.js.map +1 -1
  36. package/dist/config-command.js +2 -2
  37. package/dist/config-command.js.map +1 -1
  38. package/dist/connect-project.d.ts.map +1 -1
  39. package/dist/connect-project.js +2 -1
  40. package/dist/connect-project.js.map +1 -1
  41. package/dist/console-keys.d.ts +11 -0
  42. package/dist/console-keys.d.ts.map +1 -0
  43. package/dist/console-keys.js +24 -0
  44. package/dist/console-keys.js.map +1 -0
  45. package/dist/cursor-mcp.d.ts.map +1 -1
  46. package/dist/cursor-mcp.js +4 -0
  47. package/dist/cursor-mcp.js.map +1 -1
  48. package/dist/demo-process.js +3 -3
  49. package/dist/demo-process.js.map +1 -1
  50. package/dist/devin-capture.d.ts.map +1 -1
  51. package/dist/devin-capture.js +2 -2
  52. package/dist/devin-capture.js.map +1 -1
  53. package/dist/devin-cli.d.ts.map +1 -1
  54. package/dist/devin-cli.js +23 -13
  55. package/dist/devin-cli.js.map +1 -1
  56. package/dist/devin-client.d.ts +3 -0
  57. package/dist/devin-client.d.ts.map +1 -1
  58. package/dist/devin-client.js +5 -0
  59. package/dist/devin-client.js.map +1 -1
  60. package/dist/devin.d.ts.map +1 -1
  61. package/dist/devin.js +29 -9
  62. package/dist/devin.js.map +1 -1
  63. package/dist/generated-assets.d.ts +8 -8
  64. package/dist/generated-assets.d.ts.map +1 -1
  65. package/dist/generated-assets.js +8 -8
  66. package/dist/generated-assets.js.map +1 -1
  67. package/dist/github.d.ts.map +1 -1
  68. package/dist/github.js +38 -7
  69. package/dist/github.js.map +1 -1
  70. package/dist/guard.d.ts +1 -1
  71. package/dist/guard.d.ts.map +1 -1
  72. package/dist/guard.js +42 -14
  73. package/dist/guard.js.map +1 -1
  74. package/dist/handoff.js +2 -2
  75. package/dist/handoff.js.map +1 -1
  76. package/dist/host.js +4 -4
  77. package/dist/host.js.map +1 -1
  78. package/dist/image-capture.d.ts.map +1 -1
  79. package/dist/image-capture.js +5 -4
  80. package/dist/image-capture.js.map +1 -1
  81. package/dist/learnings.d.ts.map +1 -1
  82. package/dist/learnings.js +3 -3
  83. package/dist/learnings.js.map +1 -1
  84. package/dist/live.d.ts +7 -0
  85. package/dist/live.d.ts.map +1 -1
  86. package/dist/live.js +11 -6
  87. package/dist/live.js.map +1 -1
  88. package/dist/lore-agent.d.ts +8 -1
  89. package/dist/lore-agent.d.ts.map +1 -1
  90. package/dist/lore-agent.js +33 -11
  91. package/dist/lore-agent.js.map +1 -1
  92. package/dist/mcp-config.d.ts +7 -0
  93. package/dist/mcp-config.d.ts.map +1 -1
  94. package/dist/mcp-config.js +19 -3
  95. package/dist/mcp-config.js.map +1 -1
  96. package/dist/npm-installation.d.ts +30 -0
  97. package/dist/npm-installation.d.ts.map +1 -0
  98. package/dist/npm-installation.js +46 -0
  99. package/dist/npm-installation.js.map +1 -0
  100. package/dist/owned-files.d.ts +14 -4
  101. package/dist/owned-files.d.ts.map +1 -1
  102. package/dist/owned-files.js +24 -9
  103. package/dist/owned-files.js.map +1 -1
  104. package/dist/platform.d.ts +26 -3
  105. package/dist/platform.d.ts.map +1 -1
  106. package/dist/platform.js +87 -20
  107. package/dist/platform.js.map +1 -1
  108. package/dist/pty.d.ts +2 -1
  109. package/dist/pty.d.ts.map +1 -1
  110. package/dist/pty.js +20 -7
  111. package/dist/pty.js.map +1 -1
  112. package/dist/recovery-cloud-receipt.d.ts.map +1 -1
  113. package/dist/recovery-cloud-receipt.js +2 -1
  114. package/dist/recovery-cloud-receipt.js.map +1 -1
  115. package/dist/recovery-cloud-workspace.d.ts.map +1 -1
  116. package/dist/recovery-cloud-workspace.js +2 -1
  117. package/dist/recovery-cloud-workspace.js.map +1 -1
  118. package/dist/recovery-cloud.d.ts.map +1 -1
  119. package/dist/recovery-cloud.js +8 -9
  120. package/dist/recovery-cloud.js.map +1 -1
  121. package/dist/recovery-copilot.d.ts.map +1 -1
  122. package/dist/recovery-copilot.js +3 -2
  123. package/dist/recovery-copilot.js.map +1 -1
  124. package/dist/recovery-desktop.d.ts.map +1 -1
  125. package/dist/recovery-desktop.js +3 -2
  126. package/dist/recovery-desktop.js.map +1 -1
  127. package/dist/recovery-hooks.js +2 -2
  128. package/dist/recovery-hooks.js.map +1 -1
  129. package/dist/recovery-hosts.d.ts.map +1 -1
  130. package/dist/recovery-hosts.js +53 -6
  131. package/dist/recovery-hosts.js.map +1 -1
  132. package/dist/recovery-polytoken.d.ts +2 -2
  133. package/dist/recovery-polytoken.d.ts.map +1 -1
  134. package/dist/recovery-polytoken.js +13 -8
  135. package/dist/recovery-polytoken.js.map +1 -1
  136. package/dist/recovery-remote.js +3 -3
  137. package/dist/recovery-remote.js.map +1 -1
  138. package/dist/recovery-state.d.ts +22 -1
  139. package/dist/recovery-state.d.ts.map +1 -1
  140. package/dist/recovery-state.js +167 -31
  141. package/dist/recovery-state.js.map +1 -1
  142. package/dist/recovery-transfer.d.ts.map +1 -1
  143. package/dist/recovery-transfer.js +8 -4
  144. package/dist/recovery-transfer.js.map +1 -1
  145. package/dist/recovery.d.ts.map +1 -1
  146. package/dist/recovery.js +30 -11
  147. package/dist/recovery.js.map +1 -1
  148. package/dist/reliability-store.d.ts +13 -0
  149. package/dist/reliability-store.d.ts.map +1 -1
  150. package/dist/reliability-store.js +110 -8
  151. package/dist/reliability-store.js.map +1 -1
  152. package/dist/remember.d.ts.map +1 -1
  153. package/dist/remember.js +2 -2
  154. package/dist/remember.js.map +1 -1
  155. package/dist/retained-context.d.ts.map +1 -1
  156. package/dist/retained-context.js +3 -2
  157. package/dist/retained-context.js.map +1 -1
  158. package/dist/retrieval-task.d.ts +30 -0
  159. package/dist/retrieval-task.d.ts.map +1 -1
  160. package/dist/retrieval-task.js +98 -2
  161. package/dist/retrieval-task.js.map +1 -1
  162. package/dist/review.d.ts +1 -1
  163. package/dist/review.d.ts.map +1 -1
  164. package/dist/review.js +5 -2
  165. package/dist/review.js.map +1 -1
  166. package/dist/runtime.d.ts +4 -1
  167. package/dist/runtime.d.ts.map +1 -1
  168. package/dist/runtime.js +138 -91
  169. package/dist/runtime.js.map +1 -1
  170. package/dist/self-host.d.ts +1 -1
  171. package/dist/self-host.d.ts.map +1 -1
  172. package/dist/self-host.js +105 -23
  173. package/dist/self-host.js.map +1 -1
  174. package/dist/share.d.ts.map +1 -1
  175. package/dist/share.js +2 -1
  176. package/dist/share.js.map +1 -1
  177. package/dist/signed-snapshot.d.ts +7 -0
  178. package/dist/signed-snapshot.d.ts.map +1 -1
  179. package/dist/signed-snapshot.js +27 -0
  180. package/dist/signed-snapshot.js.map +1 -1
  181. package/dist/skill-command.d.ts +5 -2
  182. package/dist/skill-command.d.ts.map +1 -1
  183. package/dist/skill-command.js +73 -10
  184. package/dist/skill-command.js.map +1 -1
  185. package/dist/skill-guidance.js +1 -1
  186. package/dist/skill-guidance.js.map +1 -1
  187. package/dist/skills.d.ts +2 -0
  188. package/dist/skills.d.ts.map +1 -1
  189. package/dist/skills.js +16 -11
  190. package/dist/skills.js.map +1 -1
  191. package/dist/status-health.d.ts +26 -0
  192. package/dist/status-health.d.ts.map +1 -0
  193. package/dist/status-health.js +49 -0
  194. package/dist/status-health.js.map +1 -0
  195. package/dist/telemetry.d.ts.map +1 -1
  196. package/dist/telemetry.js +2 -2
  197. package/dist/telemetry.js.map +1 -1
  198. package/dist/terminal-output.d.ts +76 -0
  199. package/dist/terminal-output.d.ts.map +1 -0
  200. package/dist/terminal-output.js +151 -0
  201. package/dist/terminal-output.js.map +1 -0
  202. package/dist/uninstall.d.ts.map +1 -1
  203. package/dist/uninstall.js +14 -32
  204. package/dist/uninstall.js.map +1 -1
  205. package/dist/update.d.ts +17 -1
  206. package/dist/update.d.ts.map +1 -1
  207. package/dist/update.js +150 -51
  208. package/dist/update.js.map +1 -1
  209. package/dist/windows-permissions.d.ts +6 -0
  210. package/dist/windows-permissions.d.ts.map +1 -1
  211. package/dist/windows-permissions.js +22 -2
  212. package/dist/windows-permissions.js.map +1 -1
  213. package/dist/windows-pty.d.ts.map +1 -1
  214. package/dist/windows-pty.js +4 -3
  215. package/dist/windows-pty.js.map +1 -1
  216. package/package.json +4 -4
package/dist/runtime.js CHANGED
@@ -2,10 +2,13 @@ import { redactSecrets } from "./runtime-redaction.js";
2
2
  export { redactSecrets } from "./runtime-redaction.js";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { CAPTURE_CONFLICT_MESSAGES } from "@lore-co/core/capture-conflict";
5
- import { approachGuidance } from "./approach-guidance.js";
5
+ import { guardItemLine } from "@lore-co/core/learning-line";
6
+ import { fitSkillContext } from "@lore-co/core/skill-context";
7
+ import { systemCommand } from "@lore-co/core/system-command";
8
+ import { approachGuidance, approachWorkflowRequested } from "./approach-guidance.js";
6
9
  import { skillGuidance } from "./skill-guidance.js";
7
10
  import { retainedContextTexts, retainedReceiptIds, rememberDeliveredContext } from "./retained-context.js";
8
- import { clearRetrievalTask, resolveRetrievalTask } from "./retrieval-task.js";
11
+ import { clearRetrievalTask, forgetUncapturedPromptText, resolveRetrievalTask, unsavedRetrievalTask } from "./retrieval-task.js";
9
12
  import { access, chmod, mkdir, readFile, stat, } from "node:fs/promises";
10
13
  import { constants as fsConstants, realpathSync } from "node:fs";
11
14
  import { execFile as execFileCallback } from "node:child_process";
@@ -16,10 +19,10 @@ import { promisify } from "node:util";
16
19
  import { boundedUtf8Text, defaultGitPushBranches, repositoryRemoteScope, repositoryScopeFromGitRoot, } from "./repository.js";
17
20
  import { ReliabilityStore, ReliabilityStoreError, atomicWriteJson as durableAtomicWriteJson, classifyRetryFailure, createIntegrationInvocationAttempt, durableUnlink, recordLocalGuardMetric, recordLocalRetrievalMetric, } from "./reliability-store.js";
18
21
  import { writeInvocationAttemptBounded, writeInvocationCompletionBounded, } from "./invocation-health-writer.js";
19
- import { SignedSnapshotError, verifySignedSnapshot, } from "./signed-snapshot.js";
22
+ import { SignedSnapshotError, referencesSourceConversations, verifySignedSnapshot, } from "./signed-snapshot.js";
20
23
  import { CONTEXT_CACHE_MAX_STALE_MS, isContextTransportFailure, selectCachedContext, } from "./context-fallback.js";
21
24
  import { RUNTIME_VERSION } from "./runtime-version.js";
22
- import { checkLoreAccess, checkDirectory, checkWorkspaceRepository, readLocalConfig, resolveLocalConnection, bindLocalSession } from "./directory-policy.js";
25
+ import { checkLoreAccess, checkDirectory, checkWorkspaceRepository, readLocalConfig, resolveLocalConnection, bindLocalSession, loreDisabled, RECAP_REMINDER } from "./directory-policy.js";
23
26
  import { capturePendingImages, enqueueEventImages, flushImageAttachment, hostMessageImages, rememberImageCapture } from "./image-capture.js";
24
27
  import { guardRepositoryOverride, guardRuleConflict, guardToolTrigger, supportedGuardRule, guardEmergencyCommand, emergencyOverrideApplies } from "@lore-co/core/guard-evaluation";
25
28
  const IS_STANDALONE_RUNTIME = typeof __LORE_STANDALONE__ === "boolean" && __LORE_STANDALONE__;
@@ -368,6 +371,7 @@ async function readRuntimeConfig(home, cwd) {
368
371
  agents,
369
372
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
370
373
  capture: saved?.capture !== "off",
374
+ ...(saved?.recapReminder === "off" ? { recapReminder: false } : {}),
371
375
  };
372
376
  }
373
377
  catch {
@@ -479,7 +483,7 @@ async function repositoryScope(cwd, repository = repositoryRemoteScope) {
479
483
  }
480
484
  async function gitOutput(root, args) {
481
485
  try {
482
- const result = await execFile("git", ["-C", root, ...args], {
486
+ const result = await execFile(systemCommand("git", { exclude: [root] }), ["-C", root, ...args], {
483
487
  encoding: "utf8",
484
488
  timeout: 500,
485
489
  maxBuffer: MAX_GIT_DIFF_BYTES * 2,
@@ -612,7 +616,11 @@ function createPromptObservationRequest(input, agent, sessionId, now, gitContext
612
616
  connector: "lore-cli",
613
617
  agent,
614
618
  sessionId,
615
- eventId: deterministicUuid(`lore-prompt\0${agent}\0${sessionId}\0${promptId ?? idempotencyKey}`),
619
+ // Hosts keep one prompt or turn ID for messages queued while the agent is
620
+ // still working, so the ID alone does not identify a prompt. The key also
621
+ // covers the text: a repeated hook keeps its event and a queued message
622
+ // gets its own, instead of conflicting with the first one.
623
+ eventId: deterministicUuid(`lore-prompt\0${agent}\0${sessionId}\0${idempotencyKey}`),
616
624
  prompt: redactSecrets(prompt),
617
625
  ...(promptId === undefined ? {} : { promptId }),
618
626
  timestamp: now.toISOString(),
@@ -804,8 +812,10 @@ function deliveryFromResponse(value) {
804
812
  context: boundedContext(context),
805
813
  ...(typeof value.relatedContext === "string" ? { relatedContext: redactSecrets(value.relatedContext).slice(0, 2_500) }
806
814
  : isObject(value.context) && typeof value.context.relatedContext === "string" ? { relatedContext: redactSecrets(value.context.relatedContext).slice(0, 2_500) } : {}),
807
- ...(typeof value.skillContext === "string" && value.skillContext.length <= 72_000 ? { skillContext: redactSecrets(value.skillContext) }
808
- : isObject(value.context) && typeof value.context.skillContext === "string" && value.context.skillContext.length <= 72_000 ? { skillContext: redactSecrets(value.context.skillContext) } : {}),
815
+ ...(typeof value.skillContext === "string" ? { skillContext: fitSkillContext(redactSecrets(value.skillContext)) }
816
+ : isObject(value.context) && typeof value.context.skillContext === "string" ? { skillContext: fitSkillContext(redactSecrets(value.context.skillContext)) } : {}),
817
+ ...(typeof value.approachContext === "string" ? { approachContext: redactSecrets(value.approachContext).slice(0, 600) }
818
+ : isObject(value.context) && typeof value.context.approachContext === "string" ? { approachContext: redactSecrets(value.context.approachContext).slice(0, 600) } : {}),
809
819
  completeContext: context.length <= MAX_CONTEXT_CHARS,
810
820
  ...(receiptId === undefined ? {} : { receiptId }),
811
821
  learned,
@@ -918,6 +928,7 @@ function deliveryFromSignedContext(payload, expectedRequest) {
918
928
  context: payload.context,
919
929
  relatedContext: payload.relatedContext,
920
930
  skillContext: payload.skillContext,
931
+ approachContext: payload.approachContext,
921
932
  receipt: { id: payload.receiptId },
922
933
  memories: payload.memories,
923
934
  hits: payload.hits,
@@ -1180,6 +1191,7 @@ async function postTurn(config, request, fetchImplementation, store) {
1180
1191
  headers: {
1181
1192
  "x-lore-context": "related-work-v1",
1182
1193
  "x-lore-skills": "skills-v1",
1194
+ "x-lore-approaches": "approaches-v1",
1183
1195
  accept: runtimeAccept(config),
1184
1196
  authorization: `Bearer ${config.token}`,
1185
1197
  "content-type": "application/json",
@@ -1249,11 +1261,13 @@ async function postTurn(config, request, fetchImplementation, store) {
1249
1261
  task: (request.task ?? request.currentUser.content).trim(),
1250
1262
  },
1251
1263
  });
1252
- await store
1253
- .writeContextSnapshot(request.eventId, {
1254
- compactJws: value.contextSnapshot,
1255
- })
1256
- .catch(() => undefined);
1264
+ if (!referencesSourceConversations(payload)) {
1265
+ await store
1266
+ .writeContextSnapshot(request.eventId, {
1267
+ compactJws: value.contextSnapshot,
1268
+ })
1269
+ .catch(() => undefined);
1270
+ }
1257
1271
  return { ...delivery, reliability };
1258
1272
  }
1259
1273
  async function postPromptObservation(config, request, fetchImplementation, _store) {
@@ -1334,6 +1348,7 @@ async function getPromptContext(config, agent, sessionId, sourceEventId, prompt,
1334
1348
  headers: {
1335
1349
  "x-lore-context": "related-work-v1",
1336
1350
  "x-lore-skills": "skills-v1",
1351
+ "x-lore-approaches": "approaches-v1",
1337
1352
  accept: runtimeAccept(config),
1338
1353
  authorization: `Bearer ${config.token}`,
1339
1354
  "content-type": "application/json",
@@ -1388,9 +1403,13 @@ async function getPromptContext(config, agent, sessionId, sourceEventId, prompt,
1388
1403
  };
1389
1404
  }
1390
1405
  const delivery = deliveryFromSignedContext(payload, deliveryRequest);
1391
- await store
1392
- .writeContextSnapshot(sourceEventId, { compactJws: value.snapshot })
1393
- .catch(() => undefined);
1406
+ // The signed snapshot carries the request, prompt included, so capture off
1407
+ // caches nothing. Offline fallback uses context cached while capture was on.
1408
+ if (config.capture && !referencesSourceConversations(payload)) {
1409
+ await store
1410
+ .writeContextSnapshot(sourceEventId, { compactJws: value.snapshot })
1411
+ .catch(() => undefined);
1412
+ }
1394
1413
  return {
1395
1414
  ...delivery,
1396
1415
  reliability,
@@ -1437,8 +1456,9 @@ async function getCachedPromptContext(config, prompt, gitContext, store, now, re
1437
1456
  now,
1438
1457
  }).payload;
1439
1458
  // Signed payloads cannot be redacted in place. Retire legacy snapshots
1440
- // containing raw request secrets and let a later live request replace them.
1441
- if (!hasRedactedContextRequest(payload)) {
1459
+ // containing raw request secrets or source conversations and let a
1460
+ // later live request replace them.
1461
+ if (!hasRedactedContextRequest(payload) || referencesSourceConversations(payload)) {
1442
1462
  await store.deleteContextSnapshot(record.cacheKey, value.compactJws);
1443
1463
  continue;
1444
1464
  }
@@ -1740,7 +1760,7 @@ function receiptMessage(config, agent, delivery) {
1740
1760
  const path = delivery.receiptId === undefined
1741
1761
  ? learned === undefined
1742
1762
  ? undefined
1743
- : `/memories/${encodeURIComponent(learned.id)}`
1763
+ : `/learnings/${encodeURIComponent(learned.id)}`
1744
1764
  : `/receipts/${encodeURIComponent(delivery.receiptId)}`;
1745
1765
  if (path !== undefined) {
1746
1766
  parts.push(`${learned?.status === "proposed" ? "Review proposal" : "Review or forget"}: ${config.dashboardUrl}${path}`);
@@ -1817,23 +1837,13 @@ function guardResultFromResponse(value) {
1817
1837
  if (!isObject(item) || typeof item.content !== "string") {
1818
1838
  return [];
1819
1839
  }
1820
- const scope = isObject(item.scope) ? item.scope : {};
1821
- const source = isObject(item.source) ? item.source : {};
1822
- const explanation = guardExplanationFromValue(item.explanation);
1823
1840
  return [
1824
1841
  {
1825
1842
  content: redactSecrets(item.content),
1826
1843
  enforcement: typeof item.enforcement === "string"
1827
1844
  ? item.enforcement
1828
1845
  : "advisory",
1829
- ...(typeof scope.repo === "string" ? { repo: scope.repo } : {}),
1830
- ...(typeof scope.organization === "string"
1831
- ? { organization: scope.organization }
1832
- : {}),
1833
- ...(typeof source.agent === "string"
1834
- ? { sourceAgent: source.agent }
1835
- : {}),
1836
- ...(explanation === undefined ? {} : { explanation }),
1846
+ scope: guardItemScope(item),
1837
1847
  },
1838
1848
  ];
1839
1849
  })
@@ -1856,48 +1866,23 @@ function guardResultFromResponse(value) {
1856
1866
  conflictSummaries,
1857
1867
  };
1858
1868
  }
1859
- function guardExplanationFromValue(value) {
1860
- if (!isObject(value) ||
1861
- typeof value.summary !== "string" ||
1862
- !isObject(value.provenance) ||
1863
- typeof value.provenance.agent !== "string" ||
1864
- typeof value.status !== "string" ||
1865
- !isObject(value.freshness) ||
1866
- typeof value.freshness.updatedAt !== "string") {
1867
- return undefined;
1868
- }
1869
- const scope = isObject(value.scope) ? value.scope : {};
1870
- const scopeParts = [
1871
- typeof scope.organization === "string"
1872
- ? `org ${scope.organization}`
1873
- : undefined,
1874
- typeof scope.project === "string" ? `project ${scope.project}` : undefined,
1875
- typeof scope.repo === "string" ? `repo ${scope.repo}` : undefined,
1876
- typeof scope.path === "string" ? `path ${scope.path}` : undefined,
1877
- typeof scope.component === "string"
1878
- ? `component ${scope.component}`
1879
- : undefined,
1880
- ].filter((part) => part !== undefined);
1881
- const session = typeof value.provenance.sessionId === "string"
1882
- ? ` session ${value.provenance.sessionId}`
1883
- : "";
1884
- return `Why: ${redactSecrets(value.summary)} Source: ${value.provenance.agent}${session}. Scope: ${scopeParts.join(" · ") || "workspace-wide"}. Status: ${value.status}. Updated: ${value.freshness.updatedAt.slice(0, 10)}.`;
1885
- }
1886
- function guardItemSource(item) {
1887
- const scope = item.repo !== undefined
1888
- ? `${item.repo} repo`
1889
- : item.organization !== undefined
1890
- ? "organization rule"
1891
- : "workspace knowledge";
1892
- return item.sourceAgent === undefined
1893
- ? scope
1894
- : `${scope} · ${item.sourceAgent} session`;
1895
- }
1896
- function guardAssistContext(result) {
1869
+ /** Where the item applies, as the explanation (or the item) reports it. */
1870
+ function guardItemScope(item) {
1871
+ const scope = isObject(item.explanation) && isObject(item.explanation.scope)
1872
+ ? item.explanation.scope
1873
+ : isObject(item.scope)
1874
+ ? item.scope
1875
+ : {};
1876
+ return {
1877
+ ...(typeof scope.repo === "string" ? { repo: scope.repo } : {}),
1878
+ ...(typeof scope.path === "string" ? { path: scope.path } : {}),
1879
+ ...(typeof scope.component === "string" ? { component: scope.component } : {}),
1880
+ };
1881
+ }
1882
+ function guardAssistContext(result, repo) {
1897
1883
  const lines = ["Relevant Lore context:"];
1898
1884
  for (const item of result.items) {
1899
- lines.push(`- ${item.content}`);
1900
- lines.push(` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`);
1885
+ lines.push(`- ${guardItemLine(item, repo)}`);
1901
1886
  }
1902
1887
  if (result.conflictSummaries.length > 0) {
1903
1888
  lines.push("", "Lore policy notes:");
@@ -1907,7 +1892,7 @@ function guardAssistContext(result) {
1907
1892
  }
1908
1893
  return boundedContext(lines.join("\n"));
1909
1894
  }
1910
- function guardConfirmationReason(result) {
1895
+ function guardConfirmationReason(result, repo) {
1911
1896
  if (result.policyUnavailable)
1912
1897
  return "Lore Guard cannot evaluate an existing required rule. An owner or admin must rewrite it using a supported prohibition or change it to advisory before retrying.";
1913
1898
  const required = result.items.filter((item) => item.enforcement === "required");
@@ -1915,10 +1900,7 @@ function guardConfirmationReason(result) {
1915
1900
  const lines = [
1916
1901
  result.items.length === 0 ? "Lore Guard cannot verify the enforcement policy." : "Lore Guard: this action conflicts with a required rule.",
1917
1902
  ...result.conflictSummaries,
1918
- ...shown.flatMap((item) => [
1919
- `- ${item.content}`,
1920
- ` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`,
1921
- ]),
1903
+ ...shown.map((item) => `- ${guardItemLine(item, repo)}`),
1922
1904
  ];
1923
1905
  lines.push(result.offline && result.confirmCommand !== undefined
1924
1906
  ? `Restore Lore or run: ${result.confirmCommand}`
@@ -2009,7 +1991,7 @@ async function offlineGuard(config, store, request, now, previouslyEnforced, all
2009
1991
  }) !== null) : [];
2010
1992
  return {
2011
1993
  offline: true, mode: "enforce", requiresConfirmation: rules.length > 0,
2012
- items: rules.map((rule) => ({ content: String(rule.content), enforcement: "required" })),
1994
+ items: rules.map((rule) => ({ content: String(rule.content), enforcement: "required", scope: guardItemScope(rule) })),
2013
1995
  conflictSummaries: ["Lore is offline; this action was checked against its verified, unexpired saved policy."],
2014
1996
  confirmCommand: guardEmergencyCommand(String(request.sessionId), String(request.repo ?? "")),
2015
1997
  };
@@ -2131,7 +2113,7 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now, c
2131
2113
  // independently of `requiresConfirmation`: the producer reports it via
2132
2114
  // `policyIssue` rather than `confirm`, so it never sets `requiresConfirmation`.
2133
2115
  if (result.requiresConfirmation || result.policyUnavailable) {
2134
- const reason = guardConfirmationReason(result);
2116
+ const reason = guardConfirmationReason(result, scope?.repo);
2135
2117
  if (agent === "polytoken")
2136
2118
  return { outcome: "deny", reason };
2137
2119
  if (agent === "copilot-cli") {
@@ -2166,8 +2148,11 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now, c
2166
2148
  // context was already added by userPromptTransformed.
2167
2149
  return undefined;
2168
2150
  }
2169
- if (agent === "polytoken")
2170
- return { outcome: "allow" };
2151
+ if (agent === "polytoken") {
2152
+ // Like every other host, Polytoken gets no verdict on advisory results:
2153
+ // Lore only ever blocks, it never approves an action.
2154
+ return undefined;
2155
+ }
2171
2156
  if (agent === "cursor") {
2172
2157
  // Cursor cannot inject agent context at the shell boundary on allow.
2173
2158
  return undefined;
@@ -2175,11 +2160,14 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now, c
2175
2160
  return {
2176
2161
  hookSpecificOutput: {
2177
2162
  hookEventName: "PreToolUse",
2178
- additionalContext: guardAssistContext(result),
2163
+ additionalContext: guardAssistContext(result, scope?.repo),
2179
2164
  },
2180
2165
  };
2181
2166
  }
2182
2167
  export async function handleHookEvent(value, agent, options = {}) {
2168
+ // LORE_DISABLE: the same empty answer as an unconnected machine, for every host and event.
2169
+ if (loreDisabled(options.environment ?? process.env))
2170
+ return undefined;
2183
2171
  if (!isObject(value)) {
2184
2172
  throw new InvalidHookInputError();
2185
2173
  }
@@ -2255,6 +2243,8 @@ export async function handleHookEvent(value, agent, options = {}) {
2255
2243
  : "Before answering each user request or using other tools, call lore_context with that sessionId and the full user message as prompt. This retrieves relevant shared knowledge; automatic capture is paused, so nothing is recorded.",
2256
2244
  "Use the returned context when relevant. If retrieval fails, report that limitation; do not invent shared knowledge.",
2257
2245
  ...(capture ? ["After completing each response, call lore_complete with the same sessionId and a concise response summary so the next user correction retains the prior attempt. These Lore calls are available through MCP and require no shell commands."] : ["These Lore calls are available through MCP and require no shell commands."]),
2246
+ // Cursor's session start runs once per conversation, so no marker is needed.
2247
+ ...(recapReminderApplies(agent, capture, config) ? [RECAP_REMINDER] : []),
2258
2248
  ].join("\n") };
2259
2249
  }
2260
2250
  if (eventName === "Stop" || eventName === "AssistantResponse") {
@@ -2323,27 +2313,69 @@ export async function handleHookEvent(value, agent, options = {}) {
2323
2313
  };
2324
2314
  }
2325
2315
  }
2316
+ const promptConfig = { ...config, capture };
2317
+ // Once per store: remove prompt text earlier versions kept with capture off.
2318
+ await runtimeStore(config, options.home)
2319
+ .then(store => forgetUncapturedPromptText(store, capture, now, CURSOR_SESSION_PREFIX))
2320
+ .catch(() => undefined);
2326
2321
  if (agent === "cursor") {
2327
2322
  const store = await runtimeStore(config, options.home);
2328
2323
  const existing = await store.readState(cursorSessionKey(sessionId));
2324
+ const { prompt: _prompt, ...kept } = isObject(existing) ? existing : {};
2329
2325
  const cwd = stringField(input.cwd);
2326
+ // Capture off keeps no prompt: lore_context then searches with the
2327
+ // message the agent passes it.
2330
2328
  if (cwd)
2331
2329
  await store.writeState(cursorSessionKey(sessionId), {
2332
- ...(isObject(existing) ? existing : {}), sessionId, cwd,
2333
- ...(stringField(input.prompt) ? { prompt: redactSecrets(String(input.prompt)) } : {}),
2330
+ ...kept, sessionId, cwd,
2331
+ ...(capture && stringField(input.prompt) ? { prompt: redactSecrets(String(input.prompt)) } : {}),
2334
2332
  ...(stringField(input.prompt_id) ? { promptId: input.prompt_id } : {}), updatedAt: now.toISOString(),
2335
2333
  });
2336
2334
  // Cursor's prompt hook supports submission control only. The MCP call
2337
2335
  // delivers the context; capture here retains native editor event identity.
2338
2336
  // Retrieve is skipped so the notice cooldown is not primed by a discarded
2339
2337
  // result: the MCP `lore_context` call surfaces the reliability notice.
2340
- await handlePromptEvent(input, agent, sessionId, config, options, now, capture, false);
2338
+ await handlePromptEvent(input, agent, sessionId, promptConfig, options, now, capture, false);
2341
2339
  return { continue: true };
2342
2340
  }
2343
- return handlePromptEvent(input, agent, sessionId, config, options, now, capture);
2341
+ return handlePromptEvent(input, agent, sessionId, promptConfig, options, now, capture, true, recapReminderApplies(agent, capture, config));
2342
+ }
2343
+ export { RECAP_REMINDER };
2344
+ /** Hosts where `lore connect` installs the recap skill and this runtime delivers context. */
2345
+ const RECAP_HOSTS = new Set(["claude", "codex", "copilot-cli", "copilot-vscode", "cursor"]);
2346
+ function recapReminderApplies(agent, capture, config) {
2347
+ return !capture && config.recapReminder !== false && RECAP_HOSTS.has(agent);
2348
+ }
2349
+ /** True for the first prompt of a session only. The marker is a hash of the session, never the prompt. */
2350
+ /** True once this session asked to save an approach. The marker is a hash of the session, never the prompt. */
2351
+ async function approachWorkflowStarted(store, agent, sessionId, now, requested) {
2352
+ const key = `approach-workflow-${sha256(`${agent}\0${sessionId}`)}`;
2353
+ try {
2354
+ if (!requested)
2355
+ return await store.readState(key) !== null;
2356
+ await store.updateState(key, (current) => current ?? { at: now.toISOString() }, now, { recoverCorrupt: true });
2357
+ }
2358
+ catch {
2359
+ // An unreadable marker only costs the reminder after compaction.
2360
+ }
2361
+ return requested;
2362
+ }
2363
+ async function claimRecapReminder(store, agent, sessionId, now) {
2364
+ let first = false;
2365
+ try {
2366
+ await store.updateState(`recap-reminder-${sha256(`${agent}\0${sessionId}`)}`, (current) => {
2367
+ first = current === null;
2368
+ return current ?? { at: now.toISOString() };
2369
+ }, now, { recoverCorrupt: true });
2370
+ }
2371
+ catch {
2372
+ return false;
2373
+ }
2374
+ return first;
2344
2375
  }
2376
+ const CURSOR_SESSION_PREFIX = "cursor-session-";
2345
2377
  function cursorSessionKey(sessionId) {
2346
- return `cursor-session-${sha256(sessionId)}`;
2378
+ return `${CURSOR_SESSION_PREFIX}${sha256(sessionId)}`;
2347
2379
  }
2348
2380
  /** MCP requests are bound to a session/workspace observed by the native hook. */
2349
2381
  async function cursorSession(sessionId, options) {
@@ -2371,7 +2403,7 @@ async function cursorSession(sessionId, options) {
2371
2403
  }
2372
2404
  // Same definition as the native hooks: local mode and the dashboard switches.
2373
2405
  const capture = config.capture && access.capturePausedBy === undefined;
2374
- return { config, store, state, cwd: stringField(state.cwd), capture };
2406
+ return { config: { ...config, capture }, store, state, cwd: stringField(state.cwd), capture };
2375
2407
  }
2376
2408
  export async function getCursorContext(sessionId, prompt, options = {}) {
2377
2409
  const { config, state, cwd, capture } = await cursorSession(sessionId, options);
@@ -2390,7 +2422,7 @@ export async function completeCursorResponse(sessionId, response, options = {})
2390
2422
  const { prompt: _prompt, promptId: _promptId, ...rest } = state;
2391
2423
  await store.writeState(cursorSessionKey(sessionId), { ...rest, updatedAt: now.toISOString() });
2392
2424
  }
2393
- async function handlePromptEvent(input, agent, sessionId, config, options, now, capture = true, retrieve = true) {
2425
+ async function handlePromptEvent(input, agent, sessionId, config, options, now, capture = true, retrieve = true, recapReminder = false) {
2394
2426
  const fetchImplementation = options.fetch ?? globalThis.fetch;
2395
2427
  const prompt = stringField(input.prompt);
2396
2428
  if (prompt === undefined) {
@@ -2466,11 +2498,14 @@ async function handlePromptEvent(input, agent, sessionId, config, options, now,
2466
2498
  };
2467
2499
  const lineage = await readLineageMetadata(options.home, options.environment ?? process.env);
2468
2500
  let delivery = emptyDelivery();
2469
- const retrievalTask = (repo) => resolveRetrievalTask(store, {
2501
+ const retrievalSession = (repo) => ({
2470
2502
  agent, sessionId, connectionKey: credentialReliabilityWorkspaceKey(config),
2471
2503
  cwd: stringField(input.cwd), repo,
2472
- }, prompt, now, pending?.cwd === stringField(input.cwd) && pending
2473
- ? { content: pending.assistantMessage, capturedAt: pending.capturedAt } : undefined);
2504
+ });
2505
+ const retrievalTask = (repo) => !config.capture
2506
+ ? unsavedRetrievalTask(store, retrievalSession(repo), prompt)
2507
+ : resolveRetrievalTask(store, retrievalSession(repo), prompt, now, pending?.cwd === stringField(input.cwd) && pending
2508
+ ? { content: pending.assistantMessage, capturedAt: pending.capturedAt } : undefined);
2474
2509
  if (pending !== null) {
2475
2510
  let receivedTurnContext = false;
2476
2511
  let request = await createTurnRequest(input, agent, pending, now, lineage);
@@ -2648,8 +2683,11 @@ async function handlePromptEvent(input, agent, sessionId, config, options, now,
2648
2683
  await rememberDeliveredContext(store, contextSession, delivery).catch(() => undefined);
2649
2684
  const relatedContext = delivery.relatedContext && !retainedContext.some(text => text.includes(delivery.relatedContext))
2650
2685
  ? delivery.relatedContext : undefined;
2686
+ const approachContext = delivery.approachContext && !retainedContext.some(text => text.includes(delivery.approachContext))
2687
+ ? delivery.approachContext : undefined;
2651
2688
  // Skills are fresh on every turn, even when learning context is retained.
2652
- const taskContext = [delivery.skillContext, delivery.context, relatedContext, delivery.reliability?.status === "failed" ? undefined : approachGuidance(prompt, agent, redactSecrets(sessionId), retainedContext), delivery.skillContext === undefined && delivery.reliability?.status !== "failed" ? skillGuidance(retainedContext) : undefined].filter(Boolean).join("\n\n");
2689
+ const taskContext = [delivery.skillContext, delivery.context, relatedContext, approachContext, delivery.reliability?.status === "failed" ? undefined : approachGuidance(prompt, agent, redactSecrets(sessionId), retainedContext, await approachWorkflowStarted(store, agent, sessionId, now, approachWorkflowRequested(prompt))), delivery.skillContext === undefined && delivery.reliability?.status !== "failed" ? skillGuidance(retainedContext) : undefined,
2690
+ recapReminder && await claimRecapReminder(store, agent, sessionId, now) ? RECAP_REMINDER : undefined].filter(Boolean).join("\n\n");
2653
2691
  const systemMessage = agent === "cursor" || agent === "polytoken"
2654
2692
  ? undefined
2655
2693
  : [receipt, reliabilityNotice].filter(Boolean).join(" · ") || undefined;
@@ -2738,6 +2776,15 @@ export async function runHook(args = process.argv.slice(2)) {
2738
2776
  if (agent === null) {
2739
2777
  return;
2740
2778
  }
2779
+ if (loreDisabled(process.env)) {
2780
+ // Nothing is read, sent, or written, not even invocation health. Drain the
2781
+ // payload so the host never sees a broken pipe, then exit 0 with no output.
2782
+ try {
2783
+ for await (const _chunk of process.stdin) { /* discard */ }
2784
+ }
2785
+ catch { /* nothing to report */ }
2786
+ return;
2787
+ }
2741
2788
  let tracker = null;
2742
2789
  let input;
2743
2790
  try {