hippo-memory 1.58.0 → 1.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -20,6 +20,7 @@
20
20
  * hippo rejections
21
21
  * hippo unreject <digest-prefix>
22
22
  * hippo dormant [<query>] [--limit <n>] [--json] | restore <id> | forget <id>
23
+ * hippo projects [--json] | merge <from> <into> [--apply] | repair [--apply]
23
24
  * hippo tokens [--days <n>] [--json] [--global]
24
25
  * hippo doctor [--json]
25
26
  * hippo inspect <id>
@@ -135,6 +136,7 @@ import { MAX_HOPS, DEFAULT_MAX_NEIGHBORS } from './graph-recall.js';
135
136
  import { getReranker } from './rerankers/index.js';
136
137
  import { rankRecall } from './recall-pipeline.js';
137
138
  import { JEV_DEFAULT_TOP_K } from './rerankers/jev.js';
139
+ import { isClefModel } from './rerankers/clef.js';
138
140
  import { computeSalience } from './salience.js';
139
141
  import { renderAmbientSummary } from './ambient.js';
140
142
  import { assembleCost, assembleHeading, contextCost, contextHeading, contextLine, crossProjectHeading, crossProjectLine, drillCost, handoffText, printedTokens, sessionTrailText, settleTokens, snapshotText, } from './context-render.js';
@@ -145,6 +147,8 @@ import { backfillChannel } from './connectors/slack/backfill.js';
145
147
  import { slackHistoryFetcher } from './connectors/slack/web-client.js';
146
148
  import { addWorkspace as addSlackWorkspace, listWorkspaces as listSlackWorkspaces, removeWorkspace as removeSlackWorkspace, } from './connectors/slack/workspaces.js';
147
149
  import { cmdGithub, printGithubBackfillUsage } from './connectors/github/cli-impl.js';
150
+ import { log } from './log.js';
151
+ import { printError } from './cli/output.js';
148
152
  import { parseLimitFlag, parseCountFlag, parseBudgetFlag, emitCliAudit, requireInit, runChurnStaleForRepo, runViaServerIfAvailable, fmt, recallEntryText, recallHeading, printAgentImport, hippoBlock, installCodexMemoryHooks, setupDailySchedule, parseAsOfFlag, engineFlags, collectHandoffEvidence, logSessionEndImport, appendSessionEndCloseLog, printActiveTaskSnapshot, printSessionEvents, printHandoff, cardStringFlag, hostSessionId, resetHookInjection, captureConsole, hookStoreRoot, withLedgerDb, learnFromRepo, HOOK_MARKERS, HOOKS, resolveAuthRoot, } from './cli/shared.js';
149
153
  // ---------------------------------------------------------------------------
150
154
  // Helpers
@@ -153,7 +157,7 @@ import { parseLimitFlag, parseCountFlag, parseBudgetFlag, emitCliAudit, requireI
153
157
  // and as off under === true (`--pin=true` would not pin), so parseArgs and main() refuse one.
154
158
  // tests/cli-parse-flag-equals.test.ts fails when a switch read is missing from this set.
155
159
  export const BOOLEAN_FLAGS = new Set([
156
- 'agents', 'all', 'all-tenants', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'churn', 'continuity',
160
+ 'agents', 'all', 'all-tenants', 'apply', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'churn', 'continuity',
157
161
  'cross-project', 'dry-run', 'equal-sources', 'error', 'evc-adaptive', 'extract',
158
162
  'filter-conflicts', 'fix', 'force', 'forget', 'git', 'global', 'good', 'graph-stream',
159
163
  'help', 'include-logs', 'include-superseded', 'inferred', 'json', 'last-session', 'multihop', 'no-hooks',
@@ -181,7 +185,7 @@ export const KNOWN_FLAGS = new Set([
181
185
  ]);
182
186
  // Commands that delete or hide memories: an unknown flag here stops the run instead of being ignored.
183
187
  const DESTRUCTIVE_COMMANDS = new Set([
184
- 'audit', 'dedup', 'forget', 'invalidate', 'reject', 'resolve', 'sleep', 'supersede',
188
+ 'audit', 'dedup', 'forget', 'invalidate', 'projects', 'reject', 'resolve', 'sleep', 'supersede',
185
189
  ]);
186
190
  // Commands that honour --dry-run. Any other command would ignore it and run for real.
187
191
  const DRY_RUN_COMMANDS = new Set([
@@ -637,18 +641,18 @@ async function cmdRemember(hippoRoot, text, flags) {
637
641
  // 'archived' is an internal sentinel set only inside archiveRawMemory's transaction.
638
642
  const userVisibleKinds = ['distilled', 'superseded'];
639
643
  if (kindFlag !== undefined && !userVisibleKinds.includes(kindFlag)) {
640
- console.error(`Invalid --kind: "${kindFlagRaw}". Must be one of: ${userVisibleKinds.join(', ')}`);
641
- console.error(`(kind='raw' is reserved for ingestion connectors; kind='archived' is internal.)`);
644
+ printError(`Invalid --kind: "${kindFlagRaw}". Must be one of: ${userVisibleKinds.join(', ')}`);
645
+ printError(`(kind='raw' is reserved for ingestion connectors; kind='archived' is internal.)`);
642
646
  process.exit(1);
643
647
  }
644
648
  const ownerRaw = typeof flags['owner'] === 'string' ? flags['owner'] : null;
645
649
  const ownerCheck = validateOwner(ownerRaw, { strict: isStrictOwnerEnv() });
646
650
  if (!ownerCheck.ok) {
647
- console.error(ownerCheck.message);
651
+ printError(ownerCheck.message);
648
652
  process.exit(1);
649
653
  }
650
654
  if (ownerCheck.message)
651
- console.error(ownerCheck.message);
655
+ printError(ownerCheck.message);
652
656
  const ownerFlag = ownerCheck.value ?? null;
653
657
  const artifactRefFlag = typeof flags['artifact-ref'] === 'string' ? flags['artifact-ref'] : null;
654
658
  const scopeForEnvelope = typeof flags['scope'] === 'string' ? flags['scope'].trim() || null : null;
@@ -698,7 +702,7 @@ async function cmdRemember(hippoRoot, text, flags) {
698
702
  if (entry.pinned)
699
703
  console.log(' Pinned (no decay)');
700
704
  for (const w of vetSecrets(entry.content, entry.tags, false).warnings)
701
- console.error(`Warning: ${w}`);
705
+ printError(`Warning: ${w}`);
702
706
  void embedMemory(targetRoot, entry);
703
707
  const config = loadConfig(targetRoot);
704
708
  const shouldExtract = flags['extract'] || config.extraction.enabled === true;
@@ -709,31 +713,31 @@ async function cmdRemember(hippoRoot, text, flags) {
709
713
  const facts = await extractFacts(entry.content, {
710
714
  apiKey,
711
715
  model: config.extraction.model,
712
- onError: (msg) => console.error(` (extraction failed: ${msg})`),
716
+ onError: (msg) => printError(` (extraction failed: ${msg})`),
713
717
  });
714
718
  if (facts.length > 0) {
715
719
  storeExtractedFacts(targetRoot, entry, facts);
716
- console.error(` extracted ${facts.length} fact(s)`);
720
+ printError(` extracted ${facts.length} fact(s)`);
717
721
  }
718
722
  }
719
723
  catch (err) {
720
724
  // Extraction is best-effort: report it, never block remember.
721
- console.error(` (extraction failed: ${err instanceof Error ? err.message : String(err)})`);
725
+ printError(` (extraction failed: ${err instanceof Error ? err.message : String(err)})`);
722
726
  }
723
727
  }
724
728
  else if (shouldExtract && !apiKey) {
725
- console.error(' (extraction skipped: ANTHROPIC_API_KEY not set)');
729
+ printError(' (extraction skipped: ANTHROPIC_API_KEY not set)');
726
730
  }
727
731
  }
728
732
  function cmdSupersede(hippoRoot, oldId, newContent, flags) {
729
733
  requireInit(hippoRoot);
730
734
  const old = readEntry(hippoRoot, oldId, resolveTenantId({}));
731
735
  if (!old) {
732
- console.error(`Error: memory ${oldId} not found.`);
736
+ printError(`Error: memory ${oldId} not found.`);
733
737
  process.exit(1);
734
738
  }
735
739
  if (old.superseded_by) {
736
- console.error(`Error: memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
740
+ printError(`Error: memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
737
741
  process.exit(1);
738
742
  }
739
743
  const layer = typeof flags['layer'] === 'string' ? flags['layer'] : undefined;
@@ -765,7 +769,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
765
769
  }
766
770
  catch (err) {
767
771
  if (err instanceof RejectedValueError) {
768
- console.error(`Error: ${err.message}`);
772
+ printError(`Error: ${err.message}`);
769
773
  process.exit(1);
770
774
  }
771
775
  throw err;
@@ -777,7 +781,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
777
781
  }
778
782
  function failWith(message) {
779
783
  return () => {
780
- console.error(message);
784
+ printError(message);
781
785
  process.exit(1);
782
786
  };
783
787
  }
@@ -839,7 +843,7 @@ function parseRerankerFlag(flags) {
839
843
  return {};
840
844
  const topK = flags['reranker-top-k'] !== undefined
841
845
  ? parseInt(String(flags['reranker-top-k']), 10)
842
- : name === 'jev' ? JEV_DEFAULT_TOP_K : 50;
846
+ : name === 'jev' || isClefModel(name) ? JEV_DEFAULT_TOP_K : 50;
843
847
  return { value: { fn, topK } };
844
848
  }
845
849
  function parseSalienceFlag(flags) {
@@ -917,7 +921,7 @@ async function cmdRecall(hippoRoot, query, flags) {
917
921
  const entryText = (r) => recallEntryText(r, query, showWhy, primaryIsGlobal || (globalOn && !localIndex.entries[r.entry.id]));
918
922
  const printCost = (r) => printedTokens(entryText(r));
919
923
  const entryBudget = Math.max(0, budget - printedTokens(recallHeading(budget, budget, query)));
920
- const rank = await rankRecall({ hippoRoot, globalRoot: globalRoot !== hippoRoot && globalOn ? globalRoot : undefined, tenantId, note: (line) => console.error(line) }, {
924
+ const rank = await rankRecall({ hippoRoot, globalRoot: globalRoot !== hippoRoot && globalOn ? globalRoot : undefined, tenantId, note: (line) => printError(line) }, {
921
925
  query, budget: entryBudget, cost: printCost, limit, why: showWhy, includeSuperseded, asOf,
922
926
  explicitScope: recallExplicitScope, activeScope: recallActiveScope,
923
927
  search: { ...engineFlags(flags, config), multihop: flags['multihop'] === true || config.multihop.enabled, graphStream, minResults, explain: false },
@@ -1281,7 +1285,7 @@ function noteScopeHidden(hippoRoot, globalRoot, query, tenantId, requested) {
1281
1285
  // Window-capped, so the count is a floor on large stores; fine for a "why is my row missing" hint.
1282
1286
  const hidden = probe.filter((e) => !api.passesCliRecallScopeFilter(e.scope ?? null, requested)).length;
1283
1287
  if (hidden > 0) {
1284
- console.error(`[note] ${hidden} candidate${hidden === 1 ? '' : 's'} hidden by recall scope policy (pass an explicit --scope to inspect).`);
1288
+ printError(`[note] ${hidden} candidate${hidden === 1 ? '' : 's'} hidden by recall scope policy (pass an explicit --scope to inspect).`);
1285
1289
  }
1286
1290
  }
1287
1291
  async function cmdExplain(hippoRoot, query, flags) {
@@ -1441,7 +1445,7 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
1441
1445
  baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
1442
1446
  }
1443
1447
  catch {
1444
- console.error(`Warning: eval baseline ${baselinePath} is unreadable; running without it.`);
1448
+ printError(`Warning: eval baseline ${baselinePath} is unreadable; running without it.`);
1445
1449
  }
1446
1450
  }
1447
1451
  const result = await runFeatureEval(version);
@@ -1466,11 +1470,11 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
1466
1470
  return;
1467
1471
  }
1468
1472
  if (!corpusPath) {
1469
- console.error('Usage: hippo eval <corpus.json> OR hippo eval --suite [--save-baseline] OR hippo eval --bootstrap');
1473
+ printError('Usage: hippo eval <corpus.json> OR hippo eval --suite [--save-baseline] OR hippo eval --bootstrap');
1470
1474
  process.exit(1);
1471
1475
  }
1472
1476
  if (!fs.existsSync(corpusPath)) {
1473
- console.error(`Corpus file not found: ${corpusPath}`);
1477
+ printError(`Corpus file not found: ${corpusPath}`);
1474
1478
  process.exit(1);
1475
1479
  }
1476
1480
  let cases;
@@ -1481,7 +1485,7 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
1481
1485
  throw new Error('Corpus JSON must be an array or { cases: [...] }');
1482
1486
  }
1483
1487
  catch (err) {
1484
- console.error(`Failed to read corpus: ${err instanceof Error ? err.message : err}`);
1488
+ printError(`Failed to read corpus: ${err instanceof Error ? err.message : err}`);
1485
1489
  process.exit(1);
1486
1490
  }
1487
1491
  const globalRoot = getGlobalRoot();
@@ -1539,12 +1543,12 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
1539
1543
  }
1540
1544
  }
1541
1545
  if (minMrr !== null && summary.meanMrr < minMrr) {
1542
- console.error(`MRR ${fmt(summary.meanMrr, 4)} below threshold ${minMrr}`);
1546
+ printError(`MRR ${fmt(summary.meanMrr, 4)} below threshold ${minMrr}`);
1543
1547
  process.exit(1);
1544
1548
  }
1545
1549
  if (comparePath) {
1546
1550
  if (!fs.existsSync(comparePath)) {
1547
- console.error(`Baseline file not found: ${comparePath}`);
1551
+ printError(`Baseline file not found: ${comparePath}`);
1548
1552
  process.exit(1);
1549
1553
  }
1550
1554
  let baseline;
@@ -1552,14 +1556,14 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
1552
1556
  baseline = JSON.parse(fs.readFileSync(comparePath, 'utf8'));
1553
1557
  }
1554
1558
  catch (err) {
1555
- console.error(`Failed to parse baseline: ${err instanceof Error ? err.message : err}`);
1559
+ printError(`Failed to parse baseline: ${err instanceof Error ? err.message : err}`);
1556
1560
  process.exit(1);
1557
1561
  }
1558
1562
  const cmp = compareSummaries(baseline, summary);
1559
1563
  if (asJson) {
1560
1564
  // The main JSON output already emitted; append comparison to stderr so
1561
1565
  // both can be captured independently.
1562
- console.error(JSON.stringify({ compare: cmp }, null, 2));
1566
+ printError(JSON.stringify({ compare: cmp }, null, 2));
1563
1567
  }
1564
1568
  else {
1565
1569
  console.log();
@@ -1596,11 +1600,11 @@ function cmdTraceRecord(hippoRoot, flags) {
1596
1600
  const outcome = String(flags['outcome'] ?? '').trim();
1597
1601
  const validOutcomes = ['success', 'failure', 'partial'];
1598
1602
  if (!task || !stepsJson || !outcome) {
1599
- console.error('Usage: hippo trace record --task <t> --steps <json> --outcome <success|failure|partial> [--session <id>] [--tag <t>]');
1603
+ printError('Usage: hippo trace record --task <t> --steps <json> --outcome <success|failure|partial> [--session <id>] [--tag <t>]');
1600
1604
  process.exit(1);
1601
1605
  }
1602
1606
  if (!validOutcomes.includes(outcome)) {
1603
- console.error(`Invalid outcome: "${outcome}". Must be one of: ${validOutcomes.join(', ')}.`);
1607
+ printError(`Invalid outcome: "${outcome}". Must be one of: ${validOutcomes.join(', ')}.`);
1604
1608
  process.exit(1);
1605
1609
  }
1606
1610
  let steps;
@@ -1608,7 +1612,7 @@ function cmdTraceRecord(hippoRoot, flags) {
1608
1612
  steps = parseSteps(stepsJson);
1609
1613
  }
1610
1614
  catch (err) {
1611
- console.error(String(err instanceof Error ? err.message : err));
1615
+ printError(String(err instanceof Error ? err.message : err));
1612
1616
  process.exit(1);
1613
1617
  }
1614
1618
  const sessionId = String(flags['session'] ?? '').trim() || null;
@@ -1648,7 +1652,7 @@ function cmdTrace(hippoRoot, id, flags) {
1648
1652
  sourceLabel = 'global';
1649
1653
  }
1650
1654
  if (!entry) {
1651
- console.error(`Memory not found: ${id}`);
1655
+ printError(`Memory not found: ${id}`);
1652
1656
  process.exit(1);
1653
1657
  }
1654
1658
  const now = evalNow();
@@ -1743,7 +1747,7 @@ async function cmdRefine(hippoRoot, flags) {
1743
1747
  requireInit(hippoRoot);
1744
1748
  const apiKey = process.env.ANTHROPIC_API_KEY;
1745
1749
  if (!apiKey) {
1746
- console.error('hippo refine needs ANTHROPIC_API_KEY in the environment.');
1750
+ printError('hippo refine needs ANTHROPIC_API_KEY in the environment.');
1747
1751
  process.exit(1);
1748
1752
  }
1749
1753
  const dryRun = Boolean(flags['dry-run']);
@@ -1778,7 +1782,7 @@ function cmdDedup(hippoRoot, flags) {
1778
1782
  requireInit(hippoRoot);
1779
1783
  const dryRun = Boolean(flags['dry-run']);
1780
1784
  if (flags['threshold'] !== undefined) {
1781
- console.error('hippo dedup: --threshold is ignored; a duplicate is the same text apart from spacing.');
1785
+ printError('hippo dedup: --threshold is ignored; a duplicate is the same text apart from spacing.');
1782
1786
  }
1783
1787
  const entries = loadAllEntries(hippoRoot);
1784
1788
  console.log(`Scanning ${entries.length} memories for duplicates (same text apart from spacing)${dryRun ? ' (dry run)' : ''}...\n`);
@@ -1830,14 +1834,15 @@ function cmdLastSleep(flags) {
1830
1834
  content = fs.readFileSync(logPath, 'utf8');
1831
1835
  }
1832
1836
  catch {
1837
+ // Removed or locked since the exists check: there is nothing to show this session.
1833
1838
  return;
1834
1839
  }
1835
1840
  if (content.trim().length > 0) {
1836
- console.error('=== Previous session hippo consolidation ===');
1841
+ printError('=== Previous session hippo consolidation ===');
1837
1842
  process.stderr.write(content);
1838
1843
  if (!content.endsWith('\n'))
1839
- console.error();
1840
- console.error('===========================================');
1844
+ printError();
1845
+ printError('===========================================');
1841
1846
  }
1842
1847
  if (!flags['keep']) {
1843
1848
  try {
@@ -1888,6 +1893,7 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
1888
1893
  payload = JSON.parse(stdinText.trim());
1889
1894
  }
1890
1895
  catch {
1896
+ // Malformed JSON is handled as a null payload by the fail-closed check below.
1891
1897
  payload = null;
1892
1898
  }
1893
1899
  if (!payload || typeof payload !== 'object') {
@@ -1938,7 +1944,7 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
1938
1944
  }
1939
1945
  }
1940
1946
  catch (err) {
1941
- console.error(`hippo compact-resume: trail skipped: ${err instanceof Error ? err.message : String(err)}`);
1947
+ log.warn(`hippo compact-resume: trail skipped: ${err instanceof Error ? err.message : String(err)}`);
1942
1948
  }
1943
1949
  // Printed in one write so the ledger books exactly the text the model is handed.
1944
1950
  const text = captureConsole(() => {
@@ -1961,7 +1967,7 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
1961
1967
  }
1962
1968
  catch (err) {
1963
1969
  // Empty stdout on any store error, never a crashed SessionStart; the reason goes to stderr, which the model never sees.
1964
- console.error(`hippo compact-resume: skipped: ${err instanceof Error ? err.message : String(err)}`);
1970
+ log.warn(`hippo compact-resume: skipped: ${err instanceof Error ? err.message : String(err)}`);
1965
1971
  }
1966
1972
  process.exit(0);
1967
1973
  }
@@ -2200,7 +2206,7 @@ function cmdCodexRun(hippoRoot, args) {
2200
2206
  }
2201
2207
  const child = spawnRealCodex(metadata.realCodexPath, args, process.cwd());
2202
2208
  child.on('error', (err) => {
2203
- console.error(`Failed to launch Codex: ${err.message}`);
2209
+ printError(`Failed to launch Codex: ${err.message}`);
2204
2210
  process.exit(1);
2205
2211
  });
2206
2212
  child.on('exit', async (code, signal) => {
@@ -2247,6 +2253,7 @@ function cmdCodexRun(hippoRoot, args) {
2247
2253
  process.kill(process.pid, signal);
2248
2254
  }
2249
2255
  catch {
2256
+ // Cannot re-raise the child's signal on this platform; a non-zero exit still reports the failure.
2250
2257
  process.exit(1);
2251
2258
  }
2252
2259
  return;
@@ -2417,6 +2424,7 @@ function cmdStatus(hippoRoot) {
2417
2424
  return resolveEmbeddingProvider(hippoRoot);
2418
2425
  }
2419
2426
  catch {
2427
+ // Status reports a bad provider config as "misconfigured" below instead of failing.
2420
2428
  return null;
2421
2429
  }
2422
2430
  })();
@@ -2493,7 +2501,7 @@ function cmdOutcome(hippoRoot, flags) {
2493
2501
  const good = Boolean(flags['good']);
2494
2502
  const bad = Boolean(flags['bad']);
2495
2503
  if (!good && !bad) {
2496
- console.error('Specify --good or --bad');
2504
+ printError('Specify --good or --bad');
2497
2505
  process.exit(1);
2498
2506
  }
2499
2507
  // Behavior fix (v1.11.3): cmdOutcome used to bypass api.outcome and do its
@@ -2536,7 +2544,7 @@ function cmdForget(hippoRoot, id, flags) {
2536
2544
  if (flags['archive'] === true) {
2537
2545
  const reason = typeof flags['reason'] === 'string' ? flags['reason'] : null;
2538
2546
  if (!reason) {
2539
- console.error(ARCHIVE_REASON_REQUIRED);
2547
+ printError(ARCHIVE_REASON_REQUIRED);
2540
2548
  process.exit(1);
2541
2549
  }
2542
2550
  try {
@@ -2544,7 +2552,7 @@ function cmdForget(hippoRoot, id, flags) {
2544
2552
  console.log(`Archived ${id}`);
2545
2553
  }
2546
2554
  catch (err) {
2547
- console.error(`Could not archive ${id}: ${err instanceof Error ? err.message : String(err)}`);
2555
+ printError(`Could not archive ${id}: ${err instanceof Error ? err.message : String(err)}`);
2548
2556
  process.exit(1);
2549
2557
  }
2550
2558
  return;
@@ -2558,16 +2566,16 @@ function cmdForget(hippoRoot, id, flags) {
2558
2566
  if (/append-only/i.test(msg)) {
2559
2567
  // The delete was refused by the append-only trigger — this is a raw
2560
2568
  // memory, not a missing one. Point the user at the archive path.
2561
- console.error(rawForgetRefusal(id));
2569
+ printError(rawForgetRefusal(id));
2562
2570
  }
2563
2571
  else if (api.isDormant(ctx, id)) {
2564
2572
  // Sleep moved it to the dormant store: it is not in active memory, so
2565
2573
  // point at the command that owns it.
2566
- console.error(`${id} is dormant, not in active memory. Delete it for good: hippo dormant forget ${id} ` +
2574
+ printError(`${id} is dormant, not in active memory. Delete it for good: hippo dormant forget ${id} ` +
2567
2575
  `(or bring it back: hippo dormant restore ${id})`);
2568
2576
  }
2569
2577
  else {
2570
- console.error(`Memory not found: ${id}`);
2578
+ printError(`Memory not found: ${id}`);
2571
2579
  }
2572
2580
  process.exit(1);
2573
2581
  }
@@ -2580,15 +2588,15 @@ function previewForget(hippoRoot, id, archive) {
2580
2588
  requireInit(hippoRoot);
2581
2589
  const entry = readEntry(hippoRoot, id, resolveTenantId({}));
2582
2590
  if (!entry) {
2583
- console.error(`Memory not found: ${id}`);
2591
+ printError(`Memory not found: ${id}`);
2584
2592
  process.exit(1);
2585
2593
  }
2586
2594
  if (!archive && entry.kind === 'raw') {
2587
- console.error(rawForgetRefusal(id));
2595
+ printError(rawForgetRefusal(id));
2588
2596
  process.exit(1);
2589
2597
  }
2590
2598
  if (archive && entry.kind !== 'raw') {
2591
- console.error(`Could not archive ${id}: memory ${id} is not raw (kind=${entry.kind})`);
2599
+ printError(`Could not archive ${id}: memory ${id} is not raw (kind=${entry.kind})`);
2592
2600
  process.exit(1);
2593
2601
  }
2594
2602
  const snippet = entry.content.length > 80 ? `${entry.content.slice(0, 80)}...` : entry.content;
@@ -2598,7 +2606,7 @@ function cmdInspect(hippoRoot, id) {
2598
2606
  requireInit(hippoRoot);
2599
2607
  const entry = readEntry(hippoRoot, id, resolveTenantId({}));
2600
2608
  if (!entry) {
2601
- console.error(`Memory not found: ${id}`);
2609
+ printError(`Memory not found: ${id}`);
2602
2610
  process.exit(1);
2603
2611
  }
2604
2612
  const now = evalNow();
@@ -2661,7 +2669,7 @@ function cmdResolve(hippoRoot, args, flags) {
2661
2669
  // Accept "42" or "conflict_42"
2662
2670
  const conflictId = parseInt(rawId.replace(/^conflict_/, ''), 10);
2663
2671
  if (isNaN(conflictId)) {
2664
- console.error('Usage: hippo resolve <conflict_id> --keep <memory_id> [--forget]');
2672
+ printError('Usage: hippo resolve <conflict_id> --keep <memory_id> [--forget]');
2665
2673
  process.exit(1);
2666
2674
  }
2667
2675
  const tenantId = resolveTenantId({});
@@ -2671,7 +2679,7 @@ function cmdResolve(hippoRoot, args, flags) {
2671
2679
  const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId);
2672
2680
  const conflict = conflicts.find((c) => c.id === conflictId);
2673
2681
  if (!conflict) {
2674
- console.error(`Conflict ${conflictId} not found or already resolved.`);
2682
+ printError(`Conflict ${conflictId} not found or already resolved.`);
2675
2683
  process.exit(1);
2676
2684
  }
2677
2685
  console.log(`Conflict ${conflictId}:`);
@@ -2705,7 +2713,7 @@ function cmdResolve(hippoRoot, args, flags) {
2705
2713
  reason: reasonFlag,
2706
2714
  });
2707
2715
  if (!result) {
2708
- console.error(`Could not resolve conflict ${conflictId}. Check the ID and --keep value.`);
2716
+ printError(`Could not resolve conflict ${conflictId}. Check the ID and --keep value.`);
2709
2717
  process.exit(1);
2710
2718
  }
2711
2719
  const action = rejectLoser
@@ -2728,20 +2736,20 @@ function cmdReject(hippoRoot, args, flags) {
2728
2736
  // content, so reason is its only human-readable identity.
2729
2737
  const reason = typeof flags['reason'] === 'string' ? flags['reason'].trim() : '';
2730
2738
  if (!reason) {
2731
- console.error('hippo reject requires --reason "<why>" (the tombstone stores no content; reason is its only identity).');
2739
+ printError('hippo reject requires --reason "<why>" (the tombstone stores no content; reason is its only identity).');
2732
2740
  process.exit(1);
2733
2741
  }
2734
2742
  const valueFlag = typeof flags['value'] === 'string' ? flags['value'] : undefined;
2735
2743
  const memoryId = args[0];
2736
2744
  if (!memoryId && valueFlag === undefined) {
2737
- console.error('Usage: hippo reject <memory-id> --reason "<why>"');
2738
- console.error(' or: hippo reject --value "<text>" --reason "<why>"');
2745
+ printError('Usage: hippo reject <memory-id> --reason "<why>"');
2746
+ printError(' or: hippo reject --value "<text>" --reason "<why>"');
2739
2747
  process.exit(1);
2740
2748
  }
2741
2749
  if (memoryId && valueFlag !== undefined) {
2742
2750
  // Ambiguous ask: silently preferring one form would ignore the other
2743
2751
  // without feedback (code-review round-1 low).
2744
- console.error('hippo reject takes EITHER a memory id OR --value, not both.');
2752
+ printError('hippo reject takes EITHER a memory id OR --value, not both.');
2745
2753
  process.exit(1);
2746
2754
  }
2747
2755
  try {
@@ -2771,7 +2779,7 @@ function cmdReject(hippoRoot, args, flags) {
2771
2779
  }
2772
2780
  }
2773
2781
  catch (err) {
2774
- console.error(`Could not reject: ${err instanceof Error ? err.message : String(err)}`);
2782
+ printError(`Could not reject: ${err instanceof Error ? err.message : String(err)}`);
2775
2783
  process.exit(1);
2776
2784
  }
2777
2785
  }
@@ -2805,18 +2813,18 @@ function cmdUnreject(hippoRoot, args, flags) {
2805
2813
  const tenantId = resolveTenantId({});
2806
2814
  const digestOrPrefix = (args[0] ?? '').trim();
2807
2815
  if (!digestOrPrefix) {
2808
- console.error('Usage: hippo unreject <digest-or-prefix>');
2816
+ printError('Usage: hippo unreject <digest-or-prefix>');
2809
2817
  process.exit(1);
2810
2818
  }
2811
2819
  const outcome = unrejectValue(root, tenantId, digestOrPrefix, 'cli');
2812
2820
  if (outcome.status === 'not_found') {
2813
- console.error(`No rejected value matches "${digestOrPrefix}". Run \`hippo rejections\` to list tombstones.`);
2821
+ printError(`No rejected value matches "${digestOrPrefix}". Run \`hippo rejections\` to list tombstones.`);
2814
2822
  process.exit(1);
2815
2823
  }
2816
2824
  if (outcome.status === 'ambiguous') {
2817
- console.error(`"${digestOrPrefix}" matches ${outcome.candidates.length} tombstones. Use a longer prefix:`);
2825
+ printError(`"${digestOrPrefix}" matches ${outcome.candidates.length} tombstones. Use a longer prefix:`);
2818
2826
  for (const c of outcome.candidates) {
2819
- console.error(` ${c.digest.slice(0, 16)}... ${c.reason ?? 'none given'}`);
2827
+ printError(` ${c.digest.slice(0, 16)}... ${c.reason ?? 'none given'}`);
2820
2828
  }
2821
2829
  process.exit(1);
2822
2830
  }
@@ -2839,7 +2847,7 @@ function cmdDormant(hippoRoot, args, flags) {
2839
2847
  if (sub === 'restore' || sub === 'forget') {
2840
2848
  const id = (args[1] ?? '').trim();
2841
2849
  if (!id) {
2842
- console.error(`Usage: hippo dormant ${sub} <id>`);
2850
+ printError(`Usage: hippo dormant ${sub} <id>`);
2843
2851
  process.exit(1);
2844
2852
  }
2845
2853
  try {
@@ -2854,10 +2862,10 @@ function cmdDormant(hippoRoot, args, flags) {
2854
2862
  }
2855
2863
  catch (err) {
2856
2864
  if (err instanceof RejectedValueError) {
2857
- console.error(`Cannot restore ${id}: its value was rejected (${err.reason ?? 'no reason given'}). Run \`hippo unreject\` first to allow it.`);
2865
+ printError(`Cannot restore ${id}: its value was rejected (${err.reason ?? 'no reason given'}). Run \`hippo unreject\` first to allow it.`);
2858
2866
  }
2859
2867
  else {
2860
- console.error(`Could not ${sub} ${id}: ${err instanceof Error ? err.message : String(err)}`);
2868
+ printError(`Could not ${sub} ${id}: ${err instanceof Error ? err.message : String(err)}`);
2861
2869
  }
2862
2870
  process.exit(1);
2863
2871
  }
@@ -2902,7 +2910,7 @@ function cmdQuarantine(hippoRoot, args, flags) {
2902
2910
  if (sub === 'approve' || sub === 'reject') {
2903
2911
  const id = (args[1] ?? '').trim();
2904
2912
  if (!id) {
2905
- console.error(`Usage: hippo quarantine ${sub} <id>`);
2913
+ printError(`Usage: hippo quarantine ${sub} <id>`);
2906
2914
  process.exit(1);
2907
2915
  }
2908
2916
  try {
@@ -2916,7 +2924,7 @@ function cmdQuarantine(hippoRoot, args, flags) {
2916
2924
  }
2917
2925
  }
2918
2926
  catch (err) {
2919
- console.error(`Could not ${sub} ${id}: ${err instanceof Error ? err.message : String(err)}`);
2927
+ printError(`Could not ${sub} ${id}: ${err instanceof Error ? err.message : String(err)}`);
2920
2928
  process.exit(1);
2921
2929
  }
2922
2930
  return;
@@ -3033,7 +3041,7 @@ function cmdSnapshot(hippoRoot, args, flags) {
3033
3041
  const nextStep = String(flags['next-step'] ?? '').trim();
3034
3042
  const sessionId = String(flags['session'] ?? flags['id'] ?? '').trim();
3035
3043
  if (!task || !summary || !nextStep) {
3036
- console.error('Usage: hippo snapshot save --task <task> --summary <summary> --next-step <step> [--source <source>] [--session <session-id>]');
3044
+ printError('Usage: hippo snapshot save --task <task> --summary <summary> --next-step <step> [--source <source>] [--session <session-id>]');
3037
3045
  process.exit(1);
3038
3046
  }
3039
3047
  const snapshot = saveActiveTaskSnapshot(hippoRoot, resolveTenantId({}), {
@@ -3078,7 +3086,7 @@ function cmdSnapshot(hippoRoot, args, flags) {
3078
3086
  printActiveTaskSnapshot(snapshot);
3079
3087
  return;
3080
3088
  }
3081
- console.error('Usage: hippo snapshot <save|show|clear>');
3089
+ printError('Usage: hippo snapshot <save|show|clear>');
3082
3090
  process.exit(1);
3083
3091
  }
3084
3092
  function cmdSession(hippoRoot, args, flags) {
@@ -3091,7 +3099,7 @@ function cmdSession(hippoRoot, args, flags) {
3091
3099
  const eventType = String(flags['type'] ?? 'note').trim();
3092
3100
  const content = String(flags['content'] ?? '').trim();
3093
3101
  if (!sessionId || !content) {
3094
- console.error('Usage: hippo session log --id <session-id> --content <text> [--type <type>] [--task <task>] [--source <source>]');
3102
+ printError('Usage: hippo session log --id <session-id> --content <text> [--type <type>] [--task <task>] [--source <source>]');
3095
3103
  process.exit(1);
3096
3104
  }
3097
3105
  const event = appendSessionEvent(hippoRoot, resolveTenantId({}), {
@@ -3143,11 +3151,11 @@ function cmdSession(hippoRoot, args, flags) {
3143
3151
  const outcomeRaw = String(flags['outcome'] ?? '').trim();
3144
3152
  const summary = String(flags['summary'] ?? '').trim();
3145
3153
  if (!sessionId) {
3146
- console.error('Usage: hippo session complete --session <session-id> --outcome <success|failure|partial> [--summary "..."]');
3154
+ printError('Usage: hippo session complete --session <session-id> --outcome <success|failure|partial> [--summary "..."]');
3147
3155
  process.exit(1);
3148
3156
  }
3149
3157
  if (!isHandoffOutcome(outcomeRaw)) {
3150
- console.error(`Invalid outcome: "${outcomeRaw}". Must be one of: success, failure, partial.`);
3158
+ printError(`Invalid outcome: "${outcomeRaw}". Must be one of: success, failure, partial.`);
3151
3159
  process.exit(1);
3152
3160
  }
3153
3161
  const outcome = outcomeRaw;
@@ -3214,7 +3222,7 @@ function cmdSession(hippoRoot, args, flags) {
3214
3222
  console.log(lines.join('\n'));
3215
3223
  return;
3216
3224
  }
3217
- console.error('Usage: hippo session <log|show|latest|resume|complete>');
3225
+ printError('Usage: hippo session <log|show|latest|resume|complete>');
3218
3226
  process.exit(1);
3219
3227
  }
3220
3228
  function cmdHandoff(hippoRoot, args, flags) {
@@ -3223,12 +3231,12 @@ function cmdHandoff(hippoRoot, args, flags) {
3223
3231
  if (subcommand === 'create') {
3224
3232
  const summary = String(flags['summary'] ?? '').trim();
3225
3233
  if (!summary) {
3226
- console.error('Usage: hippo handoff create --summary "..." [--next "..."] [--session <id>] [--task <id>] [--artifact <path>...] [--constraint <text>...] [--outcome <success|failure|partial>] [--target-runtime <name>] [--card-id <id>] [--tests <pass|fail|unknown>]');
3234
+ printError('Usage: hippo handoff create --summary "..." [--next "..."] [--session <id>] [--task <id>] [--artifact <path>...] [--constraint <text>...] [--outcome <success|failure|partial>] [--target-runtime <name>] [--card-id <id>] [--tests <pass|fail|unknown>]');
3227
3235
  process.exit(1);
3228
3236
  }
3229
3237
  const outcomeRaw = flags['outcome'];
3230
3238
  if (outcomeRaw !== undefined && !isHandoffOutcome(outcomeRaw)) {
3231
- console.error(`Invalid outcome: "${String(outcomeRaw)}". Must be one of: success, failure, partial.`);
3239
+ printError(`Invalid outcome: "${String(outcomeRaw)}". Must be one of: success, failure, partial.`);
3232
3240
  process.exit(1);
3233
3241
  }
3234
3242
  const sessionId = String(flags['session'] ?? flags['id'] ?? '').trim() || `fallback-${Date.now()}-${process.pid}`;
@@ -3246,7 +3254,7 @@ function cmdHandoff(hippoRoot, args, flags) {
3246
3254
  for (const name of ['target-runtime', 'card-id']) {
3247
3255
  // parseArgs turns a value-less flag into `true`; refuse rather than store "true".
3248
3256
  if (flags[name] === true) {
3249
- console.error(`--${name} needs a value`);
3257
+ printError(`--${name} needs a value`);
3250
3258
  process.exit(1);
3251
3259
  }
3252
3260
  }
@@ -3311,12 +3319,12 @@ function cmdHandoff(hippoRoot, args, flags) {
3311
3319
  if (subcommand === 'show') {
3312
3320
  const idArg = args[1];
3313
3321
  if (!idArg) {
3314
- console.error('Usage: hippo handoff show <id> [--json]');
3322
+ printError('Usage: hippo handoff show <id> [--json]');
3315
3323
  process.exit(1);
3316
3324
  }
3317
3325
  const handoffId = parseInt(idArg, 10);
3318
3326
  if (!Number.isFinite(handoffId) || handoffId <= 0) {
3319
- console.error(`Invalid handoff ID: ${idArg}`);
3327
+ printError(`Invalid handoff ID: ${idArg}`);
3320
3328
  process.exit(1);
3321
3329
  }
3322
3330
  const handoff = loadHandoffById(hippoRoot, resolveTenantId({}), handoffId);
@@ -3336,7 +3344,7 @@ function cmdHandoff(hippoRoot, args, flags) {
3336
3344
  printHandoff(handoff);
3337
3345
  return;
3338
3346
  }
3339
- console.error('Usage: hippo handoff <create|latest|show>');
3347
+ printError('Usage: hippo handoff <create|latest|show>');
3340
3348
  process.exit(1);
3341
3349
  }
3342
3350
  // Mirrors ARCHIVE_REASON_REQUIRED so the block message can't drift from its usage line.
@@ -3389,7 +3397,7 @@ function cardRunFlag(flags) {
3389
3397
  return undefined;
3390
3398
  const n = Number(raw);
3391
3399
  if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n <= 0) {
3392
- console.error(`Invalid --run: "${raw}" (expected a positive integer).`);
3400
+ printError(`Invalid --run: "${raw}" (expected a positive integer).`);
3393
3401
  process.exit(1);
3394
3402
  }
3395
3403
  return n;
@@ -3422,7 +3430,7 @@ function cmdCard(hippoRoot, args, flags) {
3422
3430
  for (const key of Object.keys(flags)) {
3423
3431
  if (!allowedFlags.includes(key)) {
3424
3432
  const valid = allowedFlags.length > 0 ? allowedFlags.map((f) => `--${f}`).join(', ') : '(none)';
3425
- console.error(`Unknown flag --${key} for hippo card ${subcommand}. Valid flags: ${valid}`);
3433
+ printError(`Unknown flag --${key} for hippo card ${subcommand}. Valid flags: ${valid}`);
3426
3434
  process.exit(1);
3427
3435
  }
3428
3436
  }
@@ -3430,7 +3438,7 @@ function cmdCard(hippoRoot, args, flags) {
3430
3438
  if (subcommand === 'create') {
3431
3439
  const title = cardStringFlag(flags, 'title') ?? '';
3432
3440
  if (!title) {
3433
- console.error('Usage: hippo card create --title "..." [--repo <name>] [--contract <text>] [--budget <n>] [--depends-on <id>...]');
3441
+ printError('Usage: hippo card create --title "..." [--repo <name>] [--contract <text>] [--budget <n>] [--depends-on <id>...]');
3434
3442
  process.exit(1);
3435
3443
  }
3436
3444
  const repo = cardStringFlag(flags, 'repo') || undefined;
@@ -3439,14 +3447,14 @@ function cmdCard(hippoRoot, args, flags) {
3439
3447
  let budget;
3440
3448
  if (budgetRaw !== undefined) {
3441
3449
  if (!/^\d+$/.test(budgetRaw)) {
3442
- console.error(`Invalid budget: "${budgetRaw}" (expected a positive integer)`);
3450
+ printError(`Invalid budget: "${budgetRaw}" (expected a positive integer)`);
3443
3451
  process.exit(1);
3444
3452
  }
3445
3453
  budget = Number(budgetRaw);
3446
3454
  }
3447
3455
  const dependsOnFlag = flags['depends-on'];
3448
3456
  if (dependsOnFlag === true) {
3449
- console.error('--depends-on requires a value');
3457
+ printError('--depends-on requires a value');
3450
3458
  process.exit(1);
3451
3459
  }
3452
3460
  const dependsOn = Array.isArray(dependsOnFlag) ? dependsOnFlag : [];
@@ -3455,7 +3463,7 @@ function cmdCard(hippoRoot, args, flags) {
3455
3463
  card = createCard(hippoRoot, tenantId, { title, repo, contract, budget, dependsOn });
3456
3464
  }
3457
3465
  catch (error) {
3458
- console.error(error instanceof Error ? error.message : String(error));
3466
+ printError(error instanceof Error ? error.message : String(error));
3459
3467
  process.exit(1);
3460
3468
  }
3461
3469
  console.log(`Created card ${card.id} (status: ${card.status})`);
@@ -3464,12 +3472,12 @@ function cmdCard(hippoRoot, args, flags) {
3464
3472
  if (subcommand === 'show') {
3465
3473
  const id = args[1];
3466
3474
  if (!id) {
3467
- console.error('Usage: hippo card show <id> [--json]');
3475
+ printError('Usage: hippo card show <id> [--json]');
3468
3476
  process.exit(1);
3469
3477
  }
3470
3478
  const detail = loadCardDetail(hippoRoot, tenantId, id);
3471
3479
  if (!detail) {
3472
- console.error(`No card found with id ${id}.`);
3480
+ printError(`No card found with id ${id}.`);
3473
3481
  process.exit(1);
3474
3482
  }
3475
3483
  if (flags['json']) {
@@ -3482,7 +3490,7 @@ function cmdCard(hippoRoot, args, flags) {
3482
3490
  if (subcommand === 'list') {
3483
3491
  const status = cardStringFlag(flags, 'status');
3484
3492
  if (status !== undefined && !isCardStatus(status)) {
3485
- console.error(`Invalid status: "${status}".`);
3493
+ printError(`Invalid status: "${status}".`);
3486
3494
  process.exit(1);
3487
3495
  }
3488
3496
  const cards = listCards(hippoRoot, tenantId, { status });
@@ -3503,7 +3511,7 @@ function cmdCard(hippoRoot, args, flags) {
3503
3511
  const id = args[1];
3504
3512
  const runtime = cardStringFlag(flags, 'runtime') ?? '';
3505
3513
  if (!id || !runtime) {
3506
- console.error('Usage: hippo card claim <id> --runtime <name> [--session <id>]');
3514
+ printError('Usage: hippo card claim <id> --runtime <name> [--session <id>]');
3507
3515
  process.exit(1);
3508
3516
  }
3509
3517
  const sessionId = cardStringFlag(flags, 'session') || undefined;
@@ -3512,11 +3520,11 @@ function cmdCard(hippoRoot, args, flags) {
3512
3520
  card = claimCard(hippoRoot, tenantId, id, runtime, sessionId);
3513
3521
  }
3514
3522
  catch (error) {
3515
- console.error(error instanceof Error ? error.message : String(error));
3523
+ printError(error instanceof Error ? error.message : String(error));
3516
3524
  process.exit(1);
3517
3525
  }
3518
3526
  if (!card) {
3519
- console.error(`Could not claim card ${id} (not ready/blocked, or already claimed).`);
3527
+ printError(`Could not claim card ${id} (not ready/blocked, or already claimed).`);
3520
3528
  process.exit(1);
3521
3529
  }
3522
3530
  console.log(`Claimed card ${card.id} for ${runtime} (run ${card.runId}, lease until ${card.leaseUntil})`);
@@ -3526,7 +3534,7 @@ function cmdCard(hippoRoot, args, flags) {
3526
3534
  const id = args[1];
3527
3535
  const runId = cardRunFlag(flags);
3528
3536
  if (!id || runId === undefined) {
3529
- console.error('Usage: hippo card heartbeat <id> --run <n>');
3537
+ printError('Usage: hippo card heartbeat <id> --run <n>');
3530
3538
  process.exit(1);
3531
3539
  }
3532
3540
  let card;
@@ -3534,11 +3542,11 @@ function cmdCard(hippoRoot, args, flags) {
3534
3542
  card = heartbeatCard(hippoRoot, tenantId, id, runId);
3535
3543
  }
3536
3544
  catch (error) {
3537
- console.error(error instanceof Error ? error.message : String(error));
3545
+ printError(error instanceof Error ? error.message : String(error));
3538
3546
  process.exit(1);
3539
3547
  }
3540
3548
  if (!card) {
3541
- console.error(`Could not heartbeat card ${id} (${cardRefusal(hippoRoot, tenantId, id)}).`);
3549
+ printError(`Could not heartbeat card ${id} (${cardRefusal(hippoRoot, tenantId, id)}).`);
3542
3550
  process.exit(1);
3543
3551
  }
3544
3552
  console.log(`Heartbeat card ${card.id}: lease until ${card.leaseUntil}`);
@@ -3548,7 +3556,7 @@ function cmdCard(hippoRoot, args, flags) {
3548
3556
  const id = args[1];
3549
3557
  const reason = cardStringFlag(flags, 'reason') ?? '';
3550
3558
  if (!id || !reason) {
3551
- console.error(CARD_BLOCK_REASON_REQUIRED);
3559
+ printError(CARD_BLOCK_REASON_REQUIRED);
3552
3560
  process.exit(1);
3553
3561
  }
3554
3562
  const runId = cardRunFlag(flags);
@@ -3557,12 +3565,12 @@ function cmdCard(hippoRoot, args, flags) {
3557
3565
  card = blockCard(hippoRoot, tenantId, id, reason, runId);
3558
3566
  }
3559
3567
  catch (error) {
3560
- console.error(error instanceof Error ? error.message : String(error));
3568
+ printError(error instanceof Error ? error.message : String(error));
3561
3569
  process.exit(1);
3562
3570
  }
3563
3571
  if (!card) {
3564
3572
  const why = runId === undefined ? 'not running' : cardRefusal(hippoRoot, tenantId, id);
3565
- console.error(`Could not block card ${id} (${why}).`);
3573
+ printError(`Could not block card ${id} (${why}).`);
3566
3574
  process.exit(1);
3567
3575
  }
3568
3576
  console.log(`Blocked card ${card.id}`);
@@ -3571,7 +3579,7 @@ function cmdCard(hippoRoot, args, flags) {
3571
3579
  if (subcommand === 'review') {
3572
3580
  const id = args[1];
3573
3581
  if (!id) {
3574
- console.error('Usage: hippo card review <id> [--run <n>]');
3582
+ printError('Usage: hippo card review <id> [--run <n>]');
3575
3583
  process.exit(1);
3576
3584
  }
3577
3585
  const runId = cardRunFlag(flags);
@@ -3580,12 +3588,12 @@ function cmdCard(hippoRoot, args, flags) {
3580
3588
  card = reviewCard(hippoRoot, tenantId, id, runId);
3581
3589
  }
3582
3590
  catch (error) {
3583
- console.error(error instanceof Error ? error.message : String(error));
3591
+ printError(error instanceof Error ? error.message : String(error));
3584
3592
  process.exit(1);
3585
3593
  }
3586
3594
  if (!card) {
3587
3595
  const why = runId === undefined ? 'not running' : cardRefusal(hippoRoot, tenantId, id);
3588
- console.error(`Could not move card ${id} to review (${why}).`);
3596
+ printError(`Could not move card ${id} to review (${why}).`);
3589
3597
  process.exit(1);
3590
3598
  }
3591
3599
  console.log(`Card ${card.id} moved to review`);
@@ -3595,7 +3603,7 @@ function cmdCard(hippoRoot, args, flags) {
3595
3603
  const id = args[1];
3596
3604
  const outcomeRaw = flags['outcome'];
3597
3605
  if (!id || !isHandoffOutcome(outcomeRaw)) {
3598
- console.error('Usage: hippo card complete <id> --outcome <success|failure|partial> [--run <n>]');
3606
+ printError('Usage: hippo card complete <id> --outcome <success|failure|partial> [--run <n>]');
3599
3607
  process.exit(1);
3600
3608
  }
3601
3609
  const runId = cardRunFlag(flags);
@@ -3604,12 +3612,12 @@ function cmdCard(hippoRoot, args, flags) {
3604
3612
  result = completeCard(hippoRoot, tenantId, id, outcomeRaw, runId);
3605
3613
  }
3606
3614
  catch (error) {
3607
- console.error(error instanceof Error ? error.message : String(error));
3615
+ printError(error instanceof Error ? error.message : String(error));
3608
3616
  process.exit(1);
3609
3617
  }
3610
3618
  if (!result) {
3611
3619
  const why = runId === undefined ? 'not in review' : cardRefusal(hippoRoot, tenantId, id);
3612
- console.error(`Could not complete card ${id} (${why}).`);
3620
+ printError(`Could not complete card ${id} (${why}).`);
3613
3621
  process.exit(1);
3614
3622
  }
3615
3623
  console.log(`Completed card ${result.card.id} (status: ${result.card.status})`);
@@ -3620,7 +3628,7 @@ function cmdCard(hippoRoot, args, flags) {
3620
3628
  }
3621
3629
  if (subcommand === 'reclaim') {
3622
3630
  if (args.length > 1) {
3623
- console.error('Usage: hippo card reclaim (sweeps every expired lease; use hippo card block <id> for one card)');
3631
+ printError('Usage: hippo card reclaim (sweeps every expired lease; use hippo card block <id> for one card)');
3624
3632
  process.exit(1);
3625
3633
  }
3626
3634
  const ids = reclaimExpiredCards(hippoRoot, tenantId);
@@ -3636,18 +3644,18 @@ function cmdCard(hippoRoot, args, flags) {
3636
3644
  if (subcommand === 'comment') {
3637
3645
  const id = args[1];
3638
3646
  if (!id) {
3639
- console.error('Usage: hippo card comment <id> --body "..." [--author <name>]');
3647
+ printError('Usage: hippo card comment <id> --body "..." [--author <name>]');
3640
3648
  process.exit(1);
3641
3649
  }
3642
3650
  // Only show and comment look the card up directly; claim/heartbeat/block/review/complete throw from the store instead.
3643
3651
  const card = loadCard(hippoRoot, tenantId, id);
3644
3652
  if (!card) {
3645
- console.error(`No card found with id ${id}.`);
3653
+ printError(`No card found with id ${id}.`);
3646
3654
  process.exit(1);
3647
3655
  }
3648
3656
  const body = cardStringFlag(flags, 'body') ?? '';
3649
3657
  if (!body) {
3650
- console.error('Usage: hippo card comment <id> --body "..." [--author <name>]');
3658
+ printError('Usage: hippo card comment <id> --body "..." [--author <name>]');
3651
3659
  process.exit(1);
3652
3660
  }
3653
3661
  const author = cardStringFlag(flags, 'author') || 'cli';
@@ -3655,7 +3663,7 @@ function cmdCard(hippoRoot, args, flags) {
3655
3663
  console.log(`Added comment ${comment.id} to card ${id}`);
3656
3664
  return;
3657
3665
  }
3658
- console.error('Usage: hippo card <create|show|list|claim|heartbeat|block|review|complete|reclaim|comment>');
3666
+ printError('Usage: hippo card <create|show|list|claim|heartbeat|block|review|complete|reclaim|comment>');
3659
3667
  process.exit(1);
3660
3668
  }
3661
3669
  // ---------------------------------------------------------------------------
@@ -3669,23 +3677,23 @@ function cmdPredict(hippoRoot, args, flags) {
3669
3677
  if (subcommand === 'close') {
3670
3678
  const idRaw = args[1];
3671
3679
  if (!idRaw) {
3672
- console.error('Usage: hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
3680
+ printError('Usage: hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
3673
3681
  process.exit(1);
3674
3682
  }
3675
3683
  const id = parseInt(String(idRaw), 10);
3676
3684
  if (!Number.isFinite(id) || id <= 0) {
3677
- console.error(`Invalid prediction id: "${idRaw}"`);
3685
+ printError(`Invalid prediction id: "${idRaw}"`);
3678
3686
  process.exit(1);
3679
3687
  }
3680
3688
  const stateRaw = typeof flags['state'] === 'string' ? flags['state'].trim() : '';
3681
3689
  if (!predictionsModule.VALID_CLOSURE_STATES.has(stateRaw) || stateRaw === 'open') {
3682
- console.error(`Invalid --state: "${stateRaw}". Must be one of: closed | closed-unknown.`);
3690
+ printError(`Invalid --state: "${stateRaw}". Must be one of: closed | closed-unknown.`);
3683
3691
  process.exit(1);
3684
3692
  }
3685
3693
  const actualRaw = flags['actual'];
3686
3694
  const actualValue = actualRaw !== undefined ? Number(actualRaw) : undefined;
3687
3695
  if (actualRaw !== undefined && !Number.isFinite(actualValue)) {
3688
- console.error(`Invalid --actual: "${actualRaw}". Must be a number.`);
3696
+ printError(`Invalid --actual: "${actualRaw}". Must be a number.`);
3689
3697
  process.exit(1);
3690
3698
  }
3691
3699
  const noteRaw = flags['note'];
@@ -3706,7 +3714,7 @@ function cmdPredict(hippoRoot, args, flags) {
3706
3714
  const limitRaw = flags['limit'];
3707
3715
  const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
3708
3716
  if (!Number.isFinite(limit) || limit <= 0) {
3709
- console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3717
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3710
3718
  process.exit(1);
3711
3719
  }
3712
3720
  let results;
@@ -3730,7 +3738,7 @@ function cmdPredict(hippoRoot, args, flags) {
3730
3738
  }
3731
3739
  else {
3732
3740
  if (!predictionsModule.VALID_CLOSURE_STATES.has(status)) {
3733
- console.error(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
3741
+ printError(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
3734
3742
  process.exit(1);
3735
3743
  }
3736
3744
  if (classTag) {
@@ -3741,7 +3749,7 @@ function cmdPredict(hippoRoot, args, flags) {
3741
3749
  }
3742
3750
  else {
3743
3751
  // status filter without class — scan all classes is more complex; v1 requires --class for non-default status
3744
- console.error('--status filter (non-open) requires --class to be set.');
3752
+ printError('--status filter (non-open) requires --class to be set.');
3745
3753
  process.exit(1);
3746
3754
  }
3747
3755
  }
@@ -3764,17 +3772,17 @@ function cmdPredict(hippoRoot, args, flags) {
3764
3772
  if (subcommand === 'show') {
3765
3773
  const idRaw = args[1];
3766
3774
  if (!idRaw) {
3767
- console.error('Usage: hippo predict show <id>');
3775
+ printError('Usage: hippo predict show <id>');
3768
3776
  process.exit(1);
3769
3777
  }
3770
3778
  const id = parseInt(String(idRaw), 10);
3771
3779
  if (!Number.isFinite(id) || id <= 0) {
3772
- console.error(`Invalid prediction id: "${idRaw}"`);
3780
+ printError(`Invalid prediction id: "${idRaw}"`);
3773
3781
  process.exit(1);
3774
3782
  }
3775
3783
  const pred = predictionsModule.loadPredictionById(hippoRoot, tenantId, id);
3776
3784
  if (!pred) {
3777
- console.error(`Prediction ${id} not found.`);
3785
+ printError(`Prediction ${id} not found.`);
3778
3786
  process.exit(1);
3779
3787
  }
3780
3788
  console.log(`Prediction #${pred.id}`);
@@ -3800,7 +3808,7 @@ function cmdPredict(hippoRoot, args, flags) {
3800
3808
  // J3 reference-class / planning-fallacy detector
3801
3809
  const classTagRaw = flags['class'];
3802
3810
  if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3803
- console.error('Usage: hippo predict baserate --class <c>');
3811
+ printError('Usage: hippo predict baserate --class <c>');
3804
3812
  process.exit(1);
3805
3813
  }
3806
3814
  const baserate = predictionsModule.computePredictionBaserate(hippoRoot, tenantId, classTagRaw.trim());
@@ -3828,22 +3836,22 @@ function cmdPredict(hippoRoot, args, flags) {
3828
3836
  // Default subcommand: create. args[0] is the claim text.
3829
3837
  const claimText = subcommand;
3830
3838
  if (!claimText) {
3831
- console.error('Usage: hippo predict "<claim>" --class <c> [--estimate <v>] [--unit <u>] [--target <YYYY-MM-DD>]');
3832
- console.error(' hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
3833
- console.error(' hippo predict list [--class X] [--status open|closed|closed-unknown|all] [--limit N]');
3834
- console.error(' hippo predict show <id>');
3839
+ printError('Usage: hippo predict "<claim>" --class <c> [--estimate <v>] [--unit <u>] [--target <YYYY-MM-DD>]');
3840
+ printError(' hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
3841
+ printError(' hippo predict list [--class X] [--status open|closed|closed-unknown|all] [--limit N]');
3842
+ printError(' hippo predict show <id>');
3835
3843
  process.exit(1);
3836
3844
  }
3837
3845
  const classTagRaw = flags['class'];
3838
3846
  if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3839
- console.error('--class is required for prediction creation.');
3847
+ printError('--class is required for prediction creation.');
3840
3848
  process.exit(1);
3841
3849
  }
3842
3850
  const classTag = classTagRaw.trim();
3843
3851
  const estimateRaw = flags['estimate'];
3844
3852
  const estimateValue = estimateRaw !== undefined ? Number(estimateRaw) : undefined;
3845
3853
  if (estimateRaw !== undefined && !Number.isFinite(estimateValue)) {
3846
- console.error(`Invalid --estimate: "${estimateRaw}". Must be a number.`);
3854
+ printError(`Invalid --estimate: "${estimateRaw}". Must be a number.`);
3847
3855
  process.exit(1);
3848
3856
  }
3849
3857
  const unitRaw = flags['unit'];
@@ -3871,7 +3879,7 @@ function cmdDecide(hippoRoot, args, flags) {
3871
3879
  const limitRaw = flags['limit'];
3872
3880
  const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
3873
3881
  if (!Number.isFinite(limit) || limit <= 0) {
3874
- console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3882
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3875
3883
  process.exit(1);
3876
3884
  }
3877
3885
  let results;
@@ -3880,7 +3888,7 @@ function cmdDecide(hippoRoot, args, flags) {
3880
3888
  }
3881
3889
  else {
3882
3890
  if (!decisionsModule.VALID_DECISION_STATES.has(status)) {
3883
- console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
3891
+ printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
3884
3892
  process.exit(1);
3885
3893
  }
3886
3894
  results = decisionsModule.loadDecisions(hippoRoot, tenantId, {
@@ -3905,17 +3913,17 @@ function cmdDecide(hippoRoot, args, flags) {
3905
3913
  if (subcommand === 'get') {
3906
3914
  const idRaw = args[1];
3907
3915
  if (!idRaw) {
3908
- console.error('Usage: hippo decide get <id>');
3916
+ printError('Usage: hippo decide get <id>');
3909
3917
  process.exit(1);
3910
3918
  }
3911
3919
  const id = parseInt(String(idRaw), 10);
3912
3920
  if (!Number.isFinite(id) || id <= 0) {
3913
- console.error(`Invalid decision id: "${idRaw}"`);
3921
+ printError(`Invalid decision id: "${idRaw}"`);
3914
3922
  process.exit(1);
3915
3923
  }
3916
3924
  const decision = decisionsModule.loadDecisionById(hippoRoot, tenantId, id);
3917
3925
  if (!decision) {
3918
- console.error(`Decision ${id} not found.`);
3926
+ printError(`Decision ${id} not found.`);
3919
3927
  process.exit(1);
3920
3928
  }
3921
3929
  console.log(`Decision #${decision.id}`);
@@ -3937,12 +3945,12 @@ function cmdDecide(hippoRoot, args, flags) {
3937
3945
  if (subcommand === 'close') {
3938
3946
  const idRaw = args[1];
3939
3947
  if (!idRaw) {
3940
- console.error('Usage: hippo decide close <id>');
3948
+ printError('Usage: hippo decide close <id>');
3941
3949
  process.exit(1);
3942
3950
  }
3943
3951
  const id = parseInt(String(idRaw), 10);
3944
3952
  if (!Number.isFinite(id) || id <= 0) {
3945
- console.error(`Invalid decision id: "${idRaw}"`);
3953
+ printError(`Invalid decision id: "${idRaw}"`);
3946
3954
  process.exit(1);
3947
3955
  }
3948
3956
  const closed = decisionsModule.closeDecision(hippoRoot, tenantId, id);
@@ -3952,10 +3960,10 @@ function cmdDecide(hippoRoot, args, flags) {
3952
3960
  // Default subcommand: create. args[0] is the decision text.
3953
3961
  const decisionText = subcommand;
3954
3962
  if (!decisionText) {
3955
- console.error('Usage: hippo decide "<decision>" [--context "<why>"] [--supersedes <memory-id>]');
3956
- console.error(' hippo decide list [--status active|superseded|closed|all] [--limit N]');
3957
- console.error(' hippo decide get <id>');
3958
- console.error(' hippo decide close <id>');
3963
+ printError('Usage: hippo decide "<decision>" [--context "<why>"] [--supersedes <memory-id>]');
3964
+ printError(' hippo decide list [--status active|superseded|closed|all] [--limit N]');
3965
+ printError(' hippo decide get <id>');
3966
+ printError(' hippo decide close <id>');
3959
3967
  process.exit(1);
3960
3968
  }
3961
3969
  const contextRaw = flags['context'];
@@ -3964,7 +3972,7 @@ function cmdDecide(hippoRoot, args, flags) {
3964
3972
  // request: the user asked to supersede but gave no memory id. Reject it rather
3965
3973
  // than silently creating a non-superseding decision (codex review 2026-05-28).
3966
3974
  if (flags['supersedes'] === true) {
3967
- console.error('--supersedes requires a memory id, e.g. hippo decide "<text>" --supersedes mem_abc123.');
3975
+ printError('--supersedes requires a memory id, e.g. hippo decide "<text>" --supersedes mem_abc123.');
3968
3976
  process.exit(1);
3969
3977
  }
3970
3978
  const supersedesMemId = typeof flags['supersedes'] === 'string' ? flags['supersedes'] : null;
@@ -3978,7 +3986,7 @@ function cmdDecide(hippoRoot, args, flags) {
3978
3986
  if (supersedesMemId) {
3979
3987
  oldEntry = readEntry(hippoRoot, supersedesMemId, tenantId) ?? null;
3980
3988
  if (!oldEntry) {
3981
- console.error(`Memory ${supersedesMemId} not found.`);
3989
+ printError(`Memory ${supersedesMemId} not found.`);
3982
3990
  process.exit(1);
3983
3991
  }
3984
3992
  supersedesDecisionId =
@@ -4008,7 +4016,7 @@ function cmdDecide(hippoRoot, args, flags) {
4008
4016
  writeEntry(hippoRoot, oldEntry);
4009
4017
  }
4010
4018
  catch (e) {
4011
- console.error(` warning: decision recorded and superseded, but failed to weaken the prior memory ${supersedesMemId}: ${e.message}`);
4019
+ printError(` warning: decision recorded and superseded, but failed to weaken the prior memory ${supersedesMemId}: ${e.message}`);
4012
4020
  }
4013
4021
  }
4014
4022
  console.log(`Decision recorded: #${created.id}`);
@@ -4029,7 +4037,7 @@ function parsePositiveIncidentId(idRaw) {
4029
4037
  const s = String(idRaw ?? '').trim();
4030
4038
  const id = parseInt(s, 10);
4031
4039
  if (!/^\d+$/.test(s) || id <= 0) {
4032
- console.error(`Invalid incident id: "${idRaw}" (expected a positive integer).`);
4040
+ printError(`Invalid incident id: "${idRaw}" (expected a positive integer).`);
4033
4041
  process.exit(1);
4034
4042
  }
4035
4043
  return id;
@@ -4044,7 +4052,7 @@ function cmdIncident(hippoRoot, args, flags) {
4044
4052
  const limitRaw = flags['limit'];
4045
4053
  const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
4046
4054
  if (!Number.isFinite(limit) || limit <= 0) {
4047
- console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4055
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4048
4056
  process.exit(1);
4049
4057
  }
4050
4058
  let results;
@@ -4053,7 +4061,7 @@ function cmdIncident(hippoRoot, args, flags) {
4053
4061
  }
4054
4062
  else {
4055
4063
  if (!incidentsModule.VALID_INCIDENT_STATES.has(status)) {
4056
- console.error(`Invalid --status: "${status}". Must be one of: open | resolved | closed | all.`);
4064
+ printError(`Invalid --status: "${status}". Must be one of: open | resolved | closed | all.`);
4057
4065
  process.exit(1);
4058
4066
  }
4059
4067
  results = incidentsModule.loadIncidents(hippoRoot, tenantId, {
@@ -4078,13 +4086,13 @@ function cmdIncident(hippoRoot, args, flags) {
4078
4086
  if (subcommand === 'get') {
4079
4087
  const idRaw = args[1];
4080
4088
  if (!idRaw) {
4081
- console.error('Usage: hippo incident get <id>');
4089
+ printError('Usage: hippo incident get <id>');
4082
4090
  process.exit(1);
4083
4091
  }
4084
4092
  const id = parsePositiveIncidentId(idRaw);
4085
4093
  const incident = incidentsModule.loadIncidentById(hippoRoot, tenantId, id);
4086
4094
  if (!incident) {
4087
- console.error(`Incident ${id} not found.`);
4095
+ printError(`Incident ${id} not found.`);
4088
4096
  process.exit(1);
4089
4097
  }
4090
4098
  console.log(`Incident #${incident.id}`);
@@ -4109,13 +4117,13 @@ function cmdIncident(hippoRoot, args, flags) {
4109
4117
  if (subcommand === 'resolve') {
4110
4118
  const idRaw = args[1];
4111
4119
  if (!idRaw) {
4112
- console.error('Usage: hippo incident resolve <id> --resolution "<text>"');
4120
+ printError('Usage: hippo incident resolve <id> --resolution "<text>"');
4113
4121
  process.exit(1);
4114
4122
  }
4115
4123
  const id = parsePositiveIncidentId(idRaw);
4116
4124
  const resolutionRaw = flags['resolution'];
4117
4125
  if (typeof resolutionRaw !== 'string' || !resolutionRaw.trim()) {
4118
- console.error('--resolution requires a non-empty value, e.g. hippo incident resolve <id> --resolution "root cause fixed".');
4126
+ printError('--resolution requires a non-empty value, e.g. hippo incident resolve <id> --resolution "root cause fixed".');
4119
4127
  process.exit(1);
4120
4128
  }
4121
4129
  const resolved = incidentsModule.resolveIncident(hippoRoot, tenantId, id, resolutionRaw);
@@ -4125,7 +4133,7 @@ function cmdIncident(hippoRoot, args, flags) {
4125
4133
  if (subcommand === 'close') {
4126
4134
  const idRaw = args[1];
4127
4135
  if (!idRaw) {
4128
- console.error('Usage: hippo incident close <id>');
4136
+ printError('Usage: hippo incident close <id>');
4129
4137
  process.exit(1);
4130
4138
  }
4131
4139
  const id = parsePositiveIncidentId(idRaw);
@@ -4138,11 +4146,11 @@ function cmdIncident(hippoRoot, args, flags) {
4138
4146
  // `open` keyword the text is args[1], otherwise args[0] IS the text.
4139
4147
  const incidentText = subcommand === 'open' ? (args[1] ?? '') : subcommand;
4140
4148
  if (!incidentText) {
4141
- console.error('Usage: hippo incident "<incident>" [--context "<details>"] [--link <memory-id>]...');
4142
- console.error(' hippo incident list [--status open|resolved|closed|all] [--limit N]');
4143
- console.error(' hippo incident get <id>');
4144
- console.error(' hippo incident resolve <id> --resolution "<text>"');
4145
- console.error(' hippo incident close <id>');
4149
+ printError('Usage: hippo incident "<incident>" [--context "<details>"] [--link <memory-id>]...');
4150
+ printError(' hippo incident list [--status open|resolved|closed|all] [--limit N]');
4151
+ printError(' hippo incident get <id>');
4152
+ printError(' hippo incident resolve <id> --resolution "<text>"');
4153
+ printError(' hippo incident close <id>');
4146
4154
  process.exit(1);
4147
4155
  }
4148
4156
  const contextRaw = flags['context'];
@@ -4158,7 +4166,7 @@ function cmdIncident(hippoRoot, args, flags) {
4158
4166
  linkedMemoryIds = [linkRaw];
4159
4167
  }
4160
4168
  else if (linkRaw === true) {
4161
- console.error('--link requires a memory id, e.g. hippo incident "<text>" --link mem_abc123.');
4169
+ printError('--link requires a memory id, e.g. hippo incident "<text>" --link mem_abc123.');
4162
4170
  process.exit(1);
4163
4171
  }
4164
4172
  const incidentPathTags = extractPathTags(process.cwd());
@@ -4184,7 +4192,7 @@ function parsePositiveProcessId(idRaw) {
4184
4192
  const s = String(idRaw ?? '').trim();
4185
4193
  const id = parseInt(s, 10);
4186
4194
  if (!/^\d+$/.test(s) || id <= 0) {
4187
- console.error(`Invalid process id: "${idRaw}" (expected a positive integer).`);
4195
+ printError(`Invalid process id: "${idRaw}" (expected a positive integer).`);
4188
4196
  process.exit(1);
4189
4197
  }
4190
4198
  return id;
@@ -4197,7 +4205,7 @@ function collectProcessSteps(stepRaw) {
4197
4205
  if (typeof stepRaw === 'string')
4198
4206
  return [stepRaw];
4199
4207
  if (stepRaw === true) {
4200
- console.error('--step requires a value, e.g. hippo process new "<name>" --step "do X".');
4208
+ printError('--step requires a value, e.g. hippo process new "<name>" --step "do X".');
4201
4209
  process.exit(1);
4202
4210
  }
4203
4211
  return [];
@@ -4212,7 +4220,7 @@ function cmdProcess(hippoRoot, args, flags) {
4212
4220
  const limitRaw = flags['limit'];
4213
4221
  const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
4214
4222
  if (!Number.isFinite(limit) || limit <= 0) {
4215
- console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4223
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4216
4224
  process.exit(1);
4217
4225
  }
4218
4226
  let results;
@@ -4221,7 +4229,7 @@ function cmdProcess(hippoRoot, args, flags) {
4221
4229
  }
4222
4230
  else {
4223
4231
  if (!processesModule.VALID_PROCESS_STATES.has(status)) {
4224
- console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4232
+ printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4225
4233
  process.exit(1);
4226
4234
  }
4227
4235
  results = processesModule.loadProcesses(hippoRoot, tenantId, {
@@ -4245,13 +4253,13 @@ function cmdProcess(hippoRoot, args, flags) {
4245
4253
  if (subcommand === 'get') {
4246
4254
  const idRaw = args[1];
4247
4255
  if (!idRaw) {
4248
- console.error('Usage: hippo process get <id>');
4256
+ printError('Usage: hippo process get <id>');
4249
4257
  process.exit(1);
4250
4258
  }
4251
4259
  const id = parsePositiveProcessId(idRaw);
4252
4260
  const proc = processesModule.loadProcessById(hippoRoot, tenantId, id);
4253
4261
  if (!proc) {
4254
- console.error(`Process ${id} not found.`);
4262
+ printError(`Process ${id} not found.`);
4255
4263
  process.exit(1);
4256
4264
  }
4257
4265
  console.log(`Process #${proc.id}`);
@@ -4280,13 +4288,13 @@ function cmdProcess(hippoRoot, args, flags) {
4280
4288
  if (subcommand === 'supersede') {
4281
4289
  const idRaw = args[1];
4282
4290
  if (!idRaw) {
4283
- console.error('Usage: hippo process supersede <id> --step "<text>" [--step ...] [--change "<summary>"] [--description "<text>"]');
4291
+ printError('Usage: hippo process supersede <id> --step "<text>" [--step ...] [--change "<summary>"] [--description "<text>"]');
4284
4292
  process.exit(1);
4285
4293
  }
4286
4294
  const id = parsePositiveProcessId(idRaw);
4287
4295
  const steps = collectProcessSteps(flags['step']);
4288
4296
  if (steps.length === 0) {
4289
- console.error('hippo process supersede requires at least one --step "<text>" for the new version.');
4297
+ printError('hippo process supersede requires at least one --step "<text>" for the new version.');
4290
4298
  process.exit(1);
4291
4299
  }
4292
4300
  // A supersession is a new version of the SAME process, so the new row reuses
@@ -4295,7 +4303,7 @@ function cmdProcess(hippoRoot, args, flags) {
4295
4303
  // preflight is the authoritative active-state check.
4296
4304
  const existing = processesModule.loadProcessById(hippoRoot, tenantId, id);
4297
4305
  if (!existing) {
4298
- console.error(`Process ${id} not found.`);
4306
+ printError(`Process ${id} not found.`);
4299
4307
  process.exit(1);
4300
4308
  }
4301
4309
  const changeRaw = flags['change'];
@@ -4319,7 +4327,7 @@ function cmdProcess(hippoRoot, args, flags) {
4319
4327
  if (subcommand === 'close') {
4320
4328
  const idRaw = args[1];
4321
4329
  if (!idRaw) {
4322
- console.error('Usage: hippo process close <id>');
4330
+ printError('Usage: hippo process close <id>');
4323
4331
  process.exit(1);
4324
4332
  }
4325
4333
  const id = parsePositiveProcessId(idRaw);
@@ -4332,11 +4340,11 @@ function cmdProcess(hippoRoot, args, flags) {
4332
4340
  // `new` keyword the name is args[1], otherwise args[0] IS the name.
4333
4341
  const processName = subcommand === 'new' ? (args[1] ?? '') : subcommand;
4334
4342
  if (!processName) {
4335
- console.error('Usage: hippo process new "<name>" --step "<text>" [--step ...] [--description "<text>"]');
4336
- console.error(' hippo process list [--status active|superseded|closed|all] [--limit N]');
4337
- console.error(' hippo process get <id>');
4338
- console.error(' hippo process supersede <id> --step "<text>" [--change "<summary>"]');
4339
- console.error(' hippo process close <id>');
4343
+ printError('Usage: hippo process new "<name>" --step "<text>" [--step ...] [--description "<text>"]');
4344
+ printError(' hippo process list [--status active|superseded|closed|all] [--limit N]');
4345
+ printError(' hippo process get <id>');
4346
+ printError(' hippo process supersede <id> --step "<text>" [--change "<summary>"]');
4347
+ printError(' hippo process close <id>');
4340
4348
  process.exit(1);
4341
4349
  }
4342
4350
  const steps = collectProcessSteps(flags['step']);
@@ -4359,7 +4367,7 @@ function parsePositivePolicyId(idRaw) {
4359
4367
  const s = String(idRaw ?? '').trim();
4360
4368
  const id = parseInt(s, 10);
4361
4369
  if (!/^\d+$/.test(s) || id <= 0) {
4362
- console.error(`Invalid policy id: "${idRaw}" (expected a positive integer).`);
4370
+ printError(`Invalid policy id: "${idRaw}" (expected a positive integer).`);
4363
4371
  process.exit(1);
4364
4372
  }
4365
4373
  return id;
@@ -4381,7 +4389,7 @@ function cmdPolicy(hippoRoot, args, flags) {
4381
4389
  const limitRaw = flags['limit'];
4382
4390
  const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
4383
4391
  if (!Number.isFinite(limit) || limit <= 0) {
4384
- console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4392
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4385
4393
  process.exit(1);
4386
4394
  }
4387
4395
  let results;
@@ -4390,7 +4398,7 @@ function cmdPolicy(hippoRoot, args, flags) {
4390
4398
  }
4391
4399
  else {
4392
4400
  if (!policiesModule.VALID_POLICY_STATES.has(status)) {
4393
- console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4401
+ printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4394
4402
  process.exit(1);
4395
4403
  }
4396
4404
  results = policiesModule.loadPolicies(hippoRoot, tenantId, {
@@ -4410,7 +4418,7 @@ function cmdPolicy(hippoRoot, args, flags) {
4410
4418
  if (subcommand === 'asof') {
4411
4419
  const dateRaw = args[1];
4412
4420
  if (!dateRaw) {
4413
- console.error('Usage: hippo policy asof <iso-date> [--name "<policy>"]');
4421
+ printError('Usage: hippo policy asof <iso-date> [--name "<policy>"]');
4414
4422
  process.exit(1);
4415
4423
  }
4416
4424
  const nameRaw = flags['name'];
@@ -4420,7 +4428,7 @@ function cmdPolicy(hippoRoot, args, flags) {
4420
4428
  results = policiesModule.loadPoliciesAsOf(hippoRoot, tenantId, dateRaw, { name });
4421
4429
  }
4422
4430
  catch (e) {
4423
- console.error(e.message);
4431
+ printError(e.message);
4424
4432
  process.exit(1);
4425
4433
  }
4426
4434
  if (results.length === 0) {
@@ -4435,13 +4443,13 @@ function cmdPolicy(hippoRoot, args, flags) {
4435
4443
  if (subcommand === 'get') {
4436
4444
  const idRaw = args[1];
4437
4445
  if (!idRaw) {
4438
- console.error('Usage: hippo policy get <id>');
4446
+ printError('Usage: hippo policy get <id>');
4439
4447
  process.exit(1);
4440
4448
  }
4441
4449
  const id = parsePositivePolicyId(idRaw);
4442
4450
  const p = policiesModule.loadPolicyById(hippoRoot, tenantId, id);
4443
4451
  if (!p) {
4444
- console.error(`Policy ${id} not found.`);
4452
+ printError(`Policy ${id} not found.`);
4445
4453
  process.exit(1);
4446
4454
  }
4447
4455
  console.log(`Policy #${p.id}`);
@@ -4467,18 +4475,18 @@ function cmdPolicy(hippoRoot, args, flags) {
4467
4475
  if (subcommand === 'supersede') {
4468
4476
  const idRaw = args[1];
4469
4477
  if (!idRaw) {
4470
- console.error('Usage: hippo policy supersede <id> --text "<rule>" [--from <iso>] [--to <iso>] [--change "<summary>"]');
4478
+ printError('Usage: hippo policy supersede <id> --text "<rule>" [--from <iso>] [--to <iso>] [--change "<summary>"]');
4471
4479
  process.exit(1);
4472
4480
  }
4473
4481
  const id = parsePositivePolicyId(idRaw);
4474
4482
  const textRaw = flags['text'];
4475
4483
  if (typeof textRaw !== 'string' || !textRaw.trim()) {
4476
- console.error('hippo policy supersede requires --text "<rule>" for the new version.');
4484
+ printError('hippo policy supersede requires --text "<rule>" for the new version.');
4477
4485
  process.exit(1);
4478
4486
  }
4479
4487
  const existing = policiesModule.loadPolicyById(hippoRoot, tenantId, id);
4480
4488
  if (!existing) {
4481
- console.error(`Policy ${id} not found.`);
4489
+ printError(`Policy ${id} not found.`);
4482
4490
  process.exit(1);
4483
4491
  }
4484
4492
  const fromRaw = flags['from'];
@@ -4499,7 +4507,7 @@ function cmdPolicy(hippoRoot, args, flags) {
4499
4507
  console.log(` memory: ${created.memoryId}`);
4500
4508
  }
4501
4509
  catch (e) {
4502
- console.error(e.message);
4510
+ printError(e.message);
4503
4511
  process.exit(1);
4504
4512
  }
4505
4513
  return;
@@ -4507,7 +4515,7 @@ function cmdPolicy(hippoRoot, args, flags) {
4507
4515
  if (subcommand === 'close') {
4508
4516
  const idRaw = args[1];
4509
4517
  if (!idRaw) {
4510
- console.error('Usage: hippo policy close <id>');
4518
+ printError('Usage: hippo policy close <id>');
4511
4519
  process.exit(1);
4512
4520
  }
4513
4521
  const id = parsePositivePolicyId(idRaw);
@@ -4520,12 +4528,12 @@ function cmdPolicy(hippoRoot, args, flags) {
4520
4528
  const policyName = subcommand === 'new' ? (args[1] ?? '') : subcommand;
4521
4529
  const textRaw = flags['text'];
4522
4530
  if (!policyName || typeof textRaw !== 'string' || !textRaw.trim()) {
4523
- console.error('Usage: hippo policy new "<name>" --text "<rule>" [--from <iso>] [--to <iso>]');
4524
- console.error(' hippo policy list [--status active|superseded|closed|all] [--limit N]');
4525
- console.error(' hippo policy get <id>');
4526
- console.error(' hippo policy asof <iso-date> [--name "<policy>"]');
4527
- console.error(' hippo policy supersede <id> --text "<rule>" [--from] [--to] [--change "<summary>"]');
4528
- console.error(' hippo policy close <id>');
4531
+ printError('Usage: hippo policy new "<name>" --text "<rule>" [--from <iso>] [--to <iso>]');
4532
+ printError(' hippo policy list [--status active|superseded|closed|all] [--limit N]');
4533
+ printError(' hippo policy get <id>');
4534
+ printError(' hippo policy asof <iso-date> [--name "<policy>"]');
4535
+ printError(' hippo policy supersede <id> --text "<rule>" [--from] [--to] [--change "<summary>"]');
4536
+ printError(' hippo policy close <id>');
4529
4537
  process.exit(1);
4530
4538
  }
4531
4539
  const fromRaw = flags['from'];
@@ -4544,7 +4552,7 @@ function cmdPolicy(hippoRoot, args, flags) {
4544
4552
  console.log(` memory: ${created.memoryId}`);
4545
4553
  }
4546
4554
  catch (e) {
4547
- console.error(e.message);
4555
+ printError(e.message);
4548
4556
  process.exit(1);
4549
4557
  }
4550
4558
  }
@@ -4554,7 +4562,7 @@ function parsePositiveSkillId(idRaw) {
4554
4562
  const s = String(idRaw ?? '').trim();
4555
4563
  const id = parseInt(s, 10);
4556
4564
  if (!/^\d+$/.test(s) || id <= 0) {
4557
- console.error(`Invalid skill id: "${idRaw}" (expected a positive integer).`);
4565
+ printError(`Invalid skill id: "${idRaw}" (expected a positive integer).`);
4558
4566
  process.exit(1);
4559
4567
  }
4560
4568
  return id;
@@ -4576,7 +4584,7 @@ function cmdSkill(hippoRoot, args, flags) {
4576
4584
  const limitRaw = flags['limit'];
4577
4585
  const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
4578
4586
  if (!Number.isFinite(limit) || limit <= 0) {
4579
- console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4587
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4580
4588
  process.exit(1);
4581
4589
  }
4582
4590
  let results;
@@ -4585,7 +4593,7 @@ function cmdSkill(hippoRoot, args, flags) {
4585
4593
  }
4586
4594
  else {
4587
4595
  if (!skillsModule.VALID_SKILL_STATES.has(status)) {
4588
- console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4596
+ printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4589
4597
  process.exit(1);
4590
4598
  }
4591
4599
  results = skillsModule.loadSkills(hippoRoot, tenantId, {
@@ -4614,13 +4622,13 @@ function cmdSkill(hippoRoot, args, flags) {
4614
4622
  if (subcommand === 'get') {
4615
4623
  const idRaw = args[1];
4616
4624
  if (!idRaw) {
4617
- console.error('Usage: hippo skill get <id>');
4625
+ printError('Usage: hippo skill get <id>');
4618
4626
  process.exit(1);
4619
4627
  }
4620
4628
  const id = parsePositiveSkillId(idRaw);
4621
4629
  const s = skillsModule.loadSkillById(hippoRoot, tenantId, id);
4622
4630
  if (!s) {
4623
- console.error(`Skill ${id} not found.`);
4631
+ printError(`Skill ${id} not found.`);
4624
4632
  process.exit(1);
4625
4633
  }
4626
4634
  console.log(`Skill #${s.id}`);
@@ -4646,18 +4654,18 @@ function cmdSkill(hippoRoot, args, flags) {
4646
4654
  if (subcommand === 'supersede') {
4647
4655
  const idRaw = args[1];
4648
4656
  if (!idRaw) {
4649
- console.error('Usage: hippo skill supersede <id> --instructions "<text>" [--trigger "<when>"] [--change "<summary>"]');
4657
+ printError('Usage: hippo skill supersede <id> --instructions "<text>" [--trigger "<when>"] [--change "<summary>"]');
4650
4658
  process.exit(1);
4651
4659
  }
4652
4660
  const id = parsePositiveSkillId(idRaw);
4653
4661
  const instrRaw = flags['instructions'];
4654
4662
  if (typeof instrRaw !== 'string' || !instrRaw.trim()) {
4655
- console.error('hippo skill supersede requires --instructions "<text>" for the new version.');
4663
+ printError('hippo skill supersede requires --instructions "<text>" for the new version.');
4656
4664
  process.exit(1);
4657
4665
  }
4658
4666
  const existing = skillsModule.loadSkillById(hippoRoot, tenantId, id);
4659
4667
  if (!existing) {
4660
- console.error(`Skill ${id} not found.`);
4668
+ printError(`Skill ${id} not found.`);
4661
4669
  process.exit(1);
4662
4670
  }
4663
4671
  const trigRaw = flags['trigger'];
@@ -4676,7 +4684,7 @@ function cmdSkill(hippoRoot, args, flags) {
4676
4684
  console.log(` memory: ${created.memoryId}`);
4677
4685
  }
4678
4686
  catch (e) {
4679
- console.error(e.message);
4687
+ printError(e.message);
4680
4688
  process.exit(1);
4681
4689
  }
4682
4690
  return;
@@ -4684,7 +4692,7 @@ function cmdSkill(hippoRoot, args, flags) {
4684
4692
  if (subcommand === 'close') {
4685
4693
  const idRaw = args[1];
4686
4694
  if (!idRaw) {
4687
- console.error('Usage: hippo skill close <id>');
4695
+ printError('Usage: hippo skill close <id>');
4688
4696
  process.exit(1);
4689
4697
  }
4690
4698
  const id = parsePositiveSkillId(idRaw);
@@ -4697,12 +4705,12 @@ function cmdSkill(hippoRoot, args, flags) {
4697
4705
  const skillName = subcommand === 'new' ? (args[1] ?? '') : subcommand;
4698
4706
  const instrRaw = flags['instructions'];
4699
4707
  if (!skillName || typeof instrRaw !== 'string' || !instrRaw.trim()) {
4700
- console.error('Usage: hippo skill new "<name>" --instructions "<text>" [--trigger "<when>"]');
4701
- console.error(' hippo skill list [--status active|superseded|closed|all] [--limit N]');
4702
- console.error(' hippo skill get <id>');
4703
- console.error(' hippo skill export (render active skills as an AGENTS.md/CLAUDE.md block)');
4704
- console.error(' hippo skill supersede <id> --instructions "<text>" [--trigger] [--change "<summary>"]');
4705
- console.error(' hippo skill close <id>');
4708
+ printError('Usage: hippo skill new "<name>" --instructions "<text>" [--trigger "<when>"]');
4709
+ printError(' hippo skill list [--status active|superseded|closed|all] [--limit N]');
4710
+ printError(' hippo skill get <id>');
4711
+ printError(' hippo skill export (render active skills as an AGENTS.md/CLAUDE.md block)');
4712
+ printError(' hippo skill supersede <id> --instructions "<text>" [--trigger] [--change "<summary>"]');
4713
+ printError(' hippo skill close <id>');
4706
4714
  process.exit(1);
4707
4715
  }
4708
4716
  const trigRaw = flags['trigger'];
@@ -4718,7 +4726,7 @@ function cmdSkill(hippoRoot, args, flags) {
4718
4726
  console.log(` memory: ${created.memoryId}`);
4719
4727
  }
4720
4728
  catch (e) {
4721
- console.error(e.message);
4729
+ printError(e.message);
4722
4730
  process.exit(1);
4723
4731
  }
4724
4732
  }
@@ -4726,7 +4734,7 @@ function parsePositiveBriefId(idRaw) {
4726
4734
  const s = String(idRaw ?? '').trim();
4727
4735
  const id = parseInt(s, 10);
4728
4736
  if (!/^\d+$/.test(s) || id <= 0) {
4729
- console.error(`Invalid brief id: "${idRaw}" (expected a positive integer).`);
4737
+ printError(`Invalid brief id: "${idRaw}" (expected a positive integer).`);
4730
4738
  process.exit(1);
4731
4739
  }
4732
4740
  return id;
@@ -4737,12 +4745,12 @@ function printBriefRow(b) {
4737
4745
  console.log(` change: ${b.changeSummary}`);
4738
4746
  }
4739
4747
  function briefUsage() {
4740
- console.error('Usage: hippo brief new "<repo>" --summary "<text>"');
4741
- console.error(' hippo brief list [--status active|superseded|closed|all] [--repo "<repo>"] [--limit N]');
4742
- console.error(' hippo brief get <id>');
4743
- console.error(' hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
4744
- console.error(' hippo brief close <id>');
4745
- console.error(' hippo brief refresh "<repo>" [--dry-run] (auto-assemble the brief from the repo\'s receipts)');
4748
+ printError('Usage: hippo brief new "<repo>" --summary "<text>"');
4749
+ printError(' hippo brief list [--status active|superseded|closed|all] [--repo "<repo>"] [--limit N]');
4750
+ printError(' hippo brief get <id>');
4751
+ printError(' hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
4752
+ printError(' hippo brief close <id>');
4753
+ printError(' hippo brief refresh "<repo>" [--dry-run] (auto-assemble the brief from the repo\'s receipts)');
4746
4754
  }
4747
4755
  function cmdProjectBrief(hippoRoot, args, flags) {
4748
4756
  requireInit(hippoRoot);
@@ -4756,13 +4764,13 @@ function cmdProjectBrief(hippoRoot, args, flags) {
4756
4764
  const limitRaw = flags['limit'];
4757
4765
  const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
4758
4766
  if (!Number.isFinite(limit) || limit <= 0) {
4759
- console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4767
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4760
4768
  process.exit(1);
4761
4769
  }
4762
4770
  const opts = { limit, repo };
4763
4771
  if (status !== 'all') {
4764
4772
  if (!briefsModule.VALID_BRIEF_STATES.has(status)) {
4765
- console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4773
+ printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4766
4774
  process.exit(1);
4767
4775
  }
4768
4776
  opts.status = status;
@@ -4780,14 +4788,14 @@ function cmdProjectBrief(hippoRoot, args, flags) {
4780
4788
  if (subcommand === 'refresh') {
4781
4789
  const repoRaw = args[1];
4782
4790
  if (!repoRaw) {
4783
- console.error('Usage: hippo brief refresh "<repo>" [--dry-run]');
4791
+ printError('Usage: hippo brief refresh "<repo>" [--dry-run]');
4784
4792
  process.exit(1);
4785
4793
  }
4786
4794
  const dryRun = Boolean(flags['dry-run']);
4787
4795
  try {
4788
4796
  if (dryRun) {
4789
4797
  const { markdown, receiptCount } = briefsModule.assembleBriefFromReceipts(hippoRoot, tenantId, repoRaw);
4790
- console.error(`(dry-run: assembled from ${receiptCount} receipt(s); brief NOT written)`);
4798
+ printError(`(dry-run: assembled from ${receiptCount} receipt(s); brief NOT written)`);
4791
4799
  console.log(markdown);
4792
4800
  return;
4793
4801
  }
@@ -4799,7 +4807,7 @@ function cmdProjectBrief(hippoRoot, args, flags) {
4799
4807
  console.log(` memory: ${created.memoryId}`);
4800
4808
  }
4801
4809
  catch (e) {
4802
- console.error(e.message);
4810
+ printError(e.message);
4803
4811
  process.exit(1);
4804
4812
  }
4805
4813
  return;
@@ -4807,13 +4815,13 @@ function cmdProjectBrief(hippoRoot, args, flags) {
4807
4815
  if (subcommand === 'get') {
4808
4816
  const idRaw = args[1];
4809
4817
  if (!idRaw) {
4810
- console.error('Usage: hippo brief get <id>');
4818
+ printError('Usage: hippo brief get <id>');
4811
4819
  process.exit(1);
4812
4820
  }
4813
4821
  const id = parsePositiveBriefId(idRaw);
4814
4822
  const b = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
4815
4823
  if (!b) {
4816
- console.error(`Project brief ${id} not found.`);
4824
+ printError(`Project brief ${id} not found.`);
4817
4825
  process.exit(1);
4818
4826
  }
4819
4827
  console.log(`Project brief #${b.id}`);
@@ -4837,18 +4845,18 @@ function cmdProjectBrief(hippoRoot, args, flags) {
4837
4845
  if (subcommand === 'supersede') {
4838
4846
  const idRaw = args[1];
4839
4847
  if (!idRaw) {
4840
- console.error('Usage: hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
4848
+ printError('Usage: hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
4841
4849
  process.exit(1);
4842
4850
  }
4843
4851
  const id = parsePositiveBriefId(idRaw);
4844
4852
  const summaryRaw = flags['summary'];
4845
4853
  if (typeof summaryRaw !== 'string' || !summaryRaw.trim()) {
4846
- console.error('hippo brief supersede requires --summary "<text>" for the new version.');
4854
+ printError('hippo brief supersede requires --summary "<text>" for the new version.');
4847
4855
  process.exit(1);
4848
4856
  }
4849
4857
  const existing = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
4850
4858
  if (!existing) {
4851
- console.error(`Project brief ${id} not found.`);
4859
+ printError(`Project brief ${id} not found.`);
4852
4860
  process.exit(1);
4853
4861
  }
4854
4862
  const changeRaw = flags['change'];
@@ -4865,7 +4873,7 @@ function cmdProjectBrief(hippoRoot, args, flags) {
4865
4873
  console.log(` memory: ${created.memoryId}`);
4866
4874
  }
4867
4875
  catch (e) {
4868
- console.error(e.message);
4876
+ printError(e.message);
4869
4877
  process.exit(1);
4870
4878
  }
4871
4879
  return;
@@ -4873,7 +4881,7 @@ function cmdProjectBrief(hippoRoot, args, flags) {
4873
4881
  if (subcommand === 'close') {
4874
4882
  const idRaw = args[1];
4875
4883
  if (!idRaw) {
4876
- console.error('Usage: hippo brief close <id>');
4884
+ printError('Usage: hippo brief close <id>');
4877
4885
  process.exit(1);
4878
4886
  }
4879
4887
  const id = parsePositiveBriefId(idRaw);
@@ -4900,7 +4908,7 @@ function cmdProjectBrief(hippoRoot, args, flags) {
4900
4908
  console.log(` memory: ${created.memoryId}`);
4901
4909
  }
4902
4910
  catch (e) {
4903
- console.error(e.message);
4911
+ printError(e.message);
4904
4912
  process.exit(1);
4905
4913
  }
4906
4914
  }
@@ -4908,7 +4916,7 @@ function parsePositiveNoteId(idRaw) {
4908
4916
  const s = String(idRaw ?? '').trim();
4909
4917
  const id = parseInt(s, 10);
4910
4918
  if (!/^\d+$/.test(s) || id <= 0) {
4911
- console.error(`Invalid note id: "${idRaw}" (expected a positive integer).`);
4919
+ printError(`Invalid note id: "${idRaw}" (expected a positive integer).`);
4912
4920
  process.exit(1);
4913
4921
  }
4914
4922
  return id;
@@ -4919,11 +4927,11 @@ function printNoteRow(n) {
4919
4927
  console.log(` change: ${n.changeSummary}`);
4920
4928
  }
4921
4929
  function noteUsage() {
4922
- console.error('Usage: hippo note new "<customer>" --text "<note>"');
4923
- console.error(' hippo note list [--status active|superseded|closed|all] [--customer "<id>"] [--limit N]');
4924
- console.error(' hippo note get <id>');
4925
- console.error(' hippo note supersede <id> --text "<note>" [--change "<summary>"]');
4926
- console.error(' hippo note close <id>');
4930
+ printError('Usage: hippo note new "<customer>" --text "<note>"');
4931
+ printError(' hippo note list [--status active|superseded|closed|all] [--customer "<id>"] [--limit N]');
4932
+ printError(' hippo note get <id>');
4933
+ printError(' hippo note supersede <id> --text "<note>" [--change "<summary>"]');
4934
+ printError(' hippo note close <id>');
4927
4935
  }
4928
4936
  function cmdGraph(hippoRoot, args, flags) {
4929
4937
  requireInit(hippoRoot);
@@ -4937,7 +4945,7 @@ function cmdGraph(hippoRoot, args, flags) {
4937
4945
  const supersedes = result.relations - result.references;
4938
4946
  console.log(`Graph extracted: ${result.entities} entities (${byType}) + ${result.relations} relations (${supersedes} supersedes, ${result.references} references).`);
4939
4947
  if (result.truncated.length > 0) {
4940
- console.error(`WARNING: under-extracted (hit the per-type cap): ${result.truncated.join(', ')}. The graph is incomplete for those types.`);
4948
+ printError(`WARNING: under-extracted (hit the per-type cap): ${result.truncated.join(', ')}. The graph is incomplete for those types.`);
4941
4949
  }
4942
4950
  return;
4943
4951
  }
@@ -4976,7 +4984,7 @@ function cmdGraph(hippoRoot, args, flags) {
4976
4984
  if (subcommand === 'view') {
4977
4985
  const format = typeof flags['format'] === 'string' ? flags['format'] : 'html';
4978
4986
  if (format !== 'html' && format !== 'canvas') {
4979
- console.error("graph view: --format must be 'html' or 'canvas'");
4987
+ printError("graph view: --format must be 'html' or 'canvas'");
4980
4988
  process.exit(1);
4981
4989
  }
4982
4990
  const model = buildGraphModel(hippoRoot, tenantId, { entity, limit: DEFAULT_VIEW_LIMIT });
@@ -5006,7 +5014,7 @@ function cmdGraph(hippoRoot, args, flags) {
5006
5014
  }
5007
5015
  return;
5008
5016
  }
5009
- console.error('Usage:\n' +
5017
+ printError('Usage:\n' +
5010
5018
  ' hippo graph extract Rebuild the entity/relation graph from consolidated objects\n' +
5011
5019
  ' hippo graph show [--entity NAME] [--json] Inspect entities + their edges (text or JSON)\n' +
5012
5020
  ' hippo graph view [--out FILE] [--open] [--format html|canvas] [--entity NAME] Generate an interactive node-link diagram');
@@ -5024,13 +5032,13 @@ function cmdCustomerNote(hippoRoot, args, flags) {
5024
5032
  const limitRaw = flags['limit'];
5025
5033
  const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
5026
5034
  if (!Number.isFinite(limit) || limit <= 0) {
5027
- console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
5035
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
5028
5036
  process.exit(1);
5029
5037
  }
5030
5038
  const opts = { limit, customer };
5031
5039
  if (status !== 'all') {
5032
5040
  if (!customerNotesModule.VALID_NOTE_STATES.has(status)) {
5033
- console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
5041
+ printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
5034
5042
  process.exit(1);
5035
5043
  }
5036
5044
  opts.status = status;
@@ -5048,13 +5056,13 @@ function cmdCustomerNote(hippoRoot, args, flags) {
5048
5056
  if (subcommand === 'get') {
5049
5057
  const idRaw = args[1];
5050
5058
  if (!idRaw) {
5051
- console.error('Usage: hippo note get <id>');
5059
+ printError('Usage: hippo note get <id>');
5052
5060
  process.exit(1);
5053
5061
  }
5054
5062
  const id = parsePositiveNoteId(idRaw);
5055
5063
  const n = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
5056
5064
  if (!n) {
5057
- console.error(`Customer note ${id} not found.`);
5065
+ printError(`Customer note ${id} not found.`);
5058
5066
  process.exit(1);
5059
5067
  }
5060
5068
  console.log(`Customer note #${n.id}`);
@@ -5078,18 +5086,18 @@ function cmdCustomerNote(hippoRoot, args, flags) {
5078
5086
  if (subcommand === 'supersede') {
5079
5087
  const idRaw = args[1];
5080
5088
  if (!idRaw) {
5081
- console.error('Usage: hippo note supersede <id> --text "<note>" [--change "<summary>"]');
5089
+ printError('Usage: hippo note supersede <id> --text "<note>" [--change "<summary>"]');
5082
5090
  process.exit(1);
5083
5091
  }
5084
5092
  const id = parsePositiveNoteId(idRaw);
5085
5093
  const textRaw = flags['text'];
5086
5094
  if (typeof textRaw !== 'string' || !textRaw.trim()) {
5087
- console.error('hippo note supersede requires --text "<note>" for the new version.');
5095
+ printError('hippo note supersede requires --text "<note>" for the new version.');
5088
5096
  process.exit(1);
5089
5097
  }
5090
5098
  const existing = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
5091
5099
  if (!existing) {
5092
- console.error(`Customer note ${id} not found.`);
5100
+ printError(`Customer note ${id} not found.`);
5093
5101
  process.exit(1);
5094
5102
  }
5095
5103
  const changeRaw = flags['change'];
@@ -5106,7 +5114,7 @@ function cmdCustomerNote(hippoRoot, args, flags) {
5106
5114
  console.log(` memory: ${created.memoryId}`);
5107
5115
  }
5108
5116
  catch (e) {
5109
- console.error(e.message);
5117
+ printError(e.message);
5110
5118
  process.exit(1);
5111
5119
  }
5112
5120
  return;
@@ -5114,7 +5122,7 @@ function cmdCustomerNote(hippoRoot, args, flags) {
5114
5122
  if (subcommand === 'close') {
5115
5123
  const idRaw = args[1];
5116
5124
  if (!idRaw) {
5117
- console.error('Usage: hippo note close <id>');
5125
+ printError('Usage: hippo note close <id>');
5118
5126
  process.exit(1);
5119
5127
  }
5120
5128
  const id = parsePositiveNoteId(idRaw);
@@ -5141,7 +5149,7 @@ function cmdCustomerNote(hippoRoot, args, flags) {
5141
5149
  console.log(` memory: ${created.memoryId}`);
5142
5150
  }
5143
5151
  catch (e) {
5144
- console.error(e.message);
5152
+ printError(e.message);
5145
5153
  process.exit(1);
5146
5154
  }
5147
5155
  }
@@ -5196,7 +5204,7 @@ function cmdCurrent(hippoRoot, args, flags) {
5196
5204
  }
5197
5205
  return;
5198
5206
  }
5199
- console.error('Usage: hippo current <show>');
5207
+ printError('Usage: hippo current <show>');
5200
5208
  process.exit(1);
5201
5209
  }
5202
5210
  /** Hook commands share one handle per store and wait at most HOOK_DB_WAIT_MS for a lock; a store still busy after that skips the hook's work with one warning, exit 0. */
@@ -5236,7 +5244,8 @@ function startDeliveryRecorder(hippoRoot, flags, stdinText) {
5236
5244
  });
5237
5245
  }
5238
5246
  catch (error) {
5239
- console.error(`[hippo] delivery ledger skipped: ${error instanceof Error ? error.message : String(error)}`);
5247
+ // The hook's one-line stderr contract pins this exact text, so it bypasses the leveled logger.
5248
+ printError(`[hippo] delivery ledger skipped:${error instanceof Error ? error.message : String(error)}`);
5240
5249
  return null;
5241
5250
  }
5242
5251
  }
@@ -5248,7 +5257,8 @@ function flushDeliveryRecorder(rec, db) {
5248
5257
  rec.flush((input) => (db ? writeDeliveryEventOnHandle(db, input) : writeDeliveryEventAtRoot(rec.root, input)));
5249
5258
  }
5250
5259
  catch (error) {
5251
- console.error(`[hippo] delivery ledger write failed: ${error instanceof Error ? error.message : String(error)}`);
5260
+ // Pinned stderr text, as in the recorder build above.
5261
+ printError(`[hippo] delivery ledger write failed:${error instanceof Error ? error.message : String(error)}`);
5252
5262
  }
5253
5263
  }
5254
5264
  /**
@@ -5489,6 +5499,7 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
5489
5499
  tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface, event: 'inject',
5490
5500
  items: staticItems.length, tokens: estimateTokens(finalStatic), hash: blockHash(finalStatic),
5491
5501
  });
5502
+ // Best-effort row: only a busy store is actionable, and a ledger failure must not break the hook.
5492
5503
  }
5493
5504
  catch (error) {
5494
5505
  if (isSqliteBusy(error))
@@ -5501,6 +5512,7 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
5501
5512
  tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: 'hook_recall', event: 'inject',
5502
5513
  items: recallItems.length, tokens: estimateTokens(recallBlock), hash: blockHash(recallBlock),
5503
5514
  });
5515
+ // Same best-effort rule as the inject row above.
5504
5516
  }
5505
5517
  catch (error) {
5506
5518
  if (isSqliteBusy(error))
@@ -5608,7 +5620,7 @@ async function cmdEmbed(hippoRoot, flags) {
5608
5620
  return resolveEmbeddingProvider(root);
5609
5621
  }
5610
5622
  catch (err) {
5611
- console.error(err instanceof Error ? err.message : String(err));
5623
+ printError(err instanceof Error ? err.message : String(err));
5612
5624
  return null;
5613
5625
  }
5614
5626
  })();
@@ -5626,8 +5638,8 @@ async function cmdEmbed(hippoRoot, flags) {
5626
5638
  console.log(' npm install @huggingface/transformers');
5627
5639
  }
5628
5640
  else {
5629
- console.error(`Embedding provider '${embedProvider.kind}' is configured but ${embedProvider.keyEnv} is not set.`);
5630
- console.error(`Export ${embedProvider.keyEnv}, or set config.embeddings.provider back to 'local'.`);
5641
+ printError(`Embedding provider '${embedProvider.kind}' is configured but ${embedProvider.keyEnv} is not set.`);
5642
+ printError(`Export ${embedProvider.keyEnv}, or set config.embeddings.provider back to 'local'.`);
5631
5643
  process.exitCode = 1;
5632
5644
  }
5633
5645
  return;
@@ -5638,9 +5650,9 @@ async function cmdEmbed(hippoRoot, flags) {
5638
5650
  count = await embedAll(root, resolveEmbeddingModel(root));
5639
5651
  }
5640
5652
  catch (err) {
5641
- console.error(`Embedding failed: ${err instanceof Error ? err.message : String(err)}`);
5653
+ printError(`Embedding failed: ${err instanceof Error ? err.message : String(err)}`);
5642
5654
  const partial = loadEmbeddingIndex(root);
5643
- console.error(`Partial progress saved: ${Object.keys(partial).length} embeddings on disk. Re-run \`hippo embed\` to resume.`);
5655
+ printError(`Partial progress saved: ${Object.keys(partial).length} embeddings on disk. Re-run \`hippo embed\` to resume.`);
5644
5656
  process.exitCode = 1;
5645
5657
  return;
5646
5658
  }
@@ -5653,7 +5665,7 @@ async function cmdEmbed(hippoRoot, flags) {
5653
5665
  // ---------------------------------------------------------------------------
5654
5666
  async function cmdWatch(command, hippoRoot) {
5655
5667
  if (!command) {
5656
- console.error('Usage: hippo watch "<command>"');
5668
+ printError('Usage: hippo watch "<command>"');
5657
5669
  process.exit(1);
5658
5670
  }
5659
5671
  const { exitCode, stderr } = await runWatched(command);
@@ -5663,7 +5675,7 @@ async function cmdWatch(command, hippoRoot) {
5663
5675
  }
5664
5676
  // Only create memory if hippo is initialized
5665
5677
  if (!isInitialized(hippoRoot)) {
5666
- console.error('Command failed but .hippo not initialized. Run `hippo init` to enable auto-learn.');
5678
+ printError('Command failed but .hippo not initialized. Run `hippo init` to enable auto-learn.');
5667
5679
  process.exit(exitCode);
5668
5680
  }
5669
5681
  const entry = captureError(exitCode, stderr, command, resolveTenantId({}));
@@ -5682,11 +5694,11 @@ async function cmdWatch(command, hippoRoot) {
5682
5694
  updateStats(hippoRoot, { remembered: 1 });
5683
5695
  void embedMemory(hippoRoot, entry);
5684
5696
  const preview = stderr.trim().slice(0, 80);
5685
- console.error(`\nHippo learned from failure: "${preview}"`);
5697
+ printError(`\nHippo learned from failure: "${preview}"`);
5686
5698
  }
5687
5699
  catch (err) {
5688
5700
  if (err instanceof RejectedValueError) {
5689
- console.error(`\nHippo: this failure matches a rejected value (${err.reason ?? 'no reason given'}); not stored.`);
5701
+ printError(`\nHippo: this failure matches a rejected value (${err.reason ?? 'no reason given'}); not stored.`);
5690
5702
  }
5691
5703
  else {
5692
5704
  throw err;
@@ -5700,7 +5712,7 @@ async function cmdWatch(command, hippoRoot) {
5700
5712
  function cmdLearn(hippoRoot, flags) {
5701
5713
  requireInit(hippoRoot);
5702
5714
  if (!flags['git']) {
5703
- console.error('Usage: hippo learn --git [--days <n>] [--repos <paths>]');
5715
+ printError('Usage: hippo learn --git [--days <n>] [--repos <paths>]');
5704
5716
  process.exit(1);
5705
5717
  }
5706
5718
  const days = parseInt(String(flags['days'] ?? '7'), 10);
@@ -5728,7 +5740,7 @@ function cmdLearn(hippoRoot, flags) {
5728
5740
  // ---------------------------------------------------------------------------
5729
5741
  function warnRedacted(count) {
5730
5742
  if (count)
5731
- console.error(`Warning: secret-shaped text was redacted from ${count} imported ${count === 1 ? 'entry' : 'entries'} before storing`);
5743
+ printError(`Warning: secret-shaped text was redacted from ${count} imported ${count === 1 ? 'entry' : 'entries'} before storing`);
5732
5744
  }
5733
5745
  function cmdImport(hippoRoot, args, flags) {
5734
5746
  const useGlobal = Boolean(flags['global']);
@@ -5748,7 +5760,7 @@ function cmdImport(hippoRoot, args, flags) {
5748
5760
  for (const line of detailLines(report, dryRun))
5749
5761
  console.log(line);
5750
5762
  for (const warning of report.warnings)
5751
- console.error(`hippo: agent memories: ${warning}`);
5763
+ printError(`hippo: agent memories: ${warning}`);
5752
5764
  return;
5753
5765
  }
5754
5766
  if (useGlobal) {
@@ -5772,11 +5784,11 @@ function cmdImport(hippoRoot, args, flags) {
5772
5784
  if (flags['vault']) {
5773
5785
  const folderPath = String(flags['vault']);
5774
5786
  if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
5775
- console.error(`Vault folder not found (or not a directory): ${folderPath}`);
5787
+ printError(`Vault folder not found (or not a directory): ${folderPath}`);
5776
5788
  process.exit(1);
5777
5789
  }
5778
5790
  if (useGlobal) {
5779
- console.error('hippo import --vault does not support --global (raw rows are tenant-local).');
5791
+ printError('hippo import --vault does not support --global (raw rows are tenant-local).');
5780
5792
  process.exit(1);
5781
5793
  }
5782
5794
  if (typeof flags['name'] !== 'string' || !flags['name'].trim()) {
@@ -5785,14 +5797,14 @@ function cmdImport(hippoRoot, args, flags) {
5785
5797
  // clobber each other (codex R10 P2). A valueless `--name` parses as boolean
5786
5798
  // true, and String(true) === "true" would silently import under vault:true:*
5787
5799
  // - reject a non-string so it fails fast instead (codex R11 P2).
5788
- console.error('hippo import --vault requires --name <vault> (a non-empty identity key for source-deletion sync).');
5800
+ printError('hippo import --vault requires --name <vault> (a non-empty identity key for source-deletion sync).');
5789
5801
  process.exit(1);
5790
5802
  }
5791
5803
  if (flags['scope'] !== undefined && (typeof flags['scope'] !== 'string' || !flags['scope'].trim())) {
5792
5804
  // Same valueless-flag trap: a bare `--scope` must not become scope "true".
5793
5805
  // Example uses the source-prefixed private form, since a bare `private` scope
5794
5806
  // is NOT treated as private by recall and importVault rejects it (R13 P2).
5795
- console.error('hippo import --vault: --scope requires a value (e.g. --scope vault:private:notes).');
5807
+ printError('hippo import --vault: --scope requires a value (e.g. --scope vault:private:notes).');
5796
5808
  process.exit(1);
5797
5809
  }
5798
5810
  const tenantId = resolveTenantId({});
@@ -5857,11 +5869,11 @@ function cmdImport(hippoRoot, args, flags) {
5857
5869
  importerName = 'File';
5858
5870
  }
5859
5871
  if (!filePath || !importer) {
5860
- console.error('Usage: hippo import <--chatgpt|--claude|--cursor|--file|--markdown|--vault> <path>, or hippo import --agents [--dry-run]');
5872
+ printError('Usage: hippo import <--chatgpt|--claude|--cursor|--file|--markdown|--vault> <path>, or hippo import --agents [--dry-run]');
5861
5873
  process.exit(1);
5862
5874
  }
5863
5875
  if (!fs.existsSync(filePath)) {
5864
- console.error(`File not found: ${filePath}`);
5876
+ printError(`File not found: ${filePath}`);
5865
5877
  process.exit(1);
5866
5878
  }
5867
5879
  const result = importer(filePath, importOptions);
@@ -5905,7 +5917,7 @@ function cmdImport(hippoRoot, args, flags) {
5905
5917
  function cmdPromote(hippoRoot, id) {
5906
5918
  requireInit(hippoRoot);
5907
5919
  if (!id) {
5908
- console.error('Usage: hippo promote <id>');
5920
+ printError('Usage: hippo promote <id>');
5909
5921
  process.exit(1);
5910
5922
  }
5911
5923
  const ctx = {
@@ -5919,7 +5931,7 @@ function cmdPromote(hippoRoot, id) {
5919
5931
  console.log(` Global store: ${getGlobalRoot()}`);
5920
5932
  }
5921
5933
  catch (err) {
5922
- console.error(`Failed to promote: ${err.message}`);
5934
+ printError(`Failed to promote: ${err.message}`);
5923
5935
  process.exit(1);
5924
5936
  }
5925
5937
  }
@@ -5957,8 +5969,8 @@ function cmdHook(args, flags) {
5957
5969
  }
5958
5970
  if (subcommand === 'install') {
5959
5971
  if (!target || !HOOKS[target]) {
5960
- console.error(`Unknown hook target: ${target ?? '(none)'}`);
5961
- console.error(` Available: ${Object.keys(HOOKS).join(', ')}`);
5972
+ printError(`Unknown hook target: ${target ?? '(none)'}`);
5973
+ printError(` Available: ${Object.keys(HOOKS).join(', ')}`);
5962
5974
  process.exit(1);
5963
5975
  }
5964
5976
  const hook = HOOKS[target];
@@ -6058,7 +6070,7 @@ function cmdHook(args, flags) {
6058
6070
  }
6059
6071
  if (subcommand === 'uninstall') {
6060
6072
  if (!target || !HOOKS[target]) {
6061
- console.error(`Unknown hook target: ${target ?? '(none)'}`);
6073
+ printError(`Unknown hook target: ${target ?? '(none)'}`);
6062
6074
  process.exit(1);
6063
6075
  }
6064
6076
  const hook = HOOKS[target];
@@ -6118,7 +6130,7 @@ function cmdHook(args, flags) {
6118
6130
  }
6119
6131
  return;
6120
6132
  }
6121
- console.error('Usage: hippo hook <install|uninstall|list> [target]');
6133
+ printError('Usage: hippo hook <install|uninstall|list> [target]');
6122
6134
  process.exit(1);
6123
6135
  }
6124
6136
  function withoutHookBlock(text) {
@@ -6265,7 +6277,7 @@ function cmdDailyRunner() {
6265
6277
  const globalRoot = getGlobalRoot();
6266
6278
  // No workspace sleep ever opens the global store, yet hooks in folders without a store compact into it.
6267
6279
  if (isInitialized(globalRoot)) {
6268
- const finished = replayCompactionsAt(globalRoot, (message) => console.error(`compaction replay: ${message}`));
6280
+ const finished = replayCompactionsAt(globalRoot, (message) => log.warn(`compaction replay: ${message}`));
6269
6281
  if (finished > 0)
6270
6282
  console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over in the global store.`);
6271
6283
  }
@@ -6291,7 +6303,7 @@ function cmdDailyRunner() {
6291
6303
  catch (err) {
6292
6304
  failed++;
6293
6305
  const action = args.join(' ');
6294
- console.error(`[hippo] daily-runner failed in ${cwd} during \`${action}\`: ${err.message}`);
6306
+ log.error(`daily-runner failed in ${cwd} during \`${action}\`: ${err.message}`);
6295
6307
  }
6296
6308
  });
6297
6309
  console.log(`Daily maintenance complete: ${processed} workspace${processed === 1 ? '' : 's'} processed, ${failed} command failure${failed === 1 ? '' : 's'}.`);
@@ -6309,7 +6321,7 @@ function cmdWm(hippoRoot, args, flags) {
6309
6321
  const sessionId = flags['session'] ? String(flags['session']).trim() : undefined;
6310
6322
  const taskId = flags['task'] ? String(flags['task']).trim() : undefined;
6311
6323
  if (!content) {
6312
- console.error('Usage: hippo wm push --scope <scope> --content "..." [--importance 0.8] [--session <id>] [--task <id>]');
6324
+ printError('Usage: hippo wm push --scope <scope> --content "..." [--importance 0.8] [--session <id>] [--task <id>]');
6313
6325
  process.exit(1);
6314
6326
  }
6315
6327
  const id = wmPush(hippoRoot, {
@@ -6360,7 +6372,7 @@ function cmdWm(hippoRoot, args, flags) {
6360
6372
  console.log(`Flushed ${count} working memory entries.`);
6361
6373
  return;
6362
6374
  }
6363
- console.error('Usage: hippo wm <push|read|clear|flush>');
6375
+ printError('Usage: hippo wm <push|read|clear|flush>');
6364
6376
  process.exit(1);
6365
6377
  }
6366
6378
  function cmdDag(hippoRoot, flags) {
@@ -6477,7 +6489,7 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
6477
6489
  let depth;
6478
6490
  if (rawDepth !== undefined) {
6479
6491
  if (!Number.isInteger(rawDepth) || rawDepth < 1 || rawDepth > 10) {
6480
- console.error(`--depth must be an integer between 1 and 10 (got ${flags['depth']})`);
6492
+ printError(`--depth must be an integer between 1 and 10 (got ${flags['depth']})`);
6481
6493
  process.exit(2);
6482
6494
  }
6483
6495
  depth = rawDepth;
@@ -6498,10 +6510,10 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
6498
6510
  // intentionally collapses cross-tenant + scope-blocked + missing
6499
6511
  // (codex round 3 P1: distinguishing scope_blocked leaked existence).
6500
6512
  if (r.failure === 'not_drillable') {
6501
- console.error(`Id ${summaryId} is a leaf row, not a level-2+ summary; nothing to drill into.`);
6513
+ printError(`Id ${summaryId} is a leaf row, not a level-2+ summary; nothing to drill into.`);
6502
6514
  }
6503
6515
  else {
6504
- console.error(`No drillable summary at id=${summaryId}.`);
6516
+ printError(`No drillable summary at id=${summaryId}.`);
6505
6517
  }
6506
6518
  process.exit(1);
6507
6519
  }
@@ -6531,7 +6543,7 @@ function cmdAuthCreate(hippoRoot, flags) {
6531
6543
  let role = 'admin';
6532
6544
  if (roleFlag !== undefined) {
6533
6545
  if (roleFlag !== 'admin' && roleFlag !== 'member') {
6534
- console.error(`Invalid --role value: '${roleFlag}'. Use 'admin' or 'member'.`);
6546
+ printError(`Invalid --role value: '${roleFlag}'. Use 'admin' or 'member'.`);
6535
6547
  process.exit(1);
6536
6548
  }
6537
6549
  role = roleFlag;
@@ -6610,7 +6622,7 @@ function cmdAuthRevoke(hippoRoot, keyId, flags) {
6610
6622
  closeHippoDb(db);
6611
6623
  }
6612
6624
  if (keyTenant === undefined) {
6613
- console.error(`Unknown key_id: ${keyId}`);
6625
+ printError(`Unknown key_id: ${keyId}`);
6614
6626
  process.exit(1);
6615
6627
  }
6616
6628
  const ctx = { hippoRoot: root, tenantId: keyTenant, actor: api.adminActor('cli') };
@@ -6619,7 +6631,7 @@ function cmdAuthRevoke(hippoRoot, keyId, flags) {
6619
6631
  revokedAt = api.authRevoke(ctx, keyId).revokedAt;
6620
6632
  }
6621
6633
  catch (err) {
6622
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
6634
+ printError(`Error: ${err instanceof Error ? err.message : String(err)}`);
6623
6635
  process.exit(1);
6624
6636
  }
6625
6637
  if (flags['json']) {
@@ -6643,7 +6655,7 @@ function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
6643
6655
  closeHippoDb(db);
6644
6656
  }
6645
6657
  if (keyTenant === undefined) {
6646
- console.error(`Unknown key_id: ${keyId}`);
6658
+ printError(`Unknown key_id: ${keyId}`);
6647
6659
  process.exit(1);
6648
6660
  }
6649
6661
  const ctx = { hippoRoot: root, tenantId: keyTenant, actor: api.adminActor('cli') };
@@ -6654,7 +6666,7 @@ function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
6654
6666
  api.authUngrant(ctx, keyId, scope);
6655
6667
  }
6656
6668
  catch (err) {
6657
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
6669
+ printError(`Error: ${err instanceof Error ? err.message : String(err)}`);
6658
6670
  process.exit(1);
6659
6671
  }
6660
6672
  if (flags['json']) {
@@ -6681,13 +6693,13 @@ function cmdAuditList(hippoRoot, flags) {
6681
6693
  // Regenerate from Set to prevent future drift (v1.11.5: pre-v1.11.5 message
6682
6694
  // was hand-maintained and had drifted — missed 'auth_revoke' and 'outcome').
6683
6695
  const expected = Array.from(VALID_AUDIT_OPS).join(' | ');
6684
- console.error(`Unknown --op value: ${opFlag}. Expected one of: ${expected}.`);
6696
+ printError(`Unknown --op value: ${opFlag}. Expected one of: ${expected}.`);
6685
6697
  process.exit(1);
6686
6698
  }
6687
6699
  const op = opFlag;
6688
6700
  const since = typeof flags['since'] === 'string' ? flags['since'] : undefined;
6689
6701
  if (since !== undefined && !Number.isFinite(new Date(since).getTime())) {
6690
- console.error(`Invalid --since: ${since} (expected an ISO timestamp like 2026-04-22 or 2026-04-22T12:00:00Z).`);
6702
+ printError(`Invalid --since: ${since} (expected an ISO timestamp like 2026-04-22 or 2026-04-22T12:00:00Z).`);
6691
6703
  process.exit(1);
6692
6704
  }
6693
6705
  const limitRaw = flags['limit'];
@@ -6695,13 +6707,13 @@ function cmdAuditList(hippoRoot, flags) {
6695
6707
  if (limitRaw !== undefined && typeof limitRaw !== 'boolean') {
6696
6708
  const parsed = parseInt(String(limitRaw), 10);
6697
6709
  if (!Number.isFinite(parsed)) {
6698
- console.error(`Invalid --limit value: ${String(limitRaw)} (expected a positive integer).`);
6710
+ printError(`Invalid --limit value: ${String(limitRaw)} (expected a positive integer).`);
6699
6711
  process.exit(1);
6700
6712
  }
6701
6713
  limit = parsed;
6702
6714
  }
6703
6715
  if (limit < 1 || limit > 10000) {
6704
- console.error(`--limit must be between 1 and 10000 (got ${limit}).`);
6716
+ printError(`--limit must be between 1 and 10000 (got ${limit}).`);
6705
6717
  process.exit(1);
6706
6718
  }
6707
6719
  const ctx = { hippoRoot: root, tenantId, actor: { subject: 'cli', role: 'admin' } };
@@ -6729,7 +6741,7 @@ function printAuditPruneUsage() {
6729
6741
  function cmdAuditPrune(hippoRoot, flags) {
6730
6742
  const olderThanRaw = typeof flags['older-than'] === 'string' ? flags['older-than'] : '';
6731
6743
  if (!olderThanRaw) {
6732
- console.error('Usage: hippo audit prune --older-than <Nd> [--dry-run] [--tenant <t>]');
6744
+ printError('Usage: hippo audit prune --older-than <Nd> [--dry-run] [--tenant <t>]');
6733
6745
  process.exit(1);
6734
6746
  }
6735
6747
  let olderThanDays;
@@ -6737,7 +6749,7 @@ function cmdAuditPrune(hippoRoot, flags) {
6737
6749
  olderThanDays = parseOlderThanFlag(olderThanRaw);
6738
6750
  }
6739
6751
  catch (e) {
6740
- console.error(e.message);
6752
+ printError(e.message);
6741
6753
  process.exit(1);
6742
6754
  }
6743
6755
  const tenantId = typeof flags['tenant'] === 'string'
@@ -6773,7 +6785,7 @@ function cmdAuditLog(hippoRoot, args, flags) {
6773
6785
  cmdAuditPrune(hippoRoot, flags);
6774
6786
  return;
6775
6787
  }
6776
- console.error(`Unknown audit subcommand: ${sub}. Expected: list | prune.`);
6788
+ printError(`Unknown audit subcommand: ${sub}. Expected: list | prune.`);
6777
6789
  process.exit(1);
6778
6790
  }
6779
6791
  // ---------------------------------------------------------------------------
@@ -6794,7 +6806,7 @@ function resolveGoalSession(flags) {
6794
6806
  ? String(flags['session-id'])
6795
6807
  : process.env.HIPPO_SESSION_ID ?? '').trim();
6796
6808
  if (!sessionId) {
6797
- console.error('session id required (set HIPPO_SESSION_ID or pass --session-id)');
6809
+ printError('session id required (set HIPPO_SESSION_ID or pass --session-id)');
6798
6810
  process.exit(1);
6799
6811
  }
6800
6812
  const tenantId = (flags['tenant-id'] !== undefined
@@ -6805,51 +6817,51 @@ function resolveGoalSession(flags) {
6805
6817
  function cmdGoalPush(hippoRoot, args, flags) {
6806
6818
  const rawName = args.join(' ').trim();
6807
6819
  if (!rawName) {
6808
- console.error('Usage: hippo goal push <name> [--policy <type>] [--success "<condition>"] [--level N] [--parent <goalId>]');
6820
+ printError('Usage: hippo goal push <name> [--policy <type>] [--success "<condition>"] [--level N] [--parent <goalId>]');
6809
6821
  process.exit(1);
6810
6822
  }
6811
6823
  // Sanitize at WRITE time so corrupt names never enter the DB.
6812
6824
  const name = sanitizeGoalName(rawName);
6813
6825
  if (name !== rawName) {
6814
- console.error('note: stripped control characters from goal name');
6826
+ printError('note: stripped control characters from goal name');
6815
6827
  }
6816
6828
  const { sessionId, tenantId } = resolveGoalSession(flags);
6817
6829
  let policy;
6818
6830
  const policyRaw = flags['policy'];
6819
6831
  if (policyRaw === true) {
6820
- console.error('--policy requires a value (e.g., --policy error-prioritized)');
6832
+ printError('--policy requires a value (e.g., --policy error-prioritized)');
6821
6833
  process.exit(1);
6822
6834
  }
6823
6835
  if (typeof policyRaw === 'string') {
6824
6836
  if (!GOAL_POLICY_TYPES.includes(policyRaw)) {
6825
- console.error(`Unknown --policy '${policyRaw}'. Expected one of: ${GOAL_POLICY_TYPES.join(' | ')}.`);
6837
+ printError(`Unknown --policy '${policyRaw}'. Expected one of: ${GOAL_POLICY_TYPES.join(' | ')}.`);
6826
6838
  process.exit(1);
6827
6839
  }
6828
6840
  policy = { policyType: policyRaw };
6829
6841
  }
6830
6842
  const successRaw = flags['success'];
6831
6843
  if (successRaw === true) {
6832
- console.error('--success requires a value (e.g., --success "<condition>")');
6844
+ printError('--success requires a value (e.g., --success "<condition>")');
6833
6845
  process.exit(1);
6834
6846
  }
6835
6847
  const successCondition = typeof successRaw === 'string' ? successRaw : undefined;
6836
6848
  const levelRaw = flags['level'];
6837
6849
  let level;
6838
6850
  if (levelRaw === true) {
6839
- console.error('--level requires a value (e.g., --level 1)');
6851
+ printError('--level requires a value (e.g., --level 1)');
6840
6852
  process.exit(1);
6841
6853
  }
6842
6854
  if (levelRaw !== undefined) {
6843
6855
  const parsed = Number(levelRaw);
6844
6856
  if (!Number.isFinite(parsed) || parsed < 0 || parsed > 2 || !Number.isInteger(parsed)) {
6845
- console.error('--level must be an integer in [0, 2]');
6857
+ printError('--level must be an integer in [0, 2]');
6846
6858
  process.exit(1);
6847
6859
  }
6848
6860
  level = parsed;
6849
6861
  }
6850
6862
  const parentRaw = flags['parent'];
6851
6863
  if (parentRaw === true) {
6852
- console.error('--parent requires a value (e.g., --parent <goalId>)');
6864
+ printError('--parent requires a value (e.g., --parent <goalId>)');
6853
6865
  process.exit(1);
6854
6866
  }
6855
6867
  const parentGoalId = typeof parentRaw === 'string' ? parentRaw : undefined;
@@ -6915,19 +6927,19 @@ function cmdGoalList(hippoRoot, flags) {
6915
6927
  function cmdGoalComplete(hippoRoot, args, flags) {
6916
6928
  const id = args[0];
6917
6929
  if (!id) {
6918
- console.error('Usage: hippo goal complete <id> [--outcome <0..1>] [--no-propagate]');
6930
+ printError('Usage: hippo goal complete <id> [--outcome <0..1>] [--no-propagate]');
6919
6931
  process.exit(1);
6920
6932
  }
6921
6933
  let outcomeScore;
6922
6934
  const outcomeRaw = flags['outcome'];
6923
6935
  if (outcomeRaw === true) {
6924
- console.error('--outcome requires a value (e.g., --outcome 0.9)');
6936
+ printError('--outcome requires a value (e.g., --outcome 0.9)');
6925
6937
  process.exit(1);
6926
6938
  }
6927
6939
  if (outcomeRaw !== undefined) {
6928
6940
  const parsed = Number(outcomeRaw);
6929
6941
  if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
6930
- console.error('--outcome must be a number in [0, 1]');
6942
+ printError('--outcome must be a number in [0, 1]');
6931
6943
  process.exit(1);
6932
6944
  }
6933
6945
  outcomeScore = parsed;
@@ -6939,7 +6951,7 @@ function cmdGoalComplete(hippoRoot, args, flags) {
6939
6951
  function cmdGoalSuspend(hippoRoot, args) {
6940
6952
  const id = args[0];
6941
6953
  if (!id) {
6942
- console.error('Usage: hippo goal suspend <id>');
6954
+ printError('Usage: hippo goal suspend <id>');
6943
6955
  process.exit(1);
6944
6956
  }
6945
6957
  suspendGoal(hippoRoot, id);
@@ -6948,7 +6960,7 @@ function cmdGoalSuspend(hippoRoot, args) {
6948
6960
  function cmdGoalResume(hippoRoot, args) {
6949
6961
  const id = args[0];
6950
6962
  if (!id) {
6951
- console.error('Usage: hippo goal resume <id>');
6963
+ printError('Usage: hippo goal resume <id>');
6952
6964
  process.exit(1);
6953
6965
  }
6954
6966
  resumeGoal(hippoRoot, id);
@@ -6957,7 +6969,7 @@ function cmdGoalResume(hippoRoot, args) {
6957
6969
  function cmdGoal(hippoRoot, args, flags) {
6958
6970
  const sub = args[0];
6959
6971
  if (!sub) {
6960
- console.error('Usage: hippo goal <push|list|complete|suspend|resume> [args]');
6972
+ printError('Usage: hippo goal <push|list|complete|suspend|resume> [args]');
6961
6973
  process.exit(1);
6962
6974
  }
6963
6975
  const subArgs = args.slice(1);
@@ -6978,14 +6990,14 @@ function cmdGoal(hippoRoot, args, flags) {
6978
6990
  cmdGoalResume(hippoRoot, subArgs);
6979
6991
  return;
6980
6992
  default:
6981
- console.error(`Unknown goal subcommand: ${sub}. Expected: push | list | complete | suspend | resume.`);
6993
+ printError(`Unknown goal subcommand: ${sub}. Expected: push | list | complete | suspend | resume.`);
6982
6994
  process.exit(1);
6983
6995
  }
6984
6996
  }
6985
6997
  function cmdAuth(hippoRoot, args, flags) {
6986
6998
  const sub = args[0];
6987
6999
  if (!sub) {
6988
- console.error('Usage: hippo auth <create|list|revoke|grant|ungrant> [options]');
7000
+ printError('Usage: hippo auth <create|list|revoke|grant|ungrant> [options]');
6989
7001
  process.exit(1);
6990
7002
  }
6991
7003
  const subArgs = args.slice(1);
@@ -6999,7 +7011,7 @@ function cmdAuth(hippoRoot, args, flags) {
6999
7011
  case 'revoke': {
7000
7012
  const keyId = subArgs[0];
7001
7013
  if (!keyId) {
7002
- console.error('Usage: hippo auth revoke <key_id>');
7014
+ printError('Usage: hippo auth revoke <key_id>');
7003
7015
  process.exit(1);
7004
7016
  }
7005
7017
  cmdAuthRevoke(hippoRoot, keyId, flags);
@@ -7009,14 +7021,14 @@ function cmdAuth(hippoRoot, args, flags) {
7009
7021
  case 'ungrant': {
7010
7022
  const [keyId, scope] = subArgs;
7011
7023
  if (!keyId || !scope) {
7012
- console.error(`Usage: hippo auth ${sub} <key_id> <scope>`);
7024
+ printError(`Usage: hippo auth ${sub} <key_id> <scope>`);
7013
7025
  process.exit(1);
7014
7026
  }
7015
7027
  cmdAuthScopeGrant(hippoRoot, keyId, scope, sub === 'grant', flags);
7016
7028
  return;
7017
7029
  }
7018
7030
  default:
7019
- console.error(`Unknown auth subcommand: ${sub}. Expected: create | list | revoke | grant | ungrant.`);
7031
+ printError(`Unknown auth subcommand: ${sub}. Expected: create | list | revoke | grant | ungrant.`);
7020
7032
  process.exit(1);
7021
7033
  }
7022
7034
  }
@@ -7037,7 +7049,7 @@ function cmdSlackBackfill(hippoRoot, flags) {
7037
7049
  // Real fetcher requires SLACK_BOT_TOKEN with channels:history scope.
7038
7050
  const token = process.env.SLACK_BOT_TOKEN;
7039
7051
  if (!token) {
7040
- console.error('SLACK_BOT_TOKEN is not set. Backfill requires a Slack bot token with channels:history scope.');
7052
+ printError('SLACK_BOT_TOKEN is not set. Backfill requires a Slack bot token with channels:history scope.');
7041
7053
  process.exit(2);
7042
7054
  }
7043
7055
  // --since is advisory in V1: the slack_cursors row drives resume, so the
@@ -7060,7 +7072,7 @@ function cmdSlackBackfill(hippoRoot, flags) {
7060
7072
  console.log(`backfill ${channel}: ${r.ingested} new messages across ${r.pages} pages`);
7061
7073
  })
7062
7074
  .catch((e) => {
7063
- console.error('backfill failed:', e.message);
7075
+ printError('backfill failed:', e.message);
7064
7076
  process.exit(3);
7065
7077
  });
7066
7078
  }
@@ -7080,12 +7092,12 @@ function cmdSlackDlqList(hippoRoot, _flags) {
7080
7092
  function cmdSlackDlqReplay(hippoRoot, args, flags) {
7081
7093
  const idArg = args[2];
7082
7094
  if (!idArg) {
7083
- console.error('Usage: hippo slack dlq replay <id> [--force]');
7095
+ printError('Usage: hippo slack dlq replay <id> [--force]');
7084
7096
  process.exit(1);
7085
7097
  }
7086
7098
  const id = Number(idArg);
7087
7099
  if (!Number.isFinite(id) || !Number.isInteger(id) || id < 1) {
7088
- console.error(`replay: invalid id ${idArg}`);
7100
+ printError(`replay: invalid id ${idArg}`);
7089
7101
  process.exit(1);
7090
7102
  }
7091
7103
  const force = flags.force === true;
@@ -7094,7 +7106,7 @@ function cmdSlackDlqReplay(hippoRoot, args, flags) {
7094
7106
  signingSecret: process.env.SLACK_SIGNING_SECRET,
7095
7107
  });
7096
7108
  if (!result.ok) {
7097
- console.error(`replay failed: status=${result.status} retry_count=${result.retryCount}${result.reason ? ` reason=${result.reason}` : ''}`);
7109
+ printError(`replay failed: status=${result.status} retry_count=${result.retryCount}${result.reason ? ` reason=${result.reason}` : ''}`);
7098
7110
  process.exit(1);
7099
7111
  }
7100
7112
  console.log(`replay ok: status=${result.status} memory_id=${result.memoryId ?? '(none)'} retry_count=${result.retryCount}`);
@@ -7109,7 +7121,7 @@ function cmdSlackWorkspacesAdd(hippoRoot, flags) {
7109
7121
  const teamId = typeof flags['team'] === 'string' ? flags['team'].trim() : '';
7110
7122
  const tenantId = typeof flags['tenant'] === 'string' ? flags['tenant'].trim() : '';
7111
7123
  if (!teamId || !tenantId) {
7112
- console.error('Usage: hippo slack workspaces add --team <T> --tenant <t>');
7124
+ printError('Usage: hippo slack workspaces add --team <T> --tenant <t>');
7113
7125
  process.exit(1);
7114
7126
  }
7115
7127
  const db = openHippoDb(hippoRoot);
@@ -7140,14 +7152,14 @@ function cmdSlackWorkspacesList(hippoRoot) {
7140
7152
  function cmdSlackWorkspacesRemove(hippoRoot, flags) {
7141
7153
  const teamId = typeof flags['team'] === 'string' ? flags['team'].trim() : '';
7142
7154
  if (!teamId) {
7143
- console.error('Usage: hippo slack workspaces remove --team <T>');
7155
+ printError('Usage: hippo slack workspaces remove --team <T>');
7144
7156
  process.exit(1);
7145
7157
  }
7146
7158
  const db = openHippoDb(hippoRoot);
7147
7159
  try {
7148
7160
  const removed = removeSlackWorkspace(db, teamId);
7149
7161
  if (!removed) {
7150
- console.error(`no workspace registered for team ${teamId}`);
7162
+ printError(`no workspace registered for team ${teamId}`);
7151
7163
  process.exit(1);
7152
7164
  }
7153
7165
  console.log(`removed: ${teamId}`);
@@ -7187,7 +7199,7 @@ function cmdSlack(hippoRoot, args, flags) {
7187
7199
  printSlackWorkspacesUsage();
7188
7200
  process.exit(1);
7189
7201
  }
7190
- console.error('Usage: hippo slack <backfill|dlq list|dlq replay <id> [--force]|workspaces add|workspaces list|workspaces remove> [...]');
7202
+ printError('Usage: hippo slack <backfill|dlq list|dlq replay <id> [--force]|workspaces add|workspaces list|workspaces remove> [...]');
7191
7203
  process.exit(1);
7192
7204
  }
7193
7205
  export function usageText() {
@@ -7261,17 +7273,21 @@ Commands:
7261
7273
  where cost_factor = min(0.3, tokens / 10000). Re-sorts
7262
7274
  results by utility. Default off. RESEARCH.md §PFC.OFC.
7263
7275
  --reranker <name> Apply a reranker pass after retrieval
7264
- (cross-encoder|jev|llm). Looks up the named
7265
- reranker from src/rerankers/index.ts and re-orders
7266
- the top-K candidates. Default unset (no reranker).
7276
+ (cross-encoder|jev|clef-flash|clef|llm). Looks up
7277
+ the named reranker from src/rerankers/index.ts and
7278
+ re-orders the top-K candidates. Default unset (no reranker).
7267
7279
  jev calls the hosted TypeSafe Jev API: it needs
7268
7280
  TYPESAFE_API_KEY, sends the query and candidate
7269
7281
  text to that API, costs about 0.0004 USD a recall,
7270
7282
  and falls back to cross-encoder on any failure.
7283
+ clef-flash and clef send the same request to
7284
+ Cloudflare Workers AI (CLOUDFLARE_ACCOUNT_ID and
7285
+ CLOUDFLARE_API_TOKEN) or to HIPPO_CLEF_ENDPOINT, and
7286
+ keep the native order on any failure.
7271
7287
  See docs/evals/2026-09-19-jev-reranker.md and
7272
7288
  docs/plans/2026-05-10-f6-reranker-hardening.md.
7273
7289
  --reranker-top-k <n> Cap candidates passed to the reranker (default 50;
7274
- 40 for jev).
7290
+ 40 for jev, clef-flash and clef).
7275
7291
  --goal <tag> dlPFC goal-conditioned recall: memories tagged with
7276
7292
  the goal tag get a 1.5x score boost and results are
7277
7293
  re-sorted. Default off. RESEARCH.md §PFC.dlPFC.
@@ -7410,7 +7426,16 @@ Commands:
7410
7426
  --global Operate on the global store
7411
7427
  dormant restore <id> Bring a dormant memory back to active memory
7412
7428
  dormant forget <id> Delete a dormant memory permanently
7413
- quarantine [list] List memories a connector flagged as an instruction attempt, pending review
7429
+ projects [list] List the project names in a store, with a hint for old worktree names
7430
+ --json Output as JSON
7431
+ --global Operate on the global store
7432
+ projects merge <from> <into> [--apply]
7433
+ Fold one project name into another (dry run unless --apply;
7434
+ writes a backup and one audit event first)
7435
+ projects repair [--apply]
7436
+ Re-tag merged rows older versions of sleep saved as user-global,
7437
+ by their parents' project (dry run unless --apply)
7438
+ quarantine [list] List memories a connector flagged as an instruction attempt, pending review
7414
7439
  --all Include approved and rejected rows too (default: pending only)
7415
7440
  --json Output as JSON
7416
7441
  --global Operate on the global store
@@ -7882,7 +7907,7 @@ async function main(command, args, flags, hippoRoot) {
7882
7907
  * --hops value-less guard, so every current and future command - including the
7883
7908
  * thin-client dispatch relays - sees --scope only as a non-empty string. */
7884
7909
  if ('scope' in flags && (typeof flags['scope'] !== 'string' || !flags['scope'].trim())) {
7885
- console.error('--scope requires a non-empty value (e.g. --scope slack:private:C1).');
7910
+ printError('--scope requires a non-empty value (e.g. --scope slack:private:C1).');
7886
7911
  process.exit(1);
7887
7912
  }
7888
7913
  // parseArgs stores a value-less flag as boolean true, and NaN then survives every
@@ -7896,7 +7921,7 @@ async function main(command, args, flags, hippoRoot) {
7896
7921
  if (raw === undefined)
7897
7922
  continue;
7898
7923
  if (typeof raw !== 'string' || !raw.trim() || !Number.isFinite(Number(raw))) {
7899
- console.error(`--${key} requires a numeric value.`);
7924
+ printError(`--${key} requires a numeric value.`);
7900
7925
  process.exit(1);
7901
7926
  }
7902
7927
  }
@@ -7904,7 +7929,7 @@ async function main(command, args, flags, hippoRoot) {
7904
7929
  // so no single coercion of an inline value would be correct for every one of them.
7905
7930
  for (const key of BOOLEAN_FLAGS) {
7906
7931
  if (Object.hasOwn(flags, key) && typeof flags[key] !== 'boolean') {
7907
- console.error(`--${key} takes no value`);
7932
+ printError(`--${key} takes no value`);
7908
7933
  process.exit(1);
7909
7934
  }
7910
7935
  }
@@ -7913,14 +7938,14 @@ async function main(command, args, flags, hippoRoot) {
7913
7938
  if (unknownFlags.length > 0) {
7914
7939
  const names = unknownFlags.map((key) => `--${key}`).join(', ');
7915
7940
  if (DESTRUCTIVE_COMMANDS.has(command)) {
7916
- console.error(`Unknown flag ${names} for hippo ${command}. Nothing was changed.`);
7941
+ printError(`Unknown flag ${names} for hippo ${command}. Nothing was changed.`);
7917
7942
  process.exit(2);
7918
7943
  }
7919
- console.error(`hippo: ignoring unknown flag ${names}. A later release will reject it.`);
7944
+ printError(`hippo: ignoring unknown flag ${names}. A later release will reject it.`);
7920
7945
  }
7921
7946
  const refusal = Object.hasOwn(flags, 'dry-run') ? dryRunRefusal(command, args, flags) : null;
7922
7947
  if (refusal) {
7923
- console.error(refusal);
7948
+ printError(refusal);
7924
7949
  process.exit(2);
7925
7950
  }
7926
7951
  switch (command) {
@@ -7936,7 +7961,7 @@ async function main(command, args, flags, hippoRoot) {
7936
7961
  text = args.join(' ').trim();
7937
7962
  }
7938
7963
  if (!text || text.length < 3) {
7939
- console.error('Memory content too short (minimum 3 characters).');
7964
+ printError('Memory content too short (minimum 3 characters).');
7940
7965
  process.exit(1);
7941
7966
  }
7942
7967
  // Thin-client routing. When a server is up, simple `remember` calls go
@@ -7960,11 +7985,11 @@ async function main(command, args, flags, hippoRoot) {
7960
7985
  const thinOwnerRaw = typeof flags['owner'] === 'string' ? flags['owner'] : undefined;
7961
7986
  const thinOwnerCheck = validateOwner(thinOwnerRaw, { strict: isStrictOwnerEnv() });
7962
7987
  if (!thinOwnerCheck.ok) {
7963
- console.error(thinOwnerCheck.message);
7988
+ printError(thinOwnerCheck.message);
7964
7989
  process.exit(1);
7965
7990
  }
7966
7991
  if (thinOwnerCheck.message)
7967
- console.error(thinOwnerCheck.message);
7992
+ printError(thinOwnerCheck.message);
7968
7993
  const remembered = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
7969
7994
  const result = await client.remember(info.url, apiKey, {
7970
7995
  content: text,
@@ -7977,7 +8002,7 @@ async function main(command, args, flags, hippoRoot) {
7977
8002
  console.log(`Remembered [${result.id}] (via ${info.url})`);
7978
8003
  console.log(` Kind: ${result.kind} | Tenant: ${result.tenantId}`);
7979
8004
  for (const w of result.warnings ?? [])
7980
- console.error(`Warning: ${w}`);
8005
+ printError(`Warning: ${w}`);
7981
8006
  });
7982
8007
  if (remembered)
7983
8008
  break;
@@ -7989,7 +8014,7 @@ async function main(command, args, flags, hippoRoot) {
7989
8014
  case 'recall': {
7990
8015
  const query = args.join(' ').trim();
7991
8016
  if (!query) {
7992
- console.error('Please provide a search query.');
8017
+ printError('Please provide a search query.');
7993
8018
  process.exit(1);
7994
8019
  }
7995
8020
  await cmdRecall(hookStoreRoot(hippoRoot), query, flags);
@@ -7998,7 +8023,7 @@ async function main(command, args, flags, hippoRoot) {
7998
8023
  case 'drill': {
7999
8024
  const summaryId = args[0];
8000
8025
  if (!summaryId) {
8001
- console.error('Usage: hippo drill <summary-id> [--limit N] [--budget N]');
8026
+ printError('Usage: hippo drill <summary-id> [--limit N] [--budget N]');
8002
8027
  process.exit(1);
8003
8028
  }
8004
8029
  cmdDrillDown(hippoRoot, summaryId, flags);
@@ -8007,7 +8032,7 @@ async function main(command, args, flags, hippoRoot) {
8007
8032
  case 'assemble': {
8008
8033
  const sessionId = typeof flags['session'] === 'string' ? flags['session'] : args[0];
8009
8034
  if (!sessionId) {
8010
- console.error('Usage: hippo assemble --session <id> [--budget N] [--fresh-tail N] [--no-summarize-older] [--json]');
8035
+ printError('Usage: hippo assemble --session <id> [--budget N] [--fresh-tail N] [--no-summarize-older] [--json]');
8011
8036
  process.exit(1);
8012
8037
  }
8013
8038
  cmdAssemble(hippoRoot, sessionId, flags);
@@ -8017,7 +8042,7 @@ async function main(command, args, flags, hippoRoot) {
8017
8042
  const oldId = args[0];
8018
8043
  const newContent = args.slice(1).join(' ').trim();
8019
8044
  if (!oldId || !newContent) {
8020
- console.error('Usage: hippo supersede <old-id> "<new content>" [--layer L] [--tag T] [--pin]');
8045
+ printError('Usage: hippo supersede <old-id> "<new content>" [--layer L] [--tag T] [--pin]');
8021
8046
  process.exit(1);
8022
8047
  }
8023
8048
  cmdSupersede(hippoRoot, oldId, newContent, flags);
@@ -8026,7 +8051,7 @@ async function main(command, args, flags, hippoRoot) {
8026
8051
  case 'explain': {
8027
8052
  const query = args.join(' ').trim();
8028
8053
  if (!query) {
8029
- console.error('Please provide a search query.');
8054
+ printError('Please provide a search query.');
8030
8055
  process.exit(1);
8031
8056
  }
8032
8057
  await cmdExplain(hippoRoot, query, flags);
@@ -8044,7 +8069,7 @@ async function main(command, args, flags, hippoRoot) {
8044
8069
  break;
8045
8070
  }
8046
8071
  if (!sub) {
8047
- console.error('Usage: hippo trace <memory-id> | hippo trace record --task <t> --steps <json> --outcome <o>');
8072
+ printError('Usage: hippo trace <memory-id> | hippo trace record --task <t> --steps <json> --outcome <o>');
8048
8073
  process.exit(1);
8049
8074
  }
8050
8075
  cmdTrace(hippoRoot, sub, flags);
@@ -8273,6 +8298,9 @@ async function main(command, args, flags, hippoRoot) {
8273
8298
  case 'dormant':
8274
8299
  cmdDormant(hippoRoot, args, flags);
8275
8300
  break;
8301
+ case 'projects':
8302
+ (await import('./cli/projects.js')).cmdProjects(hippoRoot, args, flags);
8303
+ break;
8276
8304
  case 'quarantine':
8277
8305
  cmdQuarantine(hippoRoot, args, flags);
8278
8306
  break;
@@ -8294,7 +8322,7 @@ async function main(command, args, flags, hippoRoot) {
8294
8322
  case 'support-bundle': {
8295
8323
  const outFlag = cardStringFlag(flags, 'out');
8296
8324
  if (outFlag === '') {
8297
- console.error('--out requires a file path.');
8325
+ printError('--out requires a file path.');
8298
8326
  process.exit(1);
8299
8327
  }
8300
8328
  const includeLogs = flags['include-logs'] === true;
@@ -8309,10 +8337,10 @@ async function main(command, args, flags, hippoRoot) {
8309
8337
  }
8310
8338
  catch (err) {
8311
8339
  if (err instanceof Error && 'code' in err && err.code === 'EEXIST') {
8312
- console.error(`${file} already exists; pass --out to choose another file. Nothing was written.`);
8340
+ printError(`${file} already exists; pass --out to choose another file. Nothing was written.`);
8313
8341
  }
8314
8342
  else {
8315
- console.error(err instanceof Error ? err.message : String(err));
8343
+ printError(err instanceof Error ? err.message : String(err));
8316
8344
  }
8317
8345
  process.exit(1);
8318
8346
  }
@@ -8344,7 +8372,7 @@ async function main(command, args, flags, hippoRoot) {
8344
8372
  case 'forget': {
8345
8373
  const id = args[0];
8346
8374
  if (!id) {
8347
- console.error('Please provide a memory ID.');
8375
+ printError('Please provide a memory ID.');
8348
8376
  process.exit(1);
8349
8377
  }
8350
8378
  // Archive has its own HTTP route (POST /v1/memories/:id/archive); route
@@ -8352,7 +8380,7 @@ async function main(command, args, flags, hippoRoot) {
8352
8380
  const archive = flags['archive'] === true;
8353
8381
  const reason = typeof flags['reason'] === 'string' ? flags['reason'] : null;
8354
8382
  if (archive && !reason) {
8355
- console.error(ARCHIVE_REASON_REQUIRED);
8383
+ printError(ARCHIVE_REASON_REQUIRED);
8356
8384
  process.exit(1);
8357
8385
  }
8358
8386
  if (flags['dry-run'] === true) {
@@ -8376,7 +8404,7 @@ async function main(command, args, flags, hippoRoot) {
8376
8404
  if (client.classifyTransportFailure(err) !== 'none')
8377
8405
  throw err;
8378
8406
  const msg = err instanceof Error ? err.message : String(err);
8379
- console.error(archive ? `Could not archive ${id}: ${msg}` : msg);
8407
+ printError(archive ? `Could not archive ${id}: ${msg}` : msg);
8380
8408
  process.exit(1);
8381
8409
  }
8382
8410
  });
@@ -8388,7 +8416,7 @@ async function main(command, args, flags, hippoRoot) {
8388
8416
  case 'inspect': {
8389
8417
  const id = args[0];
8390
8418
  if (!id) {
8391
- console.error('Please provide a memory ID.');
8419
+ printError('Please provide a memory ID.');
8392
8420
  process.exit(1);
8393
8421
  }
8394
8422
  cmdInspect(hippoRoot, id);
@@ -8424,7 +8452,7 @@ async function main(command, args, flags, hippoRoot) {
8424
8452
  case 'promote': {
8425
8453
  const id = args[0];
8426
8454
  if (!id) {
8427
- console.error('Please provide a memory ID.');
8455
+ printError('Please provide a memory ID.');
8428
8456
  process.exit(1);
8429
8457
  }
8430
8458
  const promoted = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
@@ -8433,7 +8461,7 @@ async function main(command, args, flags, hippoRoot) {
8433
8461
  console.log(`Promoted ${id} to global store as ${result.globalId}`);
8434
8462
  }
8435
8463
  catch (err) {
8436
- console.error(`Failed to promote: ${err.message}`);
8464
+ printError(`Failed to promote: ${err.message}`);
8437
8465
  process.exit(1);
8438
8466
  }
8439
8467
  });
@@ -8487,13 +8515,13 @@ async function main(command, args, flags, hippoRoot) {
8487
8515
  console.log('Use --force to share anyway.');
8488
8516
  }
8489
8517
  else {
8490
- console.error(`Memory not found: ${shareId}`);
8518
+ printError(`Memory not found: ${shareId}`);
8491
8519
  process.exit(1);
8492
8520
  }
8493
8521
  }
8494
8522
  }
8495
8523
  else {
8496
- console.error('Usage: hippo share <memory_id> [--force] or hippo share --auto [--dry-run]');
8524
+ printError('Usage: hippo share <memory_id> [--force] or hippo share --auto [--dry-run]');
8497
8525
  process.exit(1);
8498
8526
  }
8499
8527
  break;
@@ -8571,7 +8599,7 @@ async function main(command, args, flags, hippoRoot) {
8571
8599
  captureSource = 'last-session';
8572
8600
  }
8573
8601
  if (!captureSource) {
8574
- console.error('Usage: hippo capture --stdin|--file <path>|--last-session [--transcript <path>] [--log-file <path>] [--dry-run] [--global]');
8602
+ printError('Usage: hippo capture --stdin|--file <path>|--last-session [--transcript <path>] [--log-file <path>] [--dry-run] [--global]');
8575
8603
  process.exit(1);
8576
8604
  }
8577
8605
  // Bounded, and only when last-session has no explicit path: the
@@ -8619,7 +8647,7 @@ async function main(command, args, flags, hippoRoot) {
8619
8647
  const portRaw = flags['port'] ?? process.env['HIPPO_PORT'] ?? '6789';
8620
8648
  const port = Number(portRaw);
8621
8649
  if (!Number.isFinite(port) || port < 0) {
8622
- console.error(`Invalid --port: ${String(portRaw)}`);
8650
+ printError(`Invalid --port: ${String(portRaw)}`);
8623
8651
  process.exit(1);
8624
8652
  }
8625
8653
  const host = typeof flags['host'] === 'string' ? flags['host'] : '127.0.0.1';
@@ -8636,19 +8664,19 @@ async function main(command, args, flags, hippoRoot) {
8636
8664
  requireInit(hippoRoot);
8637
8665
  if (flags['churn'] === true) {
8638
8666
  if (args[0] || flags['id'] !== undefined) {
8639
- console.error('Usage: hippo invalidate --churn [--dry-run]');
8640
- console.error('--churn takes no pattern or --id.');
8667
+ printError('Usage: hippo invalidate --churn [--dry-run]');
8668
+ printError('--churn takes no pattern or --id.');
8641
8669
  process.exit(1);
8642
8670
  }
8643
8671
  if (!isGitRepo(process.cwd())) {
8644
- console.error('hippo invalidate --churn must run inside a git repository.');
8672
+ printError('hippo invalidate --churn must run inside a git repository.');
8645
8673
  process.exit(1);
8646
8674
  }
8647
8675
  const churnDryRun = flags['dry-run'] === true;
8648
8676
  let churnFailed = false;
8649
8677
  for (const { root, result } of runChurnStaleForRepo(hippoRoot, churnDryRun)) {
8650
8678
  if (result.error) {
8651
- console.error(`Churn-staleness check failed for ${root}: ${result.error}`);
8679
+ printError(`Churn-staleness check failed for ${root}: ${result.error}`);
8652
8680
  churnFailed = true;
8653
8681
  continue;
8654
8682
  }
@@ -8674,21 +8702,21 @@ async function main(command, args, flags, hippoRoot) {
8674
8702
  if (flags['id'] === true) {
8675
8703
  // Value-less --id must never silently fall through to pattern mode
8676
8704
  // (pattern mode writes broadly; an ignored --id reverses user intent).
8677
- console.error('--id requires a memory id');
8705
+ printError('--id requires a memory id');
8678
8706
  process.exit(1);
8679
8707
  }
8680
8708
  const onlyId = typeof flags['id'] === 'string' ? flags['id'] : undefined;
8681
8709
  if (typeof flags['dry-run'] === 'string') {
8682
8710
  // Dead: the earlier global BOOLEAN_FLAGS guard now exits first on any --dry-run=<v>.
8683
8711
  // Kept as defence in depth on a destructive command.
8684
- console.error('--dry-run takes no value');
8712
+ printError('--dry-run takes no value');
8685
8713
  process.exit(1);
8686
8714
  }
8687
8715
  const dryRun = flags['dry-run'] === true;
8688
8716
  if ((target && onlyId) || (!target && !onlyId)) {
8689
- console.error('Usage: hippo invalidate "<old pattern>" [--dry-run] [--reason "<why>"]');
8690
- console.error(' hippo invalidate --id <memory-id> [--dry-run] [--reason "<why>"]');
8691
- console.error('Pass a pattern OR --id, not both. Tag matching is EXACT: the full pattern must equal a tag.');
8717
+ printError('Usage: hippo invalidate "<old pattern>" [--dry-run] [--reason "<why>"]');
8718
+ printError(' hippo invalidate --id <memory-id> [--dry-run] [--reason "<why>"]');
8719
+ printError('Pass a pattern OR --id, not both. Tag matching is EXACT: the full pattern must equal a tag.');
8692
8720
  process.exit(1);
8693
8721
  }
8694
8722
  const reason = flags['reason'] || null;
@@ -8747,7 +8775,7 @@ async function main(command, args, flags, hippoRoot) {
8747
8775
  cmdGraph(hippoRoot, args, flags);
8748
8776
  break;
8749
8777
  default:
8750
- console.error(`Unknown command: ${command}`);
8778
+ printError(`Unknown command: ${command}`);
8751
8779
  printUsage();
8752
8780
  process.exit(1);
8753
8781
  }
@@ -8758,7 +8786,7 @@ export async function runCli(argv = process.argv) {
8758
8786
  await main(command, args, flags, getHippoRoot(process.cwd()));
8759
8787
  }
8760
8788
  catch (err) {
8761
- console.error('Error:', err instanceof Error ? err.message : err);
8789
+ printError('Error:', err instanceof Error ? err.message : err);
8762
8790
  process.exit(1);
8763
8791
  }
8764
8792
  }