hippo-memory 1.62.0 → 1.63.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.
Files changed (370) hide show
  1. package/README.md +9 -0
  2. package/dist/agent-memories/apply.d.ts +2 -0
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +2 -1
  5. package/dist/agent-memories/claude-code.js +4 -4
  6. package/dist/agent-memories/report.d.ts +1 -0
  7. package/dist/agent-memories/report.js +2 -2
  8. package/dist/agent-memories/sync.js +37 -23
  9. package/dist/ambient-store.d.ts +4 -4
  10. package/dist/ambient-store.js +4 -3
  11. package/dist/api/assemble.d.ts +7 -10
  12. package/dist/api/assemble.js +7 -12
  13. package/dist/api/audit.d.ts +2 -2
  14. package/dist/api/audit.js +2 -2
  15. package/dist/api/auth.d.ts +6 -6
  16. package/dist/api/auth.js +5 -7
  17. package/dist/api/context-select.d.ts +4 -3
  18. package/dist/api/context-select.js +9 -11
  19. package/dist/api/context-types.d.ts +12 -17
  20. package/dist/api/context.d.ts +3 -2
  21. package/dist/api/context.js +20 -20
  22. package/dist/api/dormant.js +1 -1
  23. package/dist/api/drill-down.d.ts +4 -4
  24. package/dist/api/drill-down.js +22 -10
  25. package/dist/api/outcome.d.ts +8 -13
  26. package/dist/api/outcome.js +13 -17
  27. package/dist/api/promote.d.ts +5 -8
  28. package/dist/api/quarantine.js +3 -2
  29. package/dist/api/recall-types.d.ts +43 -55
  30. package/dist/api/recall.d.ts +3 -3
  31. package/dist/api/recall.js +7 -18
  32. package/dist/api/remember.d.ts +2 -2
  33. package/dist/api/sleep.d.ts +7 -35
  34. package/dist/api/sleep.js +3 -2
  35. package/dist/api/tokens.d.ts +2 -2
  36. package/dist/api/tokens.js +2 -2
  37. package/dist/api/types.d.ts +6 -10
  38. package/dist/api/types.js +2 -4
  39. package/dist/audit-prune.d.ts +4 -6
  40. package/dist/audit-prune.js +3 -5
  41. package/dist/audit.js +11 -33
  42. package/dist/auth.d.ts +8 -9
  43. package/dist/auth.js +5 -7
  44. package/dist/autolearn.d.ts +1 -1
  45. package/dist/autolearn.js +1 -1
  46. package/dist/availability.js +3 -5
  47. package/dist/capture/command.d.ts +5 -4
  48. package/dist/capture/command.js +8 -22
  49. package/dist/capture/compact.d.ts +1 -1
  50. package/dist/capture/compact.js +9 -13
  51. package/dist/capture/extract.js +37 -115
  52. package/dist/capture-error.d.ts +1 -1
  53. package/dist/capture-error.js +1 -1
  54. package/dist/churn-git.d.ts +1 -1
  55. package/dist/churn-git.js +1 -1
  56. package/dist/cli/audit.js +3 -4
  57. package/dist/cli/auth.js +3 -6
  58. package/dist/cli/curate.d.ts +1 -1
  59. package/dist/cli/curate.js +8 -24
  60. package/dist/cli/dag.js +5 -9
  61. package/dist/cli/decisions.js +10 -21
  62. package/dist/cli/explain.js +2 -1
  63. package/dist/cli/goals.js +1 -1
  64. package/dist/cli/init.js +1 -1
  65. package/dist/cli/playbooks.js +4 -9
  66. package/dist/cli/projects.js +3 -1
  67. package/dist/cli/recall.js +2 -1
  68. package/dist/cli/remember.js +6 -18
  69. package/dist/cli/session-hooks.js +21 -40
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -6
  72. package/dist/cli/shared.js +3 -3
  73. package/dist/cli/slack.js +1 -1
  74. package/dist/cli/sleep.js +27 -1
  75. package/dist/cli/status.d.ts +4 -4
  76. package/dist/cli/status.js +7 -12
  77. package/dist/cli/transfer.js +8 -13
  78. package/dist/cli/usage.js +9 -6
  79. package/dist/cli.d.ts +1 -1
  80. package/dist/cli.js +4 -9
  81. package/dist/compaction-record.d.ts +2 -0
  82. package/dist/compaction-record.js +89 -68
  83. package/dist/compare.d.ts +11 -16
  84. package/dist/compare.js +11 -16
  85. package/dist/config.d.ts +19 -18
  86. package/dist/config.js +90 -67
  87. package/dist/connectors/github/backfill.d.ts +2 -2
  88. package/dist/connectors/github/backfill.js +8 -15
  89. package/dist/connectors/github/cli-impl.js +3 -8
  90. package/dist/connectors/github/deletion.d.ts +5 -12
  91. package/dist/connectors/github/deletion.js +5 -12
  92. package/dist/connectors/github/dlq.d.ts +6 -9
  93. package/dist/connectors/github/dlq.js +2 -3
  94. package/dist/connectors/github/ingest.d.ts +5 -7
  95. package/dist/connectors/github/ingest.js +8 -12
  96. package/dist/connectors/github/octokit-client.d.ts +3 -5
  97. package/dist/connectors/github/octokit-client.js +5 -6
  98. package/dist/connectors/github/signature.d.ts +9 -39
  99. package/dist/connectors/github/signature.js +9 -39
  100. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  101. package/dist/connectors/github/tenant-routing.js +1 -1
  102. package/dist/connectors/github/transform.js +2 -2
  103. package/dist/connectors/github/types.d.ts +2 -10
  104. package/dist/connectors/github/types.js +1 -3
  105. package/dist/connectors/slack/deletion.d.ts +3 -8
  106. package/dist/connectors/slack/deletion.js +3 -8
  107. package/dist/connectors/slack/dlq.d.ts +1 -1
  108. package/dist/connectors/slack/ingest.d.ts +1 -1
  109. package/dist/connectors/slack/ingest.js +7 -16
  110. package/dist/connectors/slack/signature.d.ts +1 -1
  111. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  112. package/dist/connectors/slack/tenant-routing.js +3 -5
  113. package/dist/connectors/slack/transform.d.ts +5 -6
  114. package/dist/connectors/slack/transform.js +5 -6
  115. package/dist/connectors/slack/types.d.ts +2 -6
  116. package/dist/connectors/slack/types.js +1 -3
  117. package/dist/connectors/slack/web-client.js +10 -3
  118. package/dist/connectors/slack/workspaces.d.ts +3 -5
  119. package/dist/connectors/slack/workspaces.js +3 -5
  120. package/dist/consolidate/conflicts.js +3 -14
  121. package/dist/consolidate/decay.js +9 -29
  122. package/dist/consolidate/llm-passes.js +4 -5
  123. package/dist/consolidate/merge.js +8 -23
  124. package/dist/consolidate/run.d.ts +1 -8
  125. package/dist/consolidate/run.js +3 -25
  126. package/dist/consolidate/sleep.js +5 -17
  127. package/dist/consolidate/traces.js +9 -21
  128. package/dist/customer-notes.d.ts +5 -7
  129. package/dist/customer-notes.js +6 -9
  130. package/dist/dag.d.ts +10 -21
  131. package/dist/dag.js +23 -73
  132. package/dist/db/continuity.js +2 -2
  133. package/dist/db/migrations/v14.js +1 -1
  134. package/dist/db/migrations/v15.js +1 -2
  135. package/dist/db/migrations/v16.js +3 -4
  136. package/dist/db/migrations/v17.js +2 -3
  137. package/dist/db/migrations/v19.js +1 -1
  138. package/dist/db/migrations/v20.js +1 -1
  139. package/dist/db/migrations/v21.js +2 -6
  140. package/dist/db/migrations/v22.js +2 -4
  141. package/dist/db/migrations/v23.js +1 -1
  142. package/dist/db/migrations/v24.js +4 -6
  143. package/dist/db/migrations/v25.js +2 -3
  144. package/dist/db/migrations/v26.js +3 -3
  145. package/dist/db/migrations/v27.js +2 -10
  146. package/dist/db/migrations/v28.js +5 -8
  147. package/dist/db/migrations/v29.js +3 -4
  148. package/dist/db/migrations/v30.js +2 -2
  149. package/dist/db/migrations/v31.js +1 -1
  150. package/dist/db/migrations/v32.js +1 -1
  151. package/dist/db/migrations/v33.js +3 -3
  152. package/dist/db/migrations/v34.js +1 -1
  153. package/dist/db/migrations/v35.js +3 -4
  154. package/dist/db/migrations/v36.js +3 -4
  155. package/dist/db/migrations/v37.js +5 -5
  156. package/dist/db/migrations/v38.js +7 -8
  157. package/dist/db/migrations/v39.js +1 -1
  158. package/dist/db/migrations/v40.js +4 -16
  159. package/dist/db/migrations/v41.js +3 -4
  160. package/dist/db/migrations/v42.js +3 -4
  161. package/dist/db/migrations/v45.js +1 -1
  162. package/dist/db/migrations/v46.js +1 -1
  163. package/dist/db/migrations/v47.js +1 -1
  164. package/dist/db/migrations/v48.js +1 -1
  165. package/dist/decisions.d.ts +2 -2
  166. package/dist/decisions.js +6 -6
  167. package/dist/dedupe.js +86 -61
  168. package/dist/delivery-recorder.js +154 -135
  169. package/dist/doctor.js +119 -104
  170. package/dist/dormant.js +1 -4
  171. package/dist/embedding-provider.d.ts +4 -8
  172. package/dist/embedding-provider.js +4 -8
  173. package/dist/embeddings.js +55 -47
  174. package/dist/env.d.ts +1 -1
  175. package/dist/env.js +12 -12
  176. package/dist/escape.d.ts +5 -0
  177. package/dist/escape.js +10 -0
  178. package/dist/eval-stats.d.ts +1 -2
  179. package/dist/eval-stats.js +1 -2
  180. package/dist/eval-suite.js +27 -21
  181. package/dist/extract.js +4 -9
  182. package/dist/failure-log.d.ts +3 -3
  183. package/dist/failure-log.js +1 -1
  184. package/dist/forward-claim-detector.d.ts +2 -4
  185. package/dist/forward-claim-detector.js +6 -11
  186. package/dist/goals.d.ts +3 -3
  187. package/dist/goals.js +5 -6
  188. package/dist/graph/read.d.ts +2 -2
  189. package/dist/graph/read.js +5 -6
  190. package/dist/graph/types.d.ts +8 -8
  191. package/dist/graph/write.d.ts +7 -14
  192. package/dist/graph/write.js +16 -23
  193. package/dist/graph-extract.d.ts +7 -8
  194. package/dist/graph-extract.js +62 -72
  195. package/dist/graph-recall.d.ts +2 -2
  196. package/dist/graph-recall.js +55 -49
  197. package/dist/graph-stream.d.ts +5 -6
  198. package/dist/graph-stream.js +66 -57
  199. package/dist/graph-view.d.ts +2 -2
  200. package/dist/graph-view.js +7 -7
  201. package/dist/half-life-migration.d.ts +1 -2
  202. package/dist/half-life-migration.js +2 -3
  203. package/dist/hooks/codex-session.js +1 -1
  204. package/dist/hooks/codex-wrapper.d.ts +1 -1
  205. package/dist/hooks/codex-wrapper.js +3 -2
  206. package/dist/hooks/json-hooks.d.ts +2 -2
  207. package/dist/hooks/json-hooks.js +5 -4
  208. package/dist/hooks/opencode.d.ts +1 -1
  209. package/dist/hooks/opencode.js +5 -4
  210. package/dist/hooks/shared.d.ts +3 -7
  211. package/dist/hooks/shared.js +1 -8
  212. package/dist/http-util.d.ts +2 -3
  213. package/dist/http-util.js +3 -0
  214. package/dist/importers/core.d.ts +2 -9
  215. package/dist/importers/core.js +15 -30
  216. package/dist/importers/sources.js +2 -1
  217. package/dist/importers/vault.js +2 -20
  218. package/dist/incidents.d.ts +1 -1
  219. package/dist/incidents.js +1 -1
  220. package/dist/instruction-detect.d.ts +1 -1
  221. package/dist/instruction-detect.js +1 -1
  222. package/dist/invalidation.d.ts +3 -0
  223. package/dist/invalidation.js +160 -114
  224. package/dist/json.d.ts +5 -0
  225. package/dist/json.js +4 -0
  226. package/dist/judgment.js +1 -2
  227. package/dist/local-embedding.js +1 -1
  228. package/dist/mcp/admin-tools.js +7 -17
  229. package/dist/mcp/format.js +1 -1
  230. package/dist/mcp/framing.js +3 -6
  231. package/dist/mcp/protocol.d.ts +2 -5
  232. package/dist/mcp/protocol.js +1 -3
  233. package/dist/mcp/recall-tools.js +12 -15
  234. package/dist/mcp/request.js +4 -3
  235. package/dist/mcp/session-state.js +2 -3
  236. package/dist/mcp/stdio.js +2 -1
  237. package/dist/mcp/tools.js +9 -6
  238. package/dist/memory-value-weights.d.ts +5 -8
  239. package/dist/memory-value-weights.js +5 -8
  240. package/dist/memory-value.d.ts +13 -13
  241. package/dist/memory-value.js +26 -37
  242. package/dist/memory.d.ts +20 -22
  243. package/dist/memory.js +24 -48
  244. package/dist/multihop.d.ts +1 -1
  245. package/dist/multihop.js +3 -2
  246. package/dist/owner-validation.d.ts +4 -5
  247. package/dist/owner-validation.js +4 -5
  248. package/dist/physics.d.ts +4 -4
  249. package/dist/physics.js +7 -9
  250. package/dist/policies.d.ts +9 -10
  251. package/dist/policies.js +12 -14
  252. package/dist/postinstall.js +3 -6
  253. package/dist/predictions/planning-fallacy.d.ts +9 -14
  254. package/dist/predictions/planning-fallacy.js +10 -16
  255. package/dist/predictions/store.d.ts +15 -23
  256. package/dist/predictions/store.js +36 -33
  257. package/dist/processes.d.ts +2 -7
  258. package/dist/processes.js +3 -3
  259. package/dist/project-briefs.d.ts +2 -3
  260. package/dist/project-briefs.js +9 -13
  261. package/dist/project-identity.d.ts +22 -9
  262. package/dist/project-identity.js +47 -12
  263. package/dist/project-merge.d.ts +17 -2
  264. package/dist/project-merge.js +109 -27
  265. package/dist/project-remote.d.ts +12 -0
  266. package/dist/project-remote.js +138 -0
  267. package/dist/prompt-recall.js +1 -2
  268. package/dist/rate-limit.d.ts +1 -1
  269. package/dist/rate-limit.js +1 -1
  270. package/dist/raw-archive.d.ts +9 -0
  271. package/dist/raw-archive.js +70 -53
  272. package/dist/recall-history.d.ts +19 -20
  273. package/dist/recall-history.js +24 -42
  274. package/dist/recall-pipeline.js +4 -28
  275. package/dist/recall-scope.d.ts +7 -8
  276. package/dist/recall-scope.js +7 -8
  277. package/dist/recall-trace.d.ts +5 -9
  278. package/dist/recall-trace.js +6 -10
  279. package/dist/refine-llm.d.ts +1 -1
  280. package/dist/refine-llm.js +2 -2
  281. package/dist/reject-flow.d.ts +3 -4
  282. package/dist/reject-flow.js +122 -117
  283. package/dist/rejection.d.ts +5 -6
  284. package/dist/rejection.js +7 -15
  285. package/dist/rerankers/clef.d.ts +1 -1
  286. package/dist/rerankers/jev.d.ts +1 -2
  287. package/dist/rerankers/jev.js +4 -5
  288. package/dist/rerankers/llm.d.ts +1 -2
  289. package/dist/rerankers/llm.js +1 -2
  290. package/dist/rerankers/types.d.ts +1 -2
  291. package/dist/rrf.d.ts +2 -2
  292. package/dist/rrf.js +2 -2
  293. package/dist/search/bm25-search.d.ts +1 -1
  294. package/dist/search/bm25-search.js +2 -1
  295. package/dist/search/boosts.js +2 -1
  296. package/dist/search/hybrid.d.ts +1 -1
  297. package/dist/search/hybrid.js +2 -1
  298. package/dist/search/physics-search.d.ts +1 -1
  299. package/dist/search/physics-search.js +2 -1
  300. package/dist/search/types.d.ts +2 -0
  301. package/dist/search/types.js +3 -1
  302. package/dist/secret-detect.d.ts +4 -5
  303. package/dist/secret-detect.js +6 -10
  304. package/dist/server/auth.js +5 -5
  305. package/dist/server/client-ip.js +1 -1
  306. package/dist/server/cursor.js +2 -1
  307. package/dist/server/mcp-http.js +4 -4
  308. package/dist/server/request.d.ts +3 -6
  309. package/dist/server/request.js +6 -7
  310. package/dist/server/routes/admin.js +5 -4
  311. package/dist/server/routes/customer-notes.js +6 -5
  312. package/dist/server/routes/decisions.js +4 -3
  313. package/dist/server/routes/incidents.js +7 -5
  314. package/dist/server/routes/memories.js +7 -7
  315. package/dist/server/routes/policies.js +3 -2
  316. package/dist/server/routes/predictions.js +12 -15
  317. package/dist/server/routes/processes.js +3 -2
  318. package/dist/server/routes/project-briefs.js +8 -7
  319. package/dist/server/routes/recall.js +95 -93
  320. package/dist/server/routes/skills.js +6 -5
  321. package/dist/server/types.d.ts +1 -1
  322. package/dist/server/validation.d.ts +1 -2
  323. package/dist/server/validation.js +7 -14
  324. package/dist/server-detect.js +72 -58
  325. package/dist/server.d.ts +2 -2
  326. package/dist/server.js +131 -117
  327. package/dist/shared.d.ts +17 -17
  328. package/dist/shared.js +93 -97
  329. package/dist/skills.d.ts +3 -3
  330. package/dist/skills.js +8 -8
  331. package/dist/store/audit-event.d.ts +2 -2
  332. package/dist/store/audit-event.js +1 -1
  333. package/dist/store/candidates.d.ts +2 -2
  334. package/dist/store/candidates.js +4 -3
  335. package/dist/store/conflicts.js +30 -22
  336. package/dist/store/delete-and-batch.d.ts +11 -14
  337. package/dist/store/delete-and-batch.js +40 -91
  338. package/dist/store/entry-reads.d.ts +17 -25
  339. package/dist/store/entry-reads.js +59 -40
  340. package/dist/store/entry-row.d.ts +6 -24
  341. package/dist/store/entry-row.js +6 -24
  342. package/dist/store/entry-writes.d.ts +6 -7
  343. package/dist/store/entry-writes.js +13 -11
  344. package/dist/store/handoffs.d.ts +1 -1
  345. package/dist/store/handoffs.js +7 -10
  346. package/dist/store/index-and-stats.d.ts +2 -6
  347. package/dist/store/index-and-stats.js +4 -10
  348. package/dist/store/mirrors.d.ts +6 -19
  349. package/dist/store/mirrors.js +14 -39
  350. package/dist/store/open.js +9 -31
  351. package/dist/store/rows.d.ts +5 -11
  352. package/dist/store/rows.js +6 -11
  353. package/dist/store/search-rows.d.ts +17 -34
  354. package/dist/store/search-rows.js +31 -59
  355. package/dist/store/sessions.d.ts +4 -5
  356. package/dist/store/sessions.js +5 -6
  357. package/dist/store/summaries.d.ts +13 -17
  358. package/dist/store/summaries.js +26 -70
  359. package/dist/support-bundle.js +4 -8
  360. package/dist/tenant.d.ts +1 -5
  361. package/dist/token-ledger.d.ts +1 -1
  362. package/dist/token-ledger.js +3 -5
  363. package/dist/trace.js +1 -3
  364. package/dist/version.d.ts +1 -1
  365. package/dist/version.js +1 -1
  366. package/dist/working-memory.d.ts +1 -1
  367. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  368. package/extensions/openclaw-plugin/package.json +1 -1
  369. package/openclaw.plugin.json +1 -1
  370. package/package.json +1 -1
@@ -12,24 +12,13 @@ import { duplicateKey } from '../same-text.js';
12
12
  import { isContentWorthStoring } from '../audit.js';
13
13
  // Sentence-level patterns
14
14
  //
15
- // T1 (DF2): each pattern now carries TWO capture groups — group 1 is the
16
- // discriminating keyword (plus its trailing separator, verbatim), group 2 is
17
- // the content that follows it. Previously only the after-keyword content was
18
- // captured, so a negation like "never" / "must not" was discarded and a
19
- // prohibition inverted into an instruction ("Never use X" stored as "use X").
20
- // `extractFromPatterns` reassembles group1 + a clause-bounded group2 (T2, via
21
- // `boundToClause` below) rather than reading a single fixed-width group —
22
- // group 2's own reach is widened to {1,500} because the true stopping point
23
- // is now found by content, not counted characters. Keeping the keyword in
24
- // its own group (rather than folding it into one bigger capture) matters:
25
- // `boundToClause` must scan for a clause boundary only in group 2, never in
26
- // group 1 — several keywords end in their own colon ("error:", "rule:",
27
- // "decision:") which is not a clause boundary in the prose sense and would
28
- // wrongly truncate the capture down to just the keyword if scanned.
29
- // `PREFERENCE_PATTERNS[0]` is the one exception — its two-capture-group
30
- // shape means something different (two content spans either side of
31
- // "instead of"/"over"/"not") and is a separate, backlogged defect (see
32
- // docs/plans/2026-08-23-df2-capture-anchoring.md); left as-is.
15
+ // Each pattern carries TWO capture groups: group 1 is the discriminating keyword (plus its
16
+ // trailing separator, verbatim), group 2 the content after it, so a negation like "never" survives
17
+ // ("Never use X" must not store as "use X"). `extractFromPatterns` joins group1 + a clause-bounded
18
+ // group2 (`boundToClause`), so group 2 may reach {1,500}. The keyword keeps its own group because
19
+ // keywords like "error:" end in a colon that is not a prose clause boundary.
20
+ // `PREFERENCE_PATTERNS[0]` is the exception: its two groups are the content spans either side of
21
+ // "instead of"/"over"/"not"; left as-is.
33
22
  const DECISION_PATTERNS = [
34
23
  /(?:we(?:'ve| have)?|i(?:'ve| have)?|let's)\s+(decid(?:ed|e)\s+(?:to\s+)?)(.{1,500})/i,
35
24
  /(?:let's|we(?:'ll| will| should)?)\s+((?:go with|do|use|try|build|implement|switch to)\s+)(.{1,500})/i,
@@ -49,9 +38,8 @@ const ERROR_PATTERNS = [
49
38
  /((?:the (?:issue|problem|fix) (?:is|was))\s+)(.{1,500})/i,
50
39
  /((?:don't forget|easy to miss):\s*)(.{1,500})/i,
51
40
  ];
52
- // PREFERENCE_PATTERNS[0] keeps its pre-DF2 two-capture-group shape
53
- // (match[1]-only, unbounded) — out of scope here, backlogged. See
54
- // extractFromPatterns' reference check against this exact array element.
41
+ // PREFERENCE_PATTERNS[0] keeps its match[1]-only, unbounded shape; extractFromPatterns
42
+ // checks for this exact array element.
55
43
  const PREFERENCE_PATTERNS = [
56
44
  /(?:prefer|use)\s+(.{5,100})\s+(?:instead of|over|not)\s+(.{3,100})/i,
57
45
  /((?:don't use|avoid|skip)\s+)(.{1,500})/i,
@@ -72,52 +60,20 @@ export function splitSentences(text) {
72
60
  .map((s) => s.trim())
73
61
  .filter((s) => s.length > 5);
74
62
  }
75
- /**
76
- * T2 (DF2): bound a keyword+content capture to its clause instead of a fixed
77
- * character count. `full` is `keywordPrefix + content` (already concatenated
78
- * so the 200-char ceiling below applies to the whole stored string, not just
79
- * the part after the keyword); `searchFrom` is `keywordPrefix.length`, so the
80
- * clause/terminator scan only ever looks INSIDE `content` — several keywords
81
- * end in their own colon ("error:", "rule:", "decision:") which must never
82
- * be mistaken for a clause boundary in the prose that follows.
83
- *
84
- * Stops at the first `,`/`;`/`:` followed by whitespace, or at a sentence
85
- * terminator `[.!?]` followed by whitespace or end-of-string — whichever
86
- * comes first — and never past `maxLen` chars total.
87
- *
88
- * The whitespace requirement on the terminator is load-bearing: a bare
89
- * `[.!?]` would split inside a token like `.env` / `capture.ts` / `v1.35.0`,
90
- * turning a full clause into a fragment that then fails the write gate and
91
- * is silently dropped (measured in the plan). The `maxLen` ceiling is also
92
- * load-bearing: without it, a clause-free span can run past the 500-char
93
- * gate in `extractFromPatterns` and the whole match is dropped, where today
94
- * it is truncated and stored.
95
- */
96
- /**
97
- * Does a plausible CLOSING single quote appear after `from`? Mirror of the
98
- * opener rule in `boundToClause`: a closer sits tight against the literal it
99
- * ends (non-whitespace before) and is followed by whitespace, punctuation, or
100
- * end-of-string - never by a letter, which is what makes "user's" an
101
- * apostrophe rather than a partner.
102
- */
103
63
  function isLetterOrDigit(ch) {
104
64
  return ch !== undefined && /[\p{L}\p{N}]/u.test(ch);
105
65
  }
66
+ /** The last plausible CLOSING single quote, or -1. Mirror of the opener rule in `boundToClause`: a closer sits tight
67
+ * against its literal and is never followed by a letter, which is what makes "user's" an apostrophe. */
106
68
  function lastCloserIndex(full) {
107
- // ONE pass, not one per apostrophe. The previous shape rescanned the whole
108
- // remaining suffix at every boundary apostrophe, so a transcript full of
109
- // elisions ("keep 'em", "wait 'til", ...) made extraction quadratic and
110
- // could stall a moderately sized capture. Codex P2, r7.
69
+ // ONE pass, not one per apostrophe: a rescan per apostrophe goes quadratic
70
+ // on a transcript full of elisions ("keep 'em", "wait 'til", ...).
111
71
  //
112
- // A quote CLOSES THE SIDE IT IS TIGHT AGAINST. The test needs both
113
- // neighbours, and one round proved that empirically: these two have the
114
- // same following character and opposite roles -
72
+ // A quote CLOSES THE SIDE IT IS TIGHT AGAINST, so the test needs both
73
+ // neighbours: these two have the same following character and opposite roles -
115
74
  // "preserve 'a, b'-style" next '-' -> CLOSER, content on the left
116
75
  // "then run '--force," next '-' -> OPENER, content on the right
117
- // so no forward-only rule can separate them. An earlier revision tried
118
- // exactly that and broke in both directions at once (codex P1+P2, r10):
119
- // it missed closers before token-joining punctuation and accepted
120
- // "'.env" as a closer because a dot happened to be in its class.
76
+ // so no forward-only rule can separate them.
121
77
  //
122
78
  // never a closer next is a letter/digit "user's", "keep 'em"
123
79
  // closer prev is non-whitespace "'a, b' to", "'a, b'-style"
@@ -137,11 +93,10 @@ function lastCloserIndex(full) {
137
93
  // "tight against content" excludes an OPENING delimiter: in
138
94
  // "call parse('--force" the paren is non-whitespace but the quote after
139
95
  // it is an opener, and treating it as a closer let an earlier elision
140
- // pair across the clause boundary. Codex P2, r11.
96
+ // pair across the clause boundary.
141
97
  const tightBefore = prev !== undefined && !/[\s([{]/.test(prev);
142
98
  const endsAfter = next === undefined || /\s/.test(next);
143
- // Punctuation splits in two, and this is the fact the last three
144
- // revisions kept rediscovering piecemeal:
99
+ // Punctuation splits in two:
145
100
  // , ; : ! ? ) ] } never join tokens - they end the literal whatever
146
101
  // follows, so "'echo a, b ';then" closes at the ;
147
102
  // . - _ / @ # DO join tokens - "'.env" and "'--force" continue a
@@ -156,17 +111,12 @@ function lastCloserIndex(full) {
156
111
  }
157
112
  return -1;
158
113
  }
114
+ /** Cuts `full` (keyword + content) at the first prose `,;:` or `[.!?]` followed by whitespace, scanning from `searchFrom` so a keyword's
115
+ * own colon never counts; the whitespace rule keeps `.env` whole and `maxLen` truncates a long span the 500-char gate would drop. */
159
116
  function boundToClause(full, searchFrom, maxLen = 200) {
160
- // Scan for the first PROSE clause boundary after `searchFrom`.
161
- //
162
- // Depth-awareness is not a nicety here: hippo memories are full of code, and
163
- // a naive scan reintroduces the exact fragment defect this whole change
164
- // exists to remove. Measured before this guard existed:
117
+ // Depth-awareness matters because memories are full of code; a naive scan cuts
165
118
  // "Always call build(x, y) before deploy." -> "Always call build(x"
166
- // "Never pass {a: 1, b: 2} to the writer." -> "Never pass {a"
167
- // Both then PASS the write gate, because they contain code punctuation and
168
- // so read as "specific" — a malformed fragment stored with high confidence.
169
- // Codex review finding (P1) on this branch.
119
+ // and the fragment PASSES the write gate, since code punctuation reads as "specific".
170
120
  //
171
121
  // So: a separator only ends the clause at bracket depth zero and outside
172
122
  // quotes. Unbalanced closers are tolerated (depth floors at 0) because
@@ -222,15 +172,8 @@ function boundToClause(full, searchFrom, maxLen = 200) {
222
172
  return bounded;
223
173
  }
224
174
  function closesQuote(full, i, quote) {
225
- // Closing uses the SAME shape test as opening. This was asymmetric:
226
- // twelve rounds went into deciding when a quote OPENS a literal, while
227
- // the close accepted any bare "'" - so the apostrophe in a possessive
228
- // INSIDE a literal closed it early:
229
- // "Always pass 'user's a, b list' to the parser."
230
- // -> "Always pass 'user's a" (master kept the whole literal)
231
- // a mid-literal fragment that then PASSES the write gate, which is
232
- // precisely the defect this branch exists to remove. Found by the
233
- // ship-gate review after every earlier gate missed it.
175
+ // Closing uses the SAME shape test as opening, else a possessive INSIDE a literal closes it early:
176
+ // "Always pass 'user's a, b list' to the parser." -> "Always pass 'user's a"
234
177
  return full[i] === quote && (quote !== "'" || !isLetterOrDigit(full[i + 1]));
235
178
  }
236
179
  function opensSingleQuote(full, i, lastCloser) {
@@ -242,7 +185,6 @@ function opensSingleQuote(full, i, lastCloser) {
242
185
  // restart..." never leaves quote mode, bounding disabled entirely
243
186
  // treat no ' as a quote -> "Always pass 'a, b' to the parser."
244
187
  // cuts at the comma and stores "Always pass 'a"
245
- // Both were codex P1s on this branch, in consecutive rounds.
246
188
  //
247
189
  // Discriminator: a ' OPENS a literal only at a word boundary - preceded
248
190
  // by start/whitespace/open-bracket AND followed by non-whitespace. An
@@ -257,7 +199,7 @@ function opensSingleQuote(full, i, lastCloser) {
257
199
  // have no closer, so the scanner never leaves quote mode and bounding
258
200
  // is disabled for the rest of the capture. Requiring an actual closing
259
201
  // quote later in the string replaces a guess with a checkable fact -
260
- // an elided form simply has no partner. Codex P2, this round.
202
+ // an elided form simply has no partner.
261
203
  // ...and the partner must LOOK like a closer, not merely be another
262
204
  // apostrophe. Distance cannot separate the two cases - both put a `'`
263
205
  // far downstream:
@@ -268,7 +210,7 @@ function opensSingleQuote(full, i, lastCloser) {
268
210
  // elision, disabling bounding for the rest of the capture
269
211
  // So apply the SAME shape rule already used to open a literal, mirrored:
270
212
  // a closer has non-whitespace before it and whitespace/punctuation (not
271
- // a letter) after. "user's" fails it on both counts. Codex P2, r6.
213
+ // a letter) after. "user's" fails it on both counts.
272
214
  return atBoundary && lastCloser > i;
273
215
  }
274
216
  function cleanExtract(raw) {
@@ -276,7 +218,7 @@ function cleanExtract(raw) {
276
218
  .replace(/^[:\s-]+/, '')
277
219
  .replace(/[.!?,;:\s]+$/, '')
278
220
  .trim();
279
- // T2 trailing cleanup: clause-bounding can cut inside a parenthetical and
221
+ // Trailing cleanup: clause-bounding can cut inside a parenthetical and
280
222
  // leave an unmatched trailing ')' (e.g. "...(two had never got entries),"
281
223
  // bounds to "never got entries)"). Strip a trailing ')' ONLY while closes
282
224
  // outnumber opens in the string so far — a balanced parenthetical like
@@ -316,51 +258,31 @@ function extractFromPatterns(sentence, patterns, category, tag) {
316
258
  if (match) {
317
259
  let bounded;
318
260
  if (pat === PREFERENCE_PATTERNS[0]) {
319
- // PREFERENCE_PATTERNS[0] is the one pattern left out of T1/T2 (see
320
- // comment at its definition) — its match[1] keeps its pre-DF2,
321
- // unbounded shape rather than going through clause-bounding.
261
+ // PREFERENCE_PATTERNS[0] skips clause-bounding (see comment at its definition).
322
262
  bounded = match[1] ?? match[0];
323
263
  }
324
264
  else {
325
265
  // group1 = keyword + separator (verbatim, never clause-bounded);
326
266
  // group2 = the content that follows it (clause-bounded below).
327
267
  //
328
- // T1 preserves the keyword only when it carries SEMANTIC SIGN — a
268
+ // The keyword is kept only when it carries SEMANTIC SIGN, a
329
269
  // negation or modality ("never", "must not", "do not ever",
330
- // "always"). Dropping those inverts the meaning, which is the whole
331
- // point of T1. A LABEL keyword ("decision:", "rule:", "error:",
270
+ // "always"), because dropping those inverts the meaning. A LABEL keyword ("decision:", "rule:", "error:",
332
271
  // "important:") carries no sign: it only names the category, which
333
272
  // is already recorded in `category`/`tags`, so prefixing it onto the
334
273
  // content is duplication that also breaks value-keyed matching
335
- // (AT1's rejected-value digest hashes the bare content).
274
+ // (the rejected-value digest hashes the bare content).
336
275
  // Discriminator: a label ends in its own colon, or is a "the X is"
337
276
  // phrase.
338
277
  const rawPrefix = match[1] ?? '';
339
- // ONLY colon-terminated labels are dropped. Dropping "the X is/was"
340
- // too left the residue starting with "to ", which isFragment then
341
- // rejected outright - so "The plan is to ship on Friday" and "The fix
342
- // was to bump the pool timeout" stored NOTHING, where every prior
343
- // version stored them. Silent loss on two of the highest-traffic
344
- // patterns, and invisible: an absent memory leaves no trace. The AT1
345
- // rejected-value evidence only ever involved colon labels
346
- // ("decision: "), so the narrower rule keeps that guarantee.
278
+ // ONLY colon-terminated labels are dropped: dropping "the X is/was" leaves a residue starting
279
+ // with "to ", which isFragment rejects, so "The plan is to ship on Friday" would store nothing.
347
280
  const isLabelPrefix = /:\s*$/.test(rawPrefix);
348
281
  const keywordPrefix = isLabelPrefix ? '' : rawPrefix;
349
- // Scan the UNTRUNCATED remainder, not match[2]. The patterns cap
350
- // their content group at 500 chars, so a quoted literal whose closer
351
- // sits past that point had no visible partner and the pairing check
352
- // read the opener as prose - cutting inside the literal. That was a
353
- // blindness built into the scanner's INPUT, not a bad predicate, so
354
- // no further predicate could have fixed it. boundToClause already
355
- // caps its OUTPUT at maxLen, so widening the input costs nothing and
356
- // makes pairing decidable on the whole sentence. Codex P2, round 5.
357
- // Group 2's REAL offset, read from the regex engine. Deriving it as
358
- // `match.index + rawPrefix.length` assumes group 1 starts the match,
359
- // but DECISION_PATTERNS carry an uncaptured subject ("we ", "let's ")
360
- // ahead of it - so the offset landed inside the keyword and the
361
- // widened slice duplicated text: "We decided to pin..." stored as
362
- // "decided to to pin...". Real corruption of the commonest decision
363
- // capture, shipped in the previous commit. Codex P1, r6.
282
+ // Scan the UNTRUNCATED remainder, not match[2]: a quoted literal whose closer sits past the
283
+ // 500-char group cap would read as prose; boundToClause caps its OUTPUT, so this costs nothing.
284
+ // Group 2's REAL offset from the regex engine: DECISION_PATTERNS carry an uncaptured subject
285
+ // ("we ", "let's ") ahead of group 1, so `match.index + rawPrefix.length` lands inside the keyword.
364
286
  const contentStart = match.indices?.[2]?.[0] ?? -1;
365
287
  const afterKeyword = contentStart >= 0 ? sentence.slice(contentStart) : (match[2] ?? match[0]);
366
288
  const keywordStart = match.indices?.[1]?.[0] ?? -1;
@@ -1,4 +1,4 @@
1
- import type { JsonValue } from './working-memory.js';
1
+ import type { JsonValue } from './json.js';
2
2
  /** Why a failure was not stored, or `stored`. */
3
3
  export type CaptureErrorOutcome = 'stored' | 'duplicate' | 'skipped-interrupt' | 'skipped-routine' | 'skipped-invalid';
4
4
  /** Which routine check skipped a failure; the log keeps it so declines can be told apart from empty searches. */
@@ -1,6 +1,6 @@
1
1
  // `hippo capture-error`, run by the Claude Code PostToolUseFailure hook: routine failures and repeats are not
2
2
  // stored, because error memories decay slowly and would crowd out real lessons; what is stored stays `observed`
3
- // until outcome feedback confirms it. Every failure, stored or not, goes to the failure log (ROADMAP CD13).
3
+ // until outcome feedback confirms it. Every failure, stored or not, goes to the failure log.
4
4
  import { createMemory } from './memory.js';
5
5
  import { writeEntry } from './store/entry-writes.js';
6
6
  import { loadContentsWithTag } from './store/entry-reads.js';
@@ -1,4 +1,4 @@
1
- /** Git subprocess helpers for FE2 churn-staleness (src/invalidation.ts). */
1
+ /** Git subprocess helpers for churn-staleness (src/invalidation.ts). */
2
2
  /** A real git failure (not git grep's expected exit-1-no-match). Callers abort the whole run on this. */
3
3
  export declare class GitReadError extends Error {
4
4
  }
package/dist/churn-git.js CHANGED
@@ -1,4 +1,4 @@
1
- /** Git subprocess helpers for FE2 churn-staleness (src/invalidation.ts). */
1
+ /** Git subprocess helpers for churn-staleness (src/invalidation.ts). */
2
2
  import { execFileSync } from 'child_process';
3
3
  import * as fs from 'fs';
4
4
  import * as os from 'os';
package/dist/cli/audit.js CHANGED
@@ -9,7 +9,7 @@ import { pruneAuditLog, parseOlderThanFlag } from '../audit-prune.js';
9
9
  import { printError } from './output.js';
10
10
  import { requireInit, resolveAuthRoot } from './shared.js';
11
11
  // ---------------------------------------------------------------------------
12
- // Audit log subcommands (A5 stub auth — `hippo audit list`)
12
+ // Audit log subcommands (`hippo audit list`)
13
13
  // ---------------------------------------------------------------------------
14
14
  const VALID_AUDIT_OPS = new Set(AUDIT_OPS);
15
15
  function formatAuditRow(ev) {
@@ -23,8 +23,7 @@ function cmdAuditList(hippoRoot, flags) {
23
23
  const tenantId = resolveTenantId({});
24
24
  const opFlag = typeof flags['op'] === 'string' ? flags['op'] : undefined;
25
25
  if (opFlag && !VALID_AUDIT_OPS.has(opFlag)) {
26
- // Regenerate from Set to prevent future drift (v1.11.5: pre-v1.11.5 message
27
- // was hand-maintained and had drifted — missed 'auth_revoke' and 'outcome').
26
+ // Built from the Set so the message cannot drift from the valid ops.
28
27
  const expected = Array.from(VALID_AUDIT_OPS).join(' | ');
29
28
  printError(`Unknown --op value: ${opFlag}. Expected one of: ${expected}.`);
30
29
  process.exit(1);
@@ -115,7 +114,7 @@ function cmdAuditLog(hippoRoot, args, flags) {
115
114
  process.exit(1);
116
115
  }
117
116
  export function handleAudit({ hippoRoot, args, flags }) {
118
- // `audit list` and `audit prune` -> A5 audit-log subcommands.
117
+ // `audit list` and `audit prune` -> audit-log subcommands.
119
118
  // Other forms (no sub, --fix) keep the existing memory-quality auditor
120
119
  // for backwards compatibility.
121
120
  if (args[0] === 'list' || args[0] === 'prune') {
package/dist/cli/auth.js CHANGED
@@ -6,16 +6,14 @@ import { resolveTenantId } from '../tenant.js';
6
6
  import { printError } from './output.js';
7
7
  import { resolveAuthRoot } from './shared.js';
8
8
  // ---------------------------------------------------------------------------
9
- // Auth subcommands (A5 stub auth)
9
+ // Auth subcommands
10
10
  // ---------------------------------------------------------------------------
11
11
  function cmdAuthCreate(hippoRoot, flags) {
12
12
  const root = resolveAuthRoot(hippoRoot, flags);
13
13
  const tenantFlag = typeof flags['tenant'] === 'string' ? flags['tenant'] : undefined;
14
14
  const labelFlag = typeof flags['label'] === 'string' ? flags['label'] : undefined;
15
15
  const asJson = Boolean(flags['json']);
16
- // v1.12.3: --role flag surfaces the api_keys.role column added v1.12.0
17
- // sub-1. Accepts 'admin' | 'member' only; anything else exits 1 with a
18
- // typed error so a typo doesn't silently default to admin.
16
+ // Accepts 'admin' | 'member' only; anything else exits 1 so a typo doesn't silently default to admin.
19
17
  const roleFlag = typeof flags['role'] === 'string' ? flags['role'] : undefined;
20
18
  let role = 'admin';
21
19
  if (roleFlag !== undefined) {
@@ -57,7 +55,6 @@ function formatKeyRow(item) {
57
55
  const label = item.label ?? '-';
58
56
  const created = item.createdAt;
59
57
  const revoked = item.revokedAt ?? '-';
60
- // v1.12.3: role column surfaced
61
58
  return `${item.keyId} ${item.tenantId} ${item.role} ${label} ${created} ${revoked}`;
62
59
  }
63
60
  function cmdAuthList(hippoRoot, flags) {
@@ -111,7 +108,7 @@ function cmdAuthRevoke(hippoRoot, keyId, flags) {
111
108
  }
112
109
  console.log(`Revoked ${keyId} at ${revokedAt}`);
113
110
  }
114
- /** EI2: `hippo auth grant|ungrant <key_id> <scope>`, routed through api so the tenant, restricted-scope and audit checks live in one place. */
111
+ /** `hippo auth grant|ungrant <key_id> <scope>`, routed through api so the tenant, restricted-scope and audit checks live in one place. */
115
112
  function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
116
113
  const ctx = keyContext(resolveAuthRoot(hippoRoot, flags), keyId);
117
114
  try {
@@ -12,7 +12,7 @@ export declare function cmdUnreject(hippoRoot: string, args: string[], flags: Re
12
12
  * `"dormant": { "enabled": false }` in .hippo/config.json deletes instead).
13
13
  */
14
14
  export declare function cmdDormant(hippoRoot: string, args: string[], flags: Record<string, string | boolean | string[]>): void;
15
- /** `hippo quarantine [list] [--all] [--json] [--global]`, `quarantine approve <id>`, `quarantine reject <id>` (CD5 poisoning defence). */
15
+ /** `hippo quarantine [list] [--all] [--json] [--global]`, `quarantine approve <id>`, `quarantine reject <id>` (poisoning defence). */
16
16
  export declare function cmdQuarantine(hippoRoot: string, args: string[], flags: Record<string, string | boolean | string[]>): void;
17
17
  export declare function handleForget({ hippoRoot, args, flags }: CommandContext): Promise<void>;
18
18
  export declare function handleInvalidate({ hippoRoot, args, flags }: CommandContext): void;
@@ -20,11 +20,7 @@ export function cmdOutcome(hippoRoot, flags) {
20
20
  printError('Specify --good or --bad');
21
21
  process.exit(1);
22
22
  }
23
- // Behavior fix (v1.11.3): cmdOutcome used to bypass api.outcome and do its
24
- // own readEntry/writeEntry inline, which silently skipped the audit_log
25
- // emission that the MCP outcome path already has via api.outcome. T6
26
- // rewires through api.outcome so every successful CLI 'outcome' call now
27
- // writes one audit_log row per affected id, matching MCP parity.
23
+ // Through api.outcome so every CLI outcome writes one audit_log row per id, as the MCP path does.
28
24
  const ctx = {
29
25
  hippoRoot,
30
26
  tenantId: resolveTenantId({}),
@@ -54,7 +50,7 @@ function cmdForget(hippoRoot, id, flags) {
54
50
  tenantId: resolveTenantId({}),
55
51
  actor: api.adminActor('cli'),
56
52
  };
57
- // A3: raw memories (Slack / GitHub connector ingestion) are append-only — a
53
+ // Raw memories (Slack / GitHub connector ingestion) are append-only: a
58
54
  // BEFORE-DELETE trigger aborts any delete. archiveRaw is the sanctioned
59
55
  // removal path; it records ctx.actor as the archiver for provenance.
60
56
  if (flags['archive'] === true) {
@@ -175,11 +171,8 @@ export function cmdResolve(hippoRoot, args, flags) {
175
171
  return;
176
172
  }
177
173
  const forgetLoser = Boolean(flags['forget']);
178
- // AT1: --reject-loser tombstones the loser's normalized digest so it
179
- // cannot be re-asserted later, in addition to removing it (kind-aware).
180
- // --reason defaults to a conflict-context string when omitted (resolve
181
- // already has the conflict id + keepId; unlike `hippo reject`, a reason
182
- // is not strictly required here).
174
+ // --reject-loser tombstones the loser's digest so it cannot be re-asserted, as well as removing it.
175
+ // --reason is optional here, unlike `hippo reject`: resolve already has the conflict id and keepId.
183
176
  const rejectLoser = Boolean(flags['reject-loser']);
184
177
  const reasonFlag = typeof flags['reason'] === 'string' ? flags['reason'] : undefined;
185
178
  const result = resolveConflict(hippoRoot, conflictId, keepId, forgetLoser, tenantId, {
@@ -198,16 +191,14 @@ export function cmdResolve(hippoRoot, args, flags) {
198
191
  console.log(`Resolved conflict ${conflictId}: kept ${keepId}, ${action} ${result.loserId}`);
199
192
  }
200
193
  // ---------------------------------------------------------------------------
201
- // AT1: reject / rejections / unreject
202
- // docs/plans/2026-08-15-at1-rejected-value-tombstone.md §4
194
+ // reject / rejections / unreject
203
195
  // ---------------------------------------------------------------------------
204
196
  export function cmdReject(hippoRoot, args, flags) {
205
197
  // Store resolution mirrors `hippo remember`: --global writes to the
206
198
  // global store, otherwise the local store (requireInit'd via resolveAuthRoot).
207
199
  const root = resolveAuthRoot(hippoRoot, flags);
208
200
  const tenantId = resolveTenantId({});
209
- // --reason is REQUIRED (plan §4, grill issue 4): the tombstone stores no
210
- // content, so reason is its only human-readable identity.
201
+ // --reason is REQUIRED: the tombstone stores no content, so reason is its only human-readable identity.
211
202
  const reason = typeof flags['reason'] === 'string' ? flags['reason'].trim() : '';
212
203
  if (!reason) {
213
204
  printError('hippo reject requires --reason "<why>" (the tombstone stores no content; reason is its only identity).');
@@ -221,8 +212,7 @@ export function cmdReject(hippoRoot, args, flags) {
221
212
  process.exit(1);
222
213
  }
223
214
  if (memoryId && valueFlag !== undefined) {
224
- // Ambiguous ask: silently preferring one form would ignore the other
225
- // without feedback (code-review round-1 low).
215
+ // Ambiguous ask: silently preferring one form would ignore the other without feedback.
226
216
  printError('hippo reject takes EITHER a memory id OR --value, not both.');
227
217
  process.exit(1);
228
218
  }
@@ -372,7 +362,7 @@ export function cmdDormant(hippoRoot, args, flags) {
372
362
  }
373
363
  console.log('Bring one back: hippo dormant restore <id> Delete for good: hippo dormant forget <id>');
374
364
  }
375
- /** `hippo quarantine [list] [--all] [--json] [--global]`, `quarantine approve <id>`, `quarantine reject <id>` (CD5 poisoning defence). */
365
+ /** `hippo quarantine [list] [--all] [--json] [--global]`, `quarantine approve <id>`, `quarantine reject <id>` (poisoning defence). */
376
366
  export function cmdQuarantine(hippoRoot, args, flags) {
377
367
  const root = resolveAuthRoot(hippoRoot, flags);
378
368
  const ctx = {
@@ -512,12 +502,6 @@ export function handleInvalidate({ hippoRoot, args, flags }) {
512
502
  process.exit(1);
513
503
  }
514
504
  const onlyId = typeof flags['id'] === 'string' ? flags['id'] : undefined;
515
- if (typeof flags['dry-run'] === 'string') {
516
- // Dead: the earlier global BOOLEAN_FLAGS guard now exits first on any --dry-run=<v>.
517
- // Kept as defence in depth on a destructive command.
518
- printError('--dry-run takes no value');
519
- process.exit(1);
520
- }
521
505
  const dryRun = flags['dry-run'] === true;
522
506
  if ((target && onlyId) || (!target && !onlyId)) {
523
507
  printError('Usage: hippo invalidate "<old pattern>" [--dry-run] [--reason "<why>"]');
package/dist/cli/dag.js CHANGED
@@ -26,9 +26,7 @@ export function cmdDag(hippoRoot, flags) {
26
26
  console.log(` Unlinked facts: ${unlinked}`);
27
27
  return;
28
28
  }
29
- // Tree view: v0.30 / E5 renders L3 entity profiles as roots (with L2
30
- // children indented), then orphan L2 summaries (no L3 parent) at top
31
- // level. Pre-E5 behavior was L2-only roots; rendering now covers L3.
29
+ // Tree view: L3 entity profiles as roots with L2 children indented, then orphan L2 summaries at top level.
32
30
  const profiles = entries.filter((e) => e.dag_level === 3);
33
31
  const l2List = entries.filter((e) => e.dag_level === 2);
34
32
  const orphanL2 = l2List.filter((e) => !e.dag_parent_id);
@@ -62,7 +60,7 @@ export function cmdDag(hippoRoot, flags) {
62
60
  }
63
61
  }
64
62
  }
65
- // Orphan L2 summaries (no L3 parent) at top level — pre-E5 default shape.
63
+ // Orphan L2 summaries (no L3 parent) at top level.
66
64
  for (const summary of orphanL2) {
67
65
  const summaryTags = summary.tags.filter((t) => t !== 'dag-summary').join(', ');
68
66
  console.log(`\n📌 ${summary.content.slice(0, 80)}`);
@@ -113,8 +111,7 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
113
111
  const limit = typeof flags['limit'] === 'string' ? Number(flags['limit']) : undefined;
114
112
  // Absent stays undefined so the api default applies; the 0 fallback is unreachable.
115
113
  const budget = flags['budget'] === undefined ? undefined : parseBudgetFlag(flags['budget'], 0);
116
- // v0.30 / E5: --depth N walks N levels down (default 1, hard cap 10).
117
- // L4 fold: reject out-of-range explicitly (no silent clamp).
114
+ // --depth N walks N levels down (default 1, hard cap 10); out-of-range is rejected, never silently clamped.
118
115
  const rawDepth = typeof flags['depth'] === 'string' ? Number(flags['depth']) : undefined;
119
116
  let depth;
120
117
  if (rawDepth !== undefined) {
@@ -136,9 +133,8 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
136
133
  cost: drillCost,
137
134
  });
138
135
  if ('failure' in r) {
139
- // v1.6.4: only `not_drillable` is caller-actionable. `not_found`
140
- // intentionally collapses cross-tenant + scope-blocked + missing
141
- // (codex round 3 P1: distinguishing scope_blocked leaked existence).
136
+ // Only `not_drillable` is caller-actionable. `not_found` collapses cross-tenant, scope-blocked
137
+ // and missing on purpose: telling scope_blocked apart would leak that the row exists.
142
138
  if (r.failure === 'not_drillable') {
143
139
  printError(`Id ${summaryId} is a leaf row, not a level-2+ summary; nothing to drill into.`);
144
140
  }
@@ -27,8 +27,7 @@ function parseObjectId(idRaw, noun) {
27
27
  return id;
28
28
  }
29
29
  // ---------------------------------------------------------------------------
30
- // E2 prediction first-class object (v0.31)
31
- // docs/plans/2026-05-26-e2-prediction-object.md
30
+ // Prediction first-class object
32
31
  // ---------------------------------------------------------------------------
33
32
  function predictClose(hippoRoot, tenantId, args, flags) {
34
33
  const idRaw = args[1];
@@ -65,14 +64,9 @@ function loadPredictionList(hippoRoot, tenantId, status, classTag, limit) {
65
64
  });
66
65
  }
67
66
  if (status === 'all') {
68
- // No closure-state filter; pull both via loadPredictionsByClass if class given
69
- if (classTag) {
70
- return predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, { limit });
71
- }
72
- // No class filter + status=all = pull open + closed across all classes
73
- // (kept simple: report open via loadOpenPredictions; closed via two
74
- // class scans isn't symmetrical. v1 callers typically pass --class.)
75
- return predictionsModule.loadOpenPredictions(hippoRoot, tenantId, { limit });
67
+ return classTag
68
+ ? predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, { limit })
69
+ : predictionsModule.loadAllPredictions(hippoRoot, tenantId, { limit });
76
70
  }
77
71
  if (!predictionsModule.VALID_CLOSURE_STATES.has(status)) {
78
72
  printError(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
@@ -141,7 +135,7 @@ function predictShow(hippoRoot, tenantId, args) {
141
135
  console.log(` created: ${pred.createdAt}`);
142
136
  }
143
137
  function predictBaserate(hippoRoot, tenantId, flags) {
144
- // J3 reference-class / planning-fallacy detector
138
+ // Reference-class / planning-fallacy detector
145
139
  const classTagRaw = flags['class'];
146
140
  if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
147
141
  printError('Usage: hippo predict baserate --class <c>');
@@ -309,16 +303,15 @@ function decideCreate(hippoRoot, tenantId, decisionText, flags) {
309
303
  }
310
304
  const contextRaw = flags['context'];
311
305
  const context = typeof contextRaw === 'string' && contextRaw ? contextRaw : undefined;
312
- // A value-less `--supersedes` (parseArgs stores boolean true) is a malformed
313
- // request: the user asked to supersede but gave no memory id. Reject it rather
314
- // than silently creating a non-superseding decision (codex review 2026-05-28).
306
+ // A value-less `--supersedes` asks to supersede but gives no memory id: reject it rather
307
+ // than silently creating a non-superseding decision.
315
308
  if (flags['supersedes'] === true) {
316
309
  printError('--supersedes requires a memory id, e.g. hippo decide "<text>" --supersedes mem_abc123.');
317
310
  process.exit(1);
318
311
  }
319
312
  const supersedesMemId = typeof flags['supersedes'] === 'string' ? flags['supersedes'] : null;
320
313
  // Backward-compat: --supersedes takes a MEMORY id. Validate it exists and
321
- // resolve it to the active decision row (if any). Grill fix: commit the
314
+ // resolve it to the active decision row (if any). Commit the
322
315
  // canonical table create+supersede FIRST (inside saveDecision's SAVEPOINT),
323
316
  // weaken the old memory LAST (best-effort) so a memory-write failure cannot
324
317
  // leave the memory stale without the table reflecting the supersession.
@@ -344,11 +337,8 @@ function decideCreate(hippoRoot, tenantId, decisionText, flags) {
344
337
  // 'superseded' tag. Preserves the exact pre-promotion behavior for the memory
345
338
  // mirror; the canonical table supersession already committed above.
346
339
  if (oldEntry) {
347
- // Best-effort: saveDecision already committed the canonical mutation (new
348
- // decision created + old row superseded). If this legacy memory-weaken
349
- // throws, do NOT fail the command — a retry would find no active decision
350
- // for the old memory and create a duplicate active successor. Warn instead
351
- // (codex review 2026-05-28).
340
+ // Best-effort: saveDecision already committed. Failing here would make a retry find no active
341
+ // decision for the old memory and create a duplicate active successor, so warn instead.
352
342
  try {
353
343
  oldEntry.half_life_days = Math.max(1, Math.floor(oldEntry.half_life_days / 2));
354
344
  oldEntry.confidence = 'stale';
@@ -373,7 +363,6 @@ function decideCreate(hippoRoot, tenantId, decisionText, flags) {
373
363
  // Strict positive-integer parse for incident id args. parseInt() alone accepts
374
364
  // trailing junk ("1abc" -> 1), which would let a mutating subcommand (close/
375
365
  // resolve) silently hit the wrong row; require the whole arg to be digits.
376
- // (codex P2, 2026-05-29.)
377
366
  function parsePositiveIncidentId(idRaw) {
378
367
  const s = String(idRaw ?? '').trim();
379
368
  const id = parseInt(s, 10);
@@ -3,6 +3,7 @@ import { confidenceFacets } from '../memory.js';
3
3
  import { isInitialized } from '../store/open.js';
4
4
  import { loadSearchEntries } from '../store/search-rows.js';
5
5
  import { loadIndex } from '../store/index-and-stats.js';
6
+ import { DEFAULT_RECALL_BUDGET } from '../search/types.js';
6
7
  import { loadConfig } from '../config.js';
7
8
  import { dropHeldCopies } from '../same-text.js';
8
9
  import { detectScope } from '../scope.js';
@@ -27,7 +28,7 @@ function noteScopeHidden(hippoRoot, globalRoot, query, tenantId, requested) {
27
28
  }
28
29
  export async function cmdExplain(hippoRoot, query, flags) {
29
30
  requireInit(hippoRoot);
30
- const budget = parseBudgetFlag(flags['budget'], 4000);
31
+ const budget = parseBudgetFlag(flags['budget'], DEFAULT_RECALL_BUDGET);
31
32
  const limit = parseLimitFlag(flags['limit']);
32
33
  const asJson = Boolean(flags['json']);
33
34
  const includeSuperseded = Boolean(flags['include-superseded']);
package/dist/cli/goals.js CHANGED
@@ -4,7 +4,7 @@ import * as api from '../api.js';
4
4
  import { resolveTenantId } from '../tenant.js';
5
5
  import { printError } from './output.js';
6
6
  // ---------------------------------------------------------------------------
7
- // `hippo goal <push|list|complete|suspend|resume>` — B3 dlPFC depth (Task 10)
7
+ // `hippo goal <push|list|complete|suspend|resume>`
8
8
  // ---------------------------------------------------------------------------
9
9
  const GOAL_POLICY_TYPES = [
10
10
  'schema-fit-biased',
package/dist/cli/init.js CHANGED
@@ -242,7 +242,7 @@ function refreshShippedBlock(filePath, text, hook) {
242
242
  /** Claude Code settings hooks, Codex's hooks.json and the OpenCode plugin, under the home directory; idempotent, so re-running init adds newer hooks. */
243
243
  function installUserLevelHooks(agents, codexHint) {
244
244
  for (const hook of agents) {
245
- // The Codex capture wrapper swaps the codex launcher binary, so init only points at the opt-in (issue #133).
245
+ // The Codex capture wrapper swaps the codex launcher binary, so init only points at the opt-in.
246
246
  if (hook === 'codex' && codexHint && !isCodexWrapperInstalled()) {
247
247
  console.log(' Codex detected. To capture Codex sessions: hippo hook install codex');
248
248
  }