hippo-memory 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +862 -861
  2. package/dist/audit.d.ts +1 -1
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +1244 -3
  7. package/dist/cli.js.map +1 -1
  8. package/dist/customer-notes.d.ts +95 -0
  9. package/dist/customer-notes.d.ts.map +1 -0
  10. package/dist/customer-notes.js +296 -0
  11. package/dist/customer-notes.js.map +1 -0
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +731 -1
  14. package/dist/db.js.map +1 -1
  15. package/dist/graph-extract.d.ts +55 -0
  16. package/dist/graph-extract.d.ts.map +1 -0
  17. package/dist/graph-extract.js +259 -0
  18. package/dist/graph-extract.js.map +1 -0
  19. package/dist/graph-recall.d.ts +41 -0
  20. package/dist/graph-recall.d.ts.map +1 -0
  21. package/dist/graph-recall.js +246 -0
  22. package/dist/graph-recall.js.map +1 -0
  23. package/dist/graph.d.ts +137 -0
  24. package/dist/graph.d.ts.map +1 -0
  25. package/dist/graph.js +433 -0
  26. package/dist/graph.js.map +1 -0
  27. package/dist/incidents.d.ts +100 -0
  28. package/dist/incidents.d.ts.map +1 -0
  29. package/dist/incidents.js +322 -0
  30. package/dist/incidents.js.map +1 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/memory.d.ts +6 -0
  36. package/dist/memory.d.ts.map +1 -1
  37. package/dist/memory.js +6 -0
  38. package/dist/memory.js.map +1 -1
  39. package/dist/policies.d.ts +149 -0
  40. package/dist/policies.d.ts.map +1 -0
  41. package/dist/policies.js +380 -0
  42. package/dist/policies.js.map +1 -0
  43. package/dist/processes.d.ts +104 -0
  44. package/dist/processes.d.ts.map +1 -0
  45. package/dist/processes.js +330 -0
  46. package/dist/processes.js.map +1 -0
  47. package/dist/project-briefs.d.ts +126 -0
  48. package/dist/project-briefs.d.ts.map +1 -0
  49. package/dist/project-briefs.js +453 -0
  50. package/dist/project-briefs.js.map +1 -0
  51. package/dist/search.d.ts +7 -0
  52. package/dist/search.d.ts.map +1 -1
  53. package/dist/search.js.map +1 -1
  54. package/dist/server.d.ts.map +1 -1
  55. package/dist/server.js +1028 -16
  56. package/dist/server.js.map +1 -1
  57. package/dist/skills.d.ts +98 -0
  58. package/dist/skills.d.ts.map +1 -0
  59. package/dist/skills.js +339 -0
  60. package/dist/skills.js.map +1 -0
  61. package/dist/src/audit.js.map +1 -1
  62. package/dist/src/cli.js +1244 -3
  63. package/dist/src/cli.js.map +1 -1
  64. package/dist/src/customer-notes.js +296 -0
  65. package/dist/src/customer-notes.js.map +1 -0
  66. package/dist/src/db.js +731 -1
  67. package/dist/src/db.js.map +1 -1
  68. package/dist/src/graph-extract.js +259 -0
  69. package/dist/src/graph-extract.js.map +1 -0
  70. package/dist/src/graph-recall.js +246 -0
  71. package/dist/src/graph-recall.js.map +1 -0
  72. package/dist/src/graph.js +433 -0
  73. package/dist/src/graph.js.map +1 -0
  74. package/dist/src/incidents.js +322 -0
  75. package/dist/src/incidents.js.map +1 -0
  76. package/dist/src/index.js +1 -0
  77. package/dist/src/index.js.map +1 -1
  78. package/dist/src/memory.js +6 -0
  79. package/dist/src/memory.js.map +1 -1
  80. package/dist/src/policies.js +380 -0
  81. package/dist/src/policies.js.map +1 -0
  82. package/dist/src/processes.js +330 -0
  83. package/dist/src/processes.js.map +1 -0
  84. package/dist/src/project-briefs.js +453 -0
  85. package/dist/src/project-briefs.js.map +1 -0
  86. package/dist/src/search.js.map +1 -1
  87. package/dist/src/server.js +1028 -16
  88. package/dist/src/server.js.map +1 -1
  89. package/dist/src/skills.js +339 -0
  90. package/dist/src/skills.js.map +1 -0
  91. package/dist/src/version.js +1 -1
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.js +1 -1
  94. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  95. package/extensions/openclaw-plugin/package.json +1 -1
  96. package/openclaw.plugin.json +1 -1
  97. package/package.json +2 -2
package/dist/src/cli.js CHANGED
@@ -59,6 +59,13 @@ import * as api from './api.js';
59
59
  import * as predictionsModule from './predictions.js';
60
60
  import { computePlanningFallacyOutput } from './predictions.js';
61
61
  import * as decisionsModule from './decisions.js';
62
+ import * as incidentsModule from './incidents.js';
63
+ import * as processesModule from './processes.js';
64
+ import * as policiesModule from './policies.js';
65
+ import * as skillsModule from './skills.js';
66
+ import * as briefsModule from './project-briefs.js';
67
+ import * as customerNotesModule from './customer-notes.js';
68
+ import { extractGraph } from './graph-extract.js';
62
69
  import { createHash } from 'node:crypto';
63
70
  import { detectAnchoring, hashQueryText, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from './recall-history.js';
64
71
  import { detectAvailabilityBias } from './availability.js';
@@ -92,6 +99,7 @@ import { runFeatureEval, formatResult, resultToBaseline, detectRegressions } fro
92
99
  import { refineStore } from './refine-llm.js';
93
100
  import { wmPush, wmRead, wmClear, wmFlush } from './working-memory.js';
94
101
  import { multihopSearch } from './multihop.js';
102
+ import { graphExpandRecall, MAX_HOPS, DEFAULT_MAX_NEIGHBORS } from './graph-recall.js';
95
103
  import { getReranker } from './rerankers/index.js';
96
104
  import { computeSalience } from './salience.js';
97
105
  import { computeAmbientState, renderAmbientSummary } from './ambient.js';
@@ -219,8 +227,8 @@ function parseArgs(argv) {
219
227
  i++;
220
228
  }
221
229
  else {
222
- // Check if it's a repeatable flag (tag, artifact)
223
- if (key === 'tag' || key === 'artifact') {
230
+ // Check if it's a repeatable flag (tag, artifact, link, step)
231
+ if (key === 'tag' || key === 'artifact' || key === 'link' || key === 'step') {
224
232
  if (Array.isArray(flags[key])) {
225
233
  flags[key].push(next);
226
234
  }
@@ -806,6 +814,51 @@ async function cmdRecall(hippoRoot, query, flags) {
806
814
  includeSuperseded, asOf,
807
815
  });
808
816
  }
817
+ // E3.2 multi-hop graph recall. After the base branch produces `results`, optionally
818
+ // augment with memories reached by walking the entities/relations graph `--hops N` out
819
+ // from the lexical seeds. Runs BEFORE the opt-in re-rankers below so graph-reached
820
+ // results are first-class candidates in any downstream re-ranking / --why. Default OFF
821
+ // (absent or 0 = no-op). Reached memories are loaded directly by id (NOT via the
822
+ // lexical candidate set, which would exclude the orthogonal neighbours graph recall
823
+ // exists to surface); the engine re-applies the same superseded/asOf hard filters.
824
+ if (flags['hops'] !== undefined) {
825
+ // Reject a value-less `--hops` (parseArgs stores boolean true): Number(true) === 1
826
+ // would otherwise silently run a 1-hop expansion when the user fat-fingered the value.
827
+ if (typeof flags['hops'] === 'boolean') {
828
+ console.error(`--hops requires an integer value 0..${MAX_HOPS} (e.g. --hops 1).`);
829
+ process.exit(1);
830
+ }
831
+ const hops = Number(flags['hops']);
832
+ if (!Number.isInteger(hops) || hops < 0 || hops > MAX_HOPS) {
833
+ console.error(`Invalid --hops: "${String(flags['hops'])}". Must be an integer 0..${MAX_HOPS}.`);
834
+ process.exit(1);
835
+ }
836
+ let maxNeighbors = DEFAULT_MAX_NEIGHBORS;
837
+ if (flags['max-neighbors'] !== undefined) {
838
+ if (typeof flags['max-neighbors'] === 'boolean') {
839
+ console.error(`--max-neighbors requires an integer value 1..200.`);
840
+ process.exit(1);
841
+ }
842
+ maxNeighbors = Number(flags['max-neighbors']);
843
+ if (!Number.isInteger(maxNeighbors) || maxNeighbors < 1 || maxNeighbors > 200) {
844
+ console.error(`Invalid --max-neighbors: "${String(flags['max-neighbors'])}". Must be an integer 1..200.`);
845
+ process.exit(1);
846
+ }
847
+ }
848
+ if (hops > 0) {
849
+ results = graphExpandRecall(results, {
850
+ hops,
851
+ maxNeighbors,
852
+ hippoRoot,
853
+ globalRoot: isInitialized(globalRoot) && globalRoot !== hippoRoot ? globalRoot : undefined,
854
+ tenantId,
855
+ includeSuperseded,
856
+ asOf,
857
+ budget,
858
+ minResults: minResults ?? 1,
859
+ });
860
+ }
861
+ }
809
862
  // ACC EVC-adaptive recall (RESEARCH.md §PFC.ACC). When the initial top-K is
810
863
  // dominated by lexically similar but distinct memories (high pairwise token
811
864
  // overlap = same topic, different facts = conflict), allocate extra retrieval
@@ -1323,6 +1376,9 @@ async function cmdRecall(hippoRoot, query, flags) {
1323
1376
  base.superseded = true;
1324
1377
  base.superseded_by = r.entry.superseded_by;
1325
1378
  }
1379
+ if (r.graphVia) {
1380
+ base.graphVia = r.graphVia;
1381
+ }
1326
1382
  if (showWhy) {
1327
1383
  const explanation = explainMatch(query, r);
1328
1384
  base.confidence = resolveConfidence(r.entry);
@@ -1436,8 +1492,9 @@ async function cmdRecall(hippoRoot, query, flags) {
1436
1492
  const isGlobal = isInitialized(globalRoot) && !localIndex.entries[e.id];
1437
1493
  const globalMark = isGlobal ? ' [global]' : '';
1438
1494
  const supersededMark = e.superseded_by ? ' [superseded]' : '';
1495
+ const graphMark = r.graphVia ? ` [graph: ${r.graphVia.hops}hop ${r.graphVia.relType}]` : '';
1439
1496
  const sourceMark = isGlobal ? ' [global]' : ' [local]';
1440
- console.log(`--- ${e.id} [${e.layer}] ${confLabel}${globalMark}${supersededMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`);
1497
+ console.log(`--- ${e.id} [${e.layer}] ${confLabel}${globalMark}${supersededMark}${graphMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`);
1441
1498
  console.log(` [${strengthBar}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`);
1442
1499
  if (showWhy) {
1443
1500
  const explanation = explainMatch(query, r);
@@ -3463,6 +3520,1062 @@ function cmdDecide(hippoRoot, args, flags) {
3463
3520
  console.log(` supersedes memory: ${supersedesMemId}${tail}`);
3464
3521
  }
3465
3522
  }
3523
+ // Strict positive-integer parse for incident id args. parseInt() alone accepts
3524
+ // trailing junk ("1abc" -> 1), which would let a mutating subcommand (close/
3525
+ // resolve) silently hit the wrong row; require the whole arg to be digits.
3526
+ // (codex P2, 2026-05-29.)
3527
+ function parsePositiveIncidentId(idRaw) {
3528
+ const s = String(idRaw ?? '').trim();
3529
+ const id = parseInt(s, 10);
3530
+ if (!/^\d+$/.test(s) || id <= 0) {
3531
+ console.error(`Invalid incident id: "${idRaw}" (expected a positive integer).`);
3532
+ process.exit(1);
3533
+ }
3534
+ return id;
3535
+ }
3536
+ function cmdIncident(hippoRoot, args, flags) {
3537
+ requireInit(hippoRoot);
3538
+ const tenantId = resolveTenantId({});
3539
+ const subcommand = args[0] ?? '';
3540
+ if (subcommand === 'list') {
3541
+ const statusRaw = flags['status'];
3542
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
3543
+ const limitRaw = flags['limit'];
3544
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
3545
+ if (!Number.isFinite(limit) || limit <= 0) {
3546
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3547
+ process.exit(1);
3548
+ }
3549
+ let results;
3550
+ if (status === 'all') {
3551
+ results = incidentsModule.loadIncidents(hippoRoot, tenantId, { limit });
3552
+ }
3553
+ else {
3554
+ if (!incidentsModule.VALID_INCIDENT_STATES.has(status)) {
3555
+ console.error(`Invalid --status: "${status}". Must be one of: open | resolved | closed | all.`);
3556
+ process.exit(1);
3557
+ }
3558
+ results = incidentsModule.loadIncidents(hippoRoot, tenantId, {
3559
+ status: status,
3560
+ limit,
3561
+ });
3562
+ }
3563
+ if (results.length === 0) {
3564
+ console.log('No incidents.');
3565
+ return;
3566
+ }
3567
+ console.log(`Found ${results.length} incidents:\n`);
3568
+ for (const inc of results) {
3569
+ const linkPart = inc.linkedMemoryIds.length > 0 ? ` links=${inc.linkedMemoryIds.length}` : '';
3570
+ console.log(`#${inc.id} [${inc.status}]${linkPart} memory=${inc.memoryId ?? '-'}`);
3571
+ console.log(` ${inc.incidentText}`);
3572
+ if (inc.context)
3573
+ console.log(` context: ${inc.context}`);
3574
+ }
3575
+ return;
3576
+ }
3577
+ if (subcommand === 'get') {
3578
+ const idRaw = args[1];
3579
+ if (!idRaw) {
3580
+ console.error('Usage: hippo incident get <id>');
3581
+ process.exit(1);
3582
+ }
3583
+ const id = parsePositiveIncidentId(idRaw);
3584
+ const incident = incidentsModule.loadIncidentById(hippoRoot, tenantId, id);
3585
+ if (!incident) {
3586
+ console.error(`Incident ${id} not found.`);
3587
+ process.exit(1);
3588
+ }
3589
+ console.log(`Incident #${incident.id}`);
3590
+ console.log(` status: ${incident.status}`);
3591
+ console.log(` text: ${incident.incidentText}`);
3592
+ if (incident.context)
3593
+ console.log(` context: ${incident.context}`);
3594
+ if (incident.resolutionText)
3595
+ console.log(` resolution: ${incident.resolutionText}`);
3596
+ if (incident.resolvedAt)
3597
+ console.log(` resolved_at: ${incident.resolvedAt}`);
3598
+ if (incident.closedAt)
3599
+ console.log(` closed_at: ${incident.closedAt}`);
3600
+ if (incident.linkedMemoryIds.length > 0) {
3601
+ console.log(` linked memories: ${incident.linkedMemoryIds.join(', ')}`);
3602
+ }
3603
+ if (incident.memoryId)
3604
+ console.log(` memory: ${incident.memoryId}`);
3605
+ console.log(` created: ${incident.createdAt}`);
3606
+ return;
3607
+ }
3608
+ if (subcommand === 'resolve') {
3609
+ const idRaw = args[1];
3610
+ if (!idRaw) {
3611
+ console.error('Usage: hippo incident resolve <id> --resolution "<text>"');
3612
+ process.exit(1);
3613
+ }
3614
+ const id = parsePositiveIncidentId(idRaw);
3615
+ const resolutionRaw = flags['resolution'];
3616
+ if (typeof resolutionRaw !== 'string' || !resolutionRaw.trim()) {
3617
+ console.error('--resolution requires a non-empty value, e.g. hippo incident resolve <id> --resolution "root cause fixed".');
3618
+ process.exit(1);
3619
+ }
3620
+ const resolved = incidentsModule.resolveIncident(hippoRoot, tenantId, id, resolutionRaw);
3621
+ console.log(`Incident #${resolved.id} resolved.`);
3622
+ return;
3623
+ }
3624
+ if (subcommand === 'close') {
3625
+ const idRaw = args[1];
3626
+ if (!idRaw) {
3627
+ console.error('Usage: hippo incident close <id>');
3628
+ process.exit(1);
3629
+ }
3630
+ const id = parsePositiveIncidentId(idRaw);
3631
+ const closed = incidentsModule.closeIncident(hippoRoot, tenantId, id);
3632
+ console.log(`Incident #${closed.id} closed.`);
3633
+ return;
3634
+ }
3635
+ // Default subcommand: open (create). Accept both the documented
3636
+ // `incident open "<text>"` form and the bare `incident "<text>"` form: for the
3637
+ // `open` keyword the text is args[1], otherwise args[0] IS the text.
3638
+ const incidentText = subcommand === 'open' ? (args[1] ?? '') : subcommand;
3639
+ if (!incidentText) {
3640
+ console.error('Usage: hippo incident "<incident>" [--context "<details>"] [--link <memory-id>]...');
3641
+ console.error(' hippo incident list [--status open|resolved|closed|all] [--limit N]');
3642
+ console.error(' hippo incident get <id>');
3643
+ console.error(' hippo incident resolve <id> --resolution "<text>"');
3644
+ console.error(' hippo incident close <id>');
3645
+ process.exit(1);
3646
+ }
3647
+ const contextRaw = flags['context'];
3648
+ const context = typeof contextRaw === 'string' && contextRaw ? contextRaw : undefined;
3649
+ // --link is a repeatable flag (collected into an array by parseArgs). A
3650
+ // single --link <id> yields a string; normalize both to string[].
3651
+ const linkRaw = flags['link'];
3652
+ let linkedMemoryIds;
3653
+ if (Array.isArray(linkRaw)) {
3654
+ linkedMemoryIds = linkRaw;
3655
+ }
3656
+ else if (typeof linkRaw === 'string') {
3657
+ linkedMemoryIds = [linkRaw];
3658
+ }
3659
+ else if (linkRaw === true) {
3660
+ console.error('--link requires a memory id, e.g. hippo incident "<text>" --link mem_abc123.');
3661
+ process.exit(1);
3662
+ }
3663
+ const incidentPathTags = extractPathTags(process.cwd());
3664
+ const created = incidentsModule.saveIncident(hippoRoot, tenantId, {
3665
+ incidentText,
3666
+ context,
3667
+ linkedMemoryIds,
3668
+ extraTags: incidentPathTags,
3669
+ });
3670
+ console.log(`Incident recorded: #${created.id}`);
3671
+ if (created.memoryId)
3672
+ console.log(` memory: ${created.memoryId}`);
3673
+ if (created.linkedMemoryIds.length > 0) {
3674
+ console.log(` linked memories: ${created.linkedMemoryIds.join(', ')}`);
3675
+ }
3676
+ }
3677
+ // Strict positive-integer id parse for the mutating process subcommands.
3678
+ // parseInt alone accepts trailing junk ('1abc' -> 1), which would let
3679
+ // `process close 1abc` / `supersede 1abc` silently hit the wrong row; require
3680
+ // the whole arg to be digits. (Mirrors parsePositiveIncidentId; codex P2,
3681
+ // 2026-05-29.)
3682
+ function parsePositiveProcessId(idRaw) {
3683
+ const s = String(idRaw ?? '').trim();
3684
+ const id = parseInt(s, 10);
3685
+ if (!/^\d+$/.test(s) || id <= 0) {
3686
+ console.error(`Invalid process id: "${idRaw}" (expected a positive integer).`);
3687
+ process.exit(1);
3688
+ }
3689
+ return id;
3690
+ }
3691
+ // --step is a repeatable flag (collected into an array by parseArgs). A single
3692
+ // --step yields a string; normalize both to string[]. A value-less --step errors.
3693
+ function collectProcessSteps(stepRaw) {
3694
+ if (Array.isArray(stepRaw))
3695
+ return stepRaw;
3696
+ if (typeof stepRaw === 'string')
3697
+ return [stepRaw];
3698
+ if (stepRaw === true) {
3699
+ console.error('--step requires a value, e.g. hippo process new "<name>" --step "do X".');
3700
+ process.exit(1);
3701
+ }
3702
+ return [];
3703
+ }
3704
+ function cmdProcess(hippoRoot, args, flags) {
3705
+ requireInit(hippoRoot);
3706
+ const tenantId = resolveTenantId({});
3707
+ const subcommand = args[0] ?? '';
3708
+ if (subcommand === 'list') {
3709
+ const statusRaw = flags['status'];
3710
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
3711
+ const limitRaw = flags['limit'];
3712
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
3713
+ if (!Number.isFinite(limit) || limit <= 0) {
3714
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3715
+ process.exit(1);
3716
+ }
3717
+ let results;
3718
+ if (status === 'all') {
3719
+ results = processesModule.loadProcesses(hippoRoot, tenantId, { limit });
3720
+ }
3721
+ else {
3722
+ if (!processesModule.VALID_PROCESS_STATES.has(status)) {
3723
+ console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
3724
+ process.exit(1);
3725
+ }
3726
+ results = processesModule.loadProcesses(hippoRoot, tenantId, {
3727
+ status: status,
3728
+ limit,
3729
+ });
3730
+ }
3731
+ if (results.length === 0) {
3732
+ console.log('No processes.');
3733
+ return;
3734
+ }
3735
+ console.log(`Found ${results.length} processes:\n`);
3736
+ for (const proc of results) {
3737
+ console.log(`#${proc.id} [${proc.status}] v${proc.version} steps=${proc.steps.length} memory=${proc.memoryId ?? '-'}`);
3738
+ console.log(` ${proc.processName}`);
3739
+ if (proc.changeSummary)
3740
+ console.log(` change: ${proc.changeSummary}`);
3741
+ }
3742
+ return;
3743
+ }
3744
+ if (subcommand === 'get') {
3745
+ const idRaw = args[1];
3746
+ if (!idRaw) {
3747
+ console.error('Usage: hippo process get <id>');
3748
+ process.exit(1);
3749
+ }
3750
+ const id = parsePositiveProcessId(idRaw);
3751
+ const proc = processesModule.loadProcessById(hippoRoot, tenantId, id);
3752
+ if (!proc) {
3753
+ console.error(`Process ${id} not found.`);
3754
+ process.exit(1);
3755
+ }
3756
+ console.log(`Process #${proc.id}`);
3757
+ console.log(` name: ${proc.processName}`);
3758
+ console.log(` status: ${proc.status}`);
3759
+ console.log(` version: ${proc.version}`);
3760
+ if (proc.description)
3761
+ console.log(` description: ${proc.description}`);
3762
+ if (proc.steps.length > 0) {
3763
+ console.log(` steps:`);
3764
+ proc.steps.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
3765
+ }
3766
+ if (proc.changeSummary)
3767
+ console.log(` change_summary: ${proc.changeSummary}`);
3768
+ if (proc.supersededBy !== null)
3769
+ console.log(` superseded_by: #${proc.supersededBy}`);
3770
+ if (proc.supersededAt)
3771
+ console.log(` superseded_at: ${proc.supersededAt}`);
3772
+ if (proc.closedAt)
3773
+ console.log(` closed_at: ${proc.closedAt}`);
3774
+ if (proc.memoryId)
3775
+ console.log(` memory: ${proc.memoryId}`);
3776
+ console.log(` created: ${proc.createdAt}`);
3777
+ return;
3778
+ }
3779
+ if (subcommand === 'supersede') {
3780
+ const idRaw = args[1];
3781
+ if (!idRaw) {
3782
+ console.error('Usage: hippo process supersede <id> --step "<text>" [--step ...] [--change "<summary>"] [--description "<text>"]');
3783
+ process.exit(1);
3784
+ }
3785
+ const id = parsePositiveProcessId(idRaw);
3786
+ const steps = collectProcessSteps(flags['step']);
3787
+ if (steps.length === 0) {
3788
+ console.error('hippo process supersede requires at least one --step "<text>" for the new version.');
3789
+ process.exit(1);
3790
+ }
3791
+ // A supersession is a new version of the SAME process, so the new row reuses
3792
+ // the predecessor's name (stable identity across versions). loadProcessById
3793
+ // gives an early not-found before the write; saveProcess's in-SAVEPOINT
3794
+ // preflight is the authoritative active-state check.
3795
+ const existing = processesModule.loadProcessById(hippoRoot, tenantId, id);
3796
+ if (!existing) {
3797
+ console.error(`Process ${id} not found.`);
3798
+ process.exit(1);
3799
+ }
3800
+ const changeRaw = flags['change'];
3801
+ const changeSummary = typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined;
3802
+ const descRaw = flags['description'];
3803
+ const description = typeof descRaw === 'string' && descRaw ? descRaw : undefined;
3804
+ const procPathTags = extractPathTags(process.cwd());
3805
+ const created = processesModule.saveProcess(hippoRoot, tenantId, {
3806
+ processName: existing.processName,
3807
+ steps,
3808
+ description,
3809
+ changeSummary,
3810
+ supersedesProcessId: id,
3811
+ extraTags: procPathTags,
3812
+ });
3813
+ console.log(`Process #${created.id} recorded (v${created.version}), superseding #${id}.`);
3814
+ if (created.memoryId)
3815
+ console.log(` memory: ${created.memoryId}`);
3816
+ return;
3817
+ }
3818
+ if (subcommand === 'close') {
3819
+ const idRaw = args[1];
3820
+ if (!idRaw) {
3821
+ console.error('Usage: hippo process close <id>');
3822
+ process.exit(1);
3823
+ }
3824
+ const id = parsePositiveProcessId(idRaw);
3825
+ const closed = processesModule.closeProcess(hippoRoot, tenantId, id);
3826
+ console.log(`Process #${closed.id} closed.`);
3827
+ return;
3828
+ }
3829
+ // Default subcommand: new (create). Accept both the documented
3830
+ // `process new "<name>"` form and the bare `process "<name>"` form: for the
3831
+ // `new` keyword the name is args[1], otherwise args[0] IS the name.
3832
+ const processName = subcommand === 'new' ? (args[1] ?? '') : subcommand;
3833
+ if (!processName) {
3834
+ console.error('Usage: hippo process new "<name>" --step "<text>" [--step ...] [--description "<text>"]');
3835
+ console.error(' hippo process list [--status active|superseded|closed|all] [--limit N]');
3836
+ console.error(' hippo process get <id>');
3837
+ console.error(' hippo process supersede <id> --step "<text>" [--change "<summary>"]');
3838
+ console.error(' hippo process close <id>');
3839
+ process.exit(1);
3840
+ }
3841
+ const steps = collectProcessSteps(flags['step']);
3842
+ const descRaw = flags['description'];
3843
+ const description = typeof descRaw === 'string' && descRaw ? descRaw : undefined;
3844
+ const procPathTags = extractPathTags(process.cwd());
3845
+ const created = processesModule.saveProcess(hippoRoot, tenantId, {
3846
+ processName,
3847
+ steps,
3848
+ description,
3849
+ extraTags: procPathTags,
3850
+ });
3851
+ console.log(`Process recorded: #${created.id} (v${created.version}, ${created.steps.length} steps)`);
3852
+ if (created.memoryId)
3853
+ console.log(` memory: ${created.memoryId}`);
3854
+ }
3855
+ // Strict positive-integer id parse for the mutating policy subcommands (mirrors
3856
+ // parsePositiveProcessId; codex P2 class - parseInt alone accepts '1abc' -> 1).
3857
+ function parsePositivePolicyId(idRaw) {
3858
+ const s = String(idRaw ?? '').trim();
3859
+ const id = parseInt(s, 10);
3860
+ if (!/^\d+$/.test(s) || id <= 0) {
3861
+ console.error(`Invalid policy id: "${idRaw}" (expected a positive integer).`);
3862
+ process.exit(1);
3863
+ }
3864
+ return id;
3865
+ }
3866
+ function printPolicyRow(p) {
3867
+ const range = p.validTo ? `${p.validFrom}..${p.validTo}` : `${p.validFrom}..(open)`;
3868
+ console.log(`#${p.id} [${p.status}] v${p.version} ${range} memory=${p.memoryId ?? '-'}`);
3869
+ console.log(` ${p.policyName}: ${p.policyText}`);
3870
+ if (p.changeSummary)
3871
+ console.log(` change: ${p.changeSummary}`);
3872
+ }
3873
+ function cmdPolicy(hippoRoot, args, flags) {
3874
+ requireInit(hippoRoot);
3875
+ const tenantId = resolveTenantId({});
3876
+ const subcommand = args[0] ?? '';
3877
+ if (subcommand === 'list') {
3878
+ const statusRaw = flags['status'];
3879
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
3880
+ const limitRaw = flags['limit'];
3881
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
3882
+ if (!Number.isFinite(limit) || limit <= 0) {
3883
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3884
+ process.exit(1);
3885
+ }
3886
+ let results;
3887
+ if (status === 'all') {
3888
+ results = policiesModule.loadPolicies(hippoRoot, tenantId, { limit });
3889
+ }
3890
+ else {
3891
+ if (!policiesModule.VALID_POLICY_STATES.has(status)) {
3892
+ console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
3893
+ process.exit(1);
3894
+ }
3895
+ results = policiesModule.loadPolicies(hippoRoot, tenantId, {
3896
+ status: status,
3897
+ limit,
3898
+ });
3899
+ }
3900
+ if (results.length === 0) {
3901
+ console.log('No policies.');
3902
+ return;
3903
+ }
3904
+ console.log(`Found ${results.length} policies:\n`);
3905
+ for (const p of results)
3906
+ printPolicyRow(p);
3907
+ return;
3908
+ }
3909
+ if (subcommand === 'asof') {
3910
+ const dateRaw = args[1];
3911
+ if (!dateRaw) {
3912
+ console.error('Usage: hippo policy asof <iso-date> [--name "<policy>"]');
3913
+ process.exit(1);
3914
+ }
3915
+ const nameRaw = flags['name'];
3916
+ const name = typeof nameRaw === 'string' && nameRaw ? nameRaw : undefined;
3917
+ let results;
3918
+ try {
3919
+ results = policiesModule.loadPoliciesAsOf(hippoRoot, tenantId, dateRaw, { name });
3920
+ }
3921
+ catch (e) {
3922
+ console.error(e.message);
3923
+ process.exit(1);
3924
+ }
3925
+ if (results.length === 0) {
3926
+ console.log(`No active policies in force at ${dateRaw}${name ? ` for "${name}"` : ''}.`);
3927
+ return;
3928
+ }
3929
+ console.log(`Policies in force at ${dateRaw}:\n`);
3930
+ for (const p of results)
3931
+ printPolicyRow(p);
3932
+ return;
3933
+ }
3934
+ if (subcommand === 'get') {
3935
+ const idRaw = args[1];
3936
+ if (!idRaw) {
3937
+ console.error('Usage: hippo policy get <id>');
3938
+ process.exit(1);
3939
+ }
3940
+ const id = parsePositivePolicyId(idRaw);
3941
+ const p = policiesModule.loadPolicyById(hippoRoot, tenantId, id);
3942
+ if (!p) {
3943
+ console.error(`Policy ${id} not found.`);
3944
+ process.exit(1);
3945
+ }
3946
+ console.log(`Policy #${p.id}`);
3947
+ console.log(` name: ${p.policyName}`);
3948
+ console.log(` text: ${p.policyText}`);
3949
+ console.log(` status: ${p.status}`);
3950
+ console.log(` version: ${p.version}`);
3951
+ console.log(` valid_from: ${p.validFrom}`);
3952
+ console.log(` valid_to: ${p.validTo ?? '(open-ended)'}`);
3953
+ if (p.changeSummary)
3954
+ console.log(` change_summary: ${p.changeSummary}`);
3955
+ if (p.supersededBy !== null)
3956
+ console.log(` superseded_by: #${p.supersededBy}`);
3957
+ if (p.supersededAt)
3958
+ console.log(` superseded_at: ${p.supersededAt}`);
3959
+ if (p.closedAt)
3960
+ console.log(` closed_at: ${p.closedAt}`);
3961
+ if (p.memoryId)
3962
+ console.log(` memory: ${p.memoryId}`);
3963
+ console.log(` created: ${p.createdAt}`);
3964
+ return;
3965
+ }
3966
+ if (subcommand === 'supersede') {
3967
+ const idRaw = args[1];
3968
+ if (!idRaw) {
3969
+ console.error('Usage: hippo policy supersede <id> --text "<rule>" [--from <iso>] [--to <iso>] [--change "<summary>"]');
3970
+ process.exit(1);
3971
+ }
3972
+ const id = parsePositivePolicyId(idRaw);
3973
+ const textRaw = flags['text'];
3974
+ if (typeof textRaw !== 'string' || !textRaw.trim()) {
3975
+ console.error('hippo policy supersede requires --text "<rule>" for the new version.');
3976
+ process.exit(1);
3977
+ }
3978
+ const existing = policiesModule.loadPolicyById(hippoRoot, tenantId, id);
3979
+ if (!existing) {
3980
+ console.error(`Policy ${id} not found.`);
3981
+ process.exit(1);
3982
+ }
3983
+ const fromRaw = flags['from'];
3984
+ const toRaw = flags['to'];
3985
+ const changeRaw = flags['change'];
3986
+ try {
3987
+ const created = policiesModule.savePolicy(hippoRoot, tenantId, {
3988
+ policyName: existing.policyName,
3989
+ policyText: textRaw,
3990
+ validFrom: typeof fromRaw === 'string' && fromRaw ? fromRaw : undefined,
3991
+ validTo: typeof toRaw === 'string' && toRaw ? toRaw : undefined,
3992
+ changeSummary: typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined,
3993
+ supersedesPolicyId: id,
3994
+ extraTags: extractPathTags(process.cwd()),
3995
+ });
3996
+ console.log(`Policy #${created.id} recorded (v${created.version}), superseding #${id}.`);
3997
+ if (created.memoryId)
3998
+ console.log(` memory: ${created.memoryId}`);
3999
+ }
4000
+ catch (e) {
4001
+ console.error(e.message);
4002
+ process.exit(1);
4003
+ }
4004
+ return;
4005
+ }
4006
+ if (subcommand === 'close') {
4007
+ const idRaw = args[1];
4008
+ if (!idRaw) {
4009
+ console.error('Usage: hippo policy close <id>');
4010
+ process.exit(1);
4011
+ }
4012
+ const id = parsePositivePolicyId(idRaw);
4013
+ const closed = policiesModule.closePolicy(hippoRoot, tenantId, id);
4014
+ console.log(`Policy #${closed.id} closed.`);
4015
+ return;
4016
+ }
4017
+ // Default subcommand: new (create). Accept both `policy new "<name>"` and the
4018
+ // bare `policy "<name>"` form: for the `new` keyword the name is args[1].
4019
+ const policyName = subcommand === 'new' ? (args[1] ?? '') : subcommand;
4020
+ const textRaw = flags['text'];
4021
+ if (!policyName || typeof textRaw !== 'string' || !textRaw.trim()) {
4022
+ console.error('Usage: hippo policy new "<name>" --text "<rule>" [--from <iso>] [--to <iso>]');
4023
+ console.error(' hippo policy list [--status active|superseded|closed|all] [--limit N]');
4024
+ console.error(' hippo policy get <id>');
4025
+ console.error(' hippo policy asof <iso-date> [--name "<policy>"]');
4026
+ console.error(' hippo policy supersede <id> --text "<rule>" [--from] [--to] [--change "<summary>"]');
4027
+ console.error(' hippo policy close <id>');
4028
+ process.exit(1);
4029
+ }
4030
+ const fromRaw = flags['from'];
4031
+ const toRaw = flags['to'];
4032
+ try {
4033
+ const created = policiesModule.savePolicy(hippoRoot, tenantId, {
4034
+ policyName,
4035
+ policyText: textRaw,
4036
+ validFrom: typeof fromRaw === 'string' && fromRaw ? fromRaw : undefined,
4037
+ validTo: typeof toRaw === 'string' && toRaw ? toRaw : undefined,
4038
+ extraTags: extractPathTags(process.cwd()),
4039
+ });
4040
+ const range = created.validTo ? `${created.validFrom}..${created.validTo}` : `${created.validFrom}..(open)`;
4041
+ console.log(`Policy recorded: #${created.id} (v${created.version}, effective ${range})`);
4042
+ if (created.memoryId)
4043
+ console.log(` memory: ${created.memoryId}`);
4044
+ }
4045
+ catch (e) {
4046
+ console.error(e.message);
4047
+ process.exit(1);
4048
+ }
4049
+ }
4050
+ // Strict positive-integer id parse for the mutating skill subcommands (mirrors
4051
+ // parsePositivePolicyId; codex P2 class - parseInt accepts '1abc' -> 1).
4052
+ function parsePositiveSkillId(idRaw) {
4053
+ const s = String(idRaw ?? '').trim();
4054
+ const id = parseInt(s, 10);
4055
+ if (!/^\d+$/.test(s) || id <= 0) {
4056
+ console.error(`Invalid skill id: "${idRaw}" (expected a positive integer).`);
4057
+ process.exit(1);
4058
+ }
4059
+ return id;
4060
+ }
4061
+ function printSkillRow(s) {
4062
+ const trig = s.trigger ? ` when="${s.trigger}"` : '';
4063
+ console.log(`#${s.id} [${s.status}] v${s.version}${trig} memory=${s.memoryId ?? '-'}`);
4064
+ console.log(` ${s.skillName}`);
4065
+ if (s.changeSummary)
4066
+ console.log(` change: ${s.changeSummary}`);
4067
+ }
4068
+ function cmdSkill(hippoRoot, args, flags) {
4069
+ requireInit(hippoRoot);
4070
+ const tenantId = resolveTenantId({});
4071
+ const subcommand = args[0] ?? '';
4072
+ if (subcommand === 'list') {
4073
+ const statusRaw = flags['status'];
4074
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
4075
+ const limitRaw = flags['limit'];
4076
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
4077
+ if (!Number.isFinite(limit) || limit <= 0) {
4078
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4079
+ process.exit(1);
4080
+ }
4081
+ let results;
4082
+ if (status === 'all') {
4083
+ results = skillsModule.loadSkills(hippoRoot, tenantId, { limit });
4084
+ }
4085
+ else {
4086
+ if (!skillsModule.VALID_SKILL_STATES.has(status)) {
4087
+ console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4088
+ process.exit(1);
4089
+ }
4090
+ results = skillsModule.loadSkills(hippoRoot, tenantId, {
4091
+ status: status,
4092
+ limit,
4093
+ });
4094
+ }
4095
+ if (results.length === 0) {
4096
+ console.log('No skills.');
4097
+ return;
4098
+ }
4099
+ console.log(`Found ${results.length} skills:\n`);
4100
+ for (const s of results)
4101
+ printSkillRow(s);
4102
+ return;
4103
+ }
4104
+ if (subcommand === 'export') {
4105
+ const md = skillsModule.exportSkills(hippoRoot, tenantId);
4106
+ if (!md) {
4107
+ console.log('No active skills.');
4108
+ return;
4109
+ }
4110
+ console.log(md);
4111
+ return;
4112
+ }
4113
+ if (subcommand === 'get') {
4114
+ const idRaw = args[1];
4115
+ if (!idRaw) {
4116
+ console.error('Usage: hippo skill get <id>');
4117
+ process.exit(1);
4118
+ }
4119
+ const id = parsePositiveSkillId(idRaw);
4120
+ const s = skillsModule.loadSkillById(hippoRoot, tenantId, id);
4121
+ if (!s) {
4122
+ console.error(`Skill ${id} not found.`);
4123
+ process.exit(1);
4124
+ }
4125
+ console.log(`Skill #${s.id}`);
4126
+ console.log(` name: ${s.skillName}`);
4127
+ console.log(` status: ${s.status}`);
4128
+ console.log(` version: ${s.version}`);
4129
+ if (s.trigger)
4130
+ console.log(` when: ${s.trigger}`);
4131
+ console.log(` instructions: ${s.instructions}`);
4132
+ if (s.changeSummary)
4133
+ console.log(` change_summary: ${s.changeSummary}`);
4134
+ if (s.supersededBy !== null)
4135
+ console.log(` superseded_by: #${s.supersededBy}`);
4136
+ if (s.supersededAt)
4137
+ console.log(` superseded_at: ${s.supersededAt}`);
4138
+ if (s.closedAt)
4139
+ console.log(` closed_at: ${s.closedAt}`);
4140
+ if (s.memoryId)
4141
+ console.log(` memory: ${s.memoryId}`);
4142
+ console.log(` created: ${s.createdAt}`);
4143
+ return;
4144
+ }
4145
+ if (subcommand === 'supersede') {
4146
+ const idRaw = args[1];
4147
+ if (!idRaw) {
4148
+ console.error('Usage: hippo skill supersede <id> --instructions "<text>" [--trigger "<when>"] [--change "<summary>"]');
4149
+ process.exit(1);
4150
+ }
4151
+ const id = parsePositiveSkillId(idRaw);
4152
+ const instrRaw = flags['instructions'];
4153
+ if (typeof instrRaw !== 'string' || !instrRaw.trim()) {
4154
+ console.error('hippo skill supersede requires --instructions "<text>" for the new version.');
4155
+ process.exit(1);
4156
+ }
4157
+ const existing = skillsModule.loadSkillById(hippoRoot, tenantId, id);
4158
+ if (!existing) {
4159
+ console.error(`Skill ${id} not found.`);
4160
+ process.exit(1);
4161
+ }
4162
+ const trigRaw = flags['trigger'];
4163
+ const changeRaw = flags['change'];
4164
+ try {
4165
+ const created = skillsModule.saveSkill(hippoRoot, tenantId, {
4166
+ skillName: existing.skillName,
4167
+ instructions: instrRaw,
4168
+ trigger: typeof trigRaw === 'string' && trigRaw ? trigRaw : undefined,
4169
+ changeSummary: typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined,
4170
+ supersedesSkillId: id,
4171
+ extraTags: extractPathTags(process.cwd()),
4172
+ });
4173
+ console.log(`Skill #${created.id} recorded (v${created.version}), superseding #${id}.`);
4174
+ if (created.memoryId)
4175
+ console.log(` memory: ${created.memoryId}`);
4176
+ }
4177
+ catch (e) {
4178
+ console.error(e.message);
4179
+ process.exit(1);
4180
+ }
4181
+ return;
4182
+ }
4183
+ if (subcommand === 'close') {
4184
+ const idRaw = args[1];
4185
+ if (!idRaw) {
4186
+ console.error('Usage: hippo skill close <id>');
4187
+ process.exit(1);
4188
+ }
4189
+ const id = parsePositiveSkillId(idRaw);
4190
+ const closed = skillsModule.closeSkill(hippoRoot, tenantId, id);
4191
+ console.log(`Skill #${closed.id} closed.`);
4192
+ return;
4193
+ }
4194
+ // Default subcommand: new (create). Accept both `skill new "<name>"` and the
4195
+ // bare `skill "<name>"` form: for the `new` keyword the name is args[1].
4196
+ const skillName = subcommand === 'new' ? (args[1] ?? '') : subcommand;
4197
+ const instrRaw = flags['instructions'];
4198
+ if (!skillName || typeof instrRaw !== 'string' || !instrRaw.trim()) {
4199
+ console.error('Usage: hippo skill new "<name>" --instructions "<text>" [--trigger "<when>"]');
4200
+ console.error(' hippo skill list [--status active|superseded|closed|all] [--limit N]');
4201
+ console.error(' hippo skill get <id>');
4202
+ console.error(' hippo skill export (render active skills as an AGENTS.md/CLAUDE.md block)');
4203
+ console.error(' hippo skill supersede <id> --instructions "<text>" [--trigger] [--change "<summary>"]');
4204
+ console.error(' hippo skill close <id>');
4205
+ process.exit(1);
4206
+ }
4207
+ const trigRaw = flags['trigger'];
4208
+ try {
4209
+ const created = skillsModule.saveSkill(hippoRoot, tenantId, {
4210
+ skillName,
4211
+ instructions: instrRaw,
4212
+ trigger: typeof trigRaw === 'string' && trigRaw ? trigRaw : undefined,
4213
+ extraTags: extractPathTags(process.cwd()),
4214
+ });
4215
+ console.log(`Skill recorded: #${created.id} (v${created.version})`);
4216
+ if (created.memoryId)
4217
+ console.log(` memory: ${created.memoryId}`);
4218
+ }
4219
+ catch (e) {
4220
+ console.error(e.message);
4221
+ process.exit(1);
4222
+ }
4223
+ }
4224
+ function parsePositiveBriefId(idRaw) {
4225
+ const s = String(idRaw ?? '').trim();
4226
+ const id = parseInt(s, 10);
4227
+ if (!/^\d+$/.test(s) || id <= 0) {
4228
+ console.error(`Invalid brief id: "${idRaw}" (expected a positive integer).`);
4229
+ process.exit(1);
4230
+ }
4231
+ return id;
4232
+ }
4233
+ function printBriefRow(b) {
4234
+ console.log(`#${b.id} [${b.status}] v${b.version} repo="${b.repo}" memory=${b.memoryId ?? '-'}`);
4235
+ if (b.changeSummary)
4236
+ console.log(` change: ${b.changeSummary}`);
4237
+ }
4238
+ function briefUsage() {
4239
+ console.error('Usage: hippo brief new "<repo>" --summary "<text>"');
4240
+ console.error(' hippo brief list [--status active|superseded|closed|all] [--repo "<repo>"] [--limit N]');
4241
+ console.error(' hippo brief get <id>');
4242
+ console.error(' hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
4243
+ console.error(' hippo brief close <id>');
4244
+ console.error(' hippo brief refresh "<repo>" [--dry-run] (auto-assemble the brief from the repo\'s receipts)');
4245
+ }
4246
+ function cmdProjectBrief(hippoRoot, args, flags) {
4247
+ requireInit(hippoRoot);
4248
+ const tenantId = resolveTenantId({});
4249
+ const subcommand = args[0] ?? '';
4250
+ if (subcommand === 'list') {
4251
+ const statusRaw = flags['status'];
4252
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
4253
+ const repoRaw = flags['repo'];
4254
+ const repo = typeof repoRaw === 'string' && repoRaw.trim() ? repoRaw.trim() : undefined;
4255
+ const limitRaw = flags['limit'];
4256
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
4257
+ if (!Number.isFinite(limit) || limit <= 0) {
4258
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4259
+ process.exit(1);
4260
+ }
4261
+ const opts = { limit, repo };
4262
+ if (status !== 'all') {
4263
+ if (!briefsModule.VALID_BRIEF_STATES.has(status)) {
4264
+ console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4265
+ process.exit(1);
4266
+ }
4267
+ opts.status = status;
4268
+ }
4269
+ const results = briefsModule.loadProjectBriefs(hippoRoot, tenantId, opts);
4270
+ if (results.length === 0) {
4271
+ console.log('No project briefs.');
4272
+ return;
4273
+ }
4274
+ console.log(`Found ${results.length} project briefs:\n`);
4275
+ for (const b of results)
4276
+ printBriefRow(b);
4277
+ return;
4278
+ }
4279
+ if (subcommand === 'refresh') {
4280
+ const repoRaw = args[1];
4281
+ if (!repoRaw) {
4282
+ console.error('Usage: hippo brief refresh "<repo>" [--dry-run]');
4283
+ process.exit(1);
4284
+ }
4285
+ const dryRun = Boolean(flags['dry-run']);
4286
+ try {
4287
+ if (dryRun) {
4288
+ const { markdown, receiptCount } = briefsModule.assembleBriefFromReceipts(hippoRoot, tenantId, repoRaw);
4289
+ console.error(`(dry-run: assembled from ${receiptCount} receipt(s); brief NOT written)`);
4290
+ console.log(markdown);
4291
+ return;
4292
+ }
4293
+ const created = briefsModule.refreshBrief(hippoRoot, tenantId, repoRaw, 'cli');
4294
+ console.log(`Project brief #${created.id} recorded (v${created.version}) for repo "${created.repo}".`);
4295
+ if (created.changeSummary)
4296
+ console.log(` change: ${created.changeSummary}`);
4297
+ if (created.memoryId)
4298
+ console.log(` memory: ${created.memoryId}`);
4299
+ }
4300
+ catch (e) {
4301
+ console.error(e.message);
4302
+ process.exit(1);
4303
+ }
4304
+ return;
4305
+ }
4306
+ if (subcommand === 'get') {
4307
+ const idRaw = args[1];
4308
+ if (!idRaw) {
4309
+ console.error('Usage: hippo brief get <id>');
4310
+ process.exit(1);
4311
+ }
4312
+ const id = parsePositiveBriefId(idRaw);
4313
+ const b = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
4314
+ if (!b) {
4315
+ console.error(`Project brief ${id} not found.`);
4316
+ process.exit(1);
4317
+ }
4318
+ console.log(`Project brief #${b.id}`);
4319
+ console.log(` repo: ${b.repo}`);
4320
+ console.log(` status: ${b.status}`);
4321
+ console.log(` version: ${b.version}`);
4322
+ console.log(` summary: ${b.summary}`);
4323
+ if (b.changeSummary)
4324
+ console.log(` change_summary: ${b.changeSummary}`);
4325
+ if (b.supersededBy !== null)
4326
+ console.log(` superseded_by: #${b.supersededBy}`);
4327
+ if (b.supersededAt)
4328
+ console.log(` superseded_at: ${b.supersededAt}`);
4329
+ if (b.closedAt)
4330
+ console.log(` closed_at: ${b.closedAt}`);
4331
+ if (b.memoryId)
4332
+ console.log(` memory: ${b.memoryId}`);
4333
+ console.log(` created: ${b.createdAt}`);
4334
+ return;
4335
+ }
4336
+ if (subcommand === 'supersede') {
4337
+ const idRaw = args[1];
4338
+ if (!idRaw) {
4339
+ console.error('Usage: hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
4340
+ process.exit(1);
4341
+ }
4342
+ const id = parsePositiveBriefId(idRaw);
4343
+ const summaryRaw = flags['summary'];
4344
+ if (typeof summaryRaw !== 'string' || !summaryRaw.trim()) {
4345
+ console.error('hippo brief supersede requires --summary "<text>" for the new version.');
4346
+ process.exit(1);
4347
+ }
4348
+ const existing = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
4349
+ if (!existing) {
4350
+ console.error(`Project brief ${id} not found.`);
4351
+ process.exit(1);
4352
+ }
4353
+ const changeRaw = flags['change'];
4354
+ try {
4355
+ const created = briefsModule.saveProjectBrief(hippoRoot, tenantId, {
4356
+ repo: existing.repo,
4357
+ summary: summaryRaw,
4358
+ changeSummary: typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined,
4359
+ supersedesBriefId: id,
4360
+ extraTags: extractPathTags(process.cwd()),
4361
+ });
4362
+ console.log(`Project brief #${created.id} recorded (v${created.version}), superseding #${id}.`);
4363
+ if (created.memoryId)
4364
+ console.log(` memory: ${created.memoryId}`);
4365
+ }
4366
+ catch (e) {
4367
+ console.error(e.message);
4368
+ process.exit(1);
4369
+ }
4370
+ return;
4371
+ }
4372
+ if (subcommand === 'close') {
4373
+ const idRaw = args[1];
4374
+ if (!idRaw) {
4375
+ console.error('Usage: hippo brief close <id>');
4376
+ process.exit(1);
4377
+ }
4378
+ const id = parsePositiveBriefId(idRaw);
4379
+ const closed = briefsModule.closeProjectBrief(hippoRoot, tenantId, id);
4380
+ console.log(`Project brief #${closed.id} closed.`);
4381
+ return;
4382
+ }
4383
+ // Default subcommand: new (create). Accept both `brief new "<repo>"` and the
4384
+ // bare `brief "<repo>"` form: for the `new` keyword the repo is args[1].
4385
+ const repo = subcommand === 'new' ? (args[1] ?? '') : subcommand;
4386
+ const summaryRaw = flags['summary'];
4387
+ if (!repo || typeof summaryRaw !== 'string' || !summaryRaw.trim()) {
4388
+ briefUsage();
4389
+ process.exit(1);
4390
+ }
4391
+ try {
4392
+ const created = briefsModule.saveProjectBrief(hippoRoot, tenantId, {
4393
+ repo,
4394
+ summary: summaryRaw,
4395
+ extraTags: extractPathTags(process.cwd()),
4396
+ });
4397
+ console.log(`Project brief recorded: #${created.id} (v${created.version}) for repo "${created.repo}"`);
4398
+ if (created.memoryId)
4399
+ console.log(` memory: ${created.memoryId}`);
4400
+ }
4401
+ catch (e) {
4402
+ console.error(e.message);
4403
+ process.exit(1);
4404
+ }
4405
+ }
4406
+ function parsePositiveNoteId(idRaw) {
4407
+ const s = String(idRaw ?? '').trim();
4408
+ const id = parseInt(s, 10);
4409
+ if (!/^\d+$/.test(s) || id <= 0) {
4410
+ console.error(`Invalid note id: "${idRaw}" (expected a positive integer).`);
4411
+ process.exit(1);
4412
+ }
4413
+ return id;
4414
+ }
4415
+ function printNoteRow(n) {
4416
+ console.log(`#${n.id} [${n.status}] v${n.version} customer="${n.customer}" memory=${n.memoryId ?? '-'}`);
4417
+ if (n.changeSummary)
4418
+ console.log(` change: ${n.changeSummary}`);
4419
+ }
4420
+ function noteUsage() {
4421
+ console.error('Usage: hippo note new "<customer>" --text "<note>"');
4422
+ console.error(' hippo note list [--status active|superseded|closed|all] [--customer "<id>"] [--limit N]');
4423
+ console.error(' hippo note get <id>');
4424
+ console.error(' hippo note supersede <id> --text "<note>" [--change "<summary>"]');
4425
+ console.error(' hippo note close <id>');
4426
+ }
4427
+ function cmdGraph(hippoRoot, args, _flags) {
4428
+ requireInit(hippoRoot);
4429
+ const tenantId = resolveTenantId({});
4430
+ const subcommand = args[0] ?? '';
4431
+ if (subcommand === 'extract') {
4432
+ const result = extractGraph(hippoRoot, tenantId);
4433
+ const byType = Object.entries(result.byType)
4434
+ .map(([t, n]) => `${t} ${n}`)
4435
+ .join(', ');
4436
+ const supersedes = result.relations - result.references;
4437
+ console.log(`Graph extracted: ${result.entities} entities (${byType}) + ${result.relations} relations (${supersedes} supersedes, ${result.references} references).`);
4438
+ if (result.truncated.length > 0) {
4439
+ console.error(`WARNING: under-extracted (hit the per-type cap): ${result.truncated.join(', ')}. The graph is incomplete for those types.`);
4440
+ }
4441
+ return;
4442
+ }
4443
+ console.error('Usage: hippo graph extract (rebuild the entity/relation graph from consolidated decisions/policies/customer-notes/project-briefs)');
4444
+ process.exit(1);
4445
+ }
4446
+ function cmdCustomerNote(hippoRoot, args, flags) {
4447
+ requireInit(hippoRoot);
4448
+ const tenantId = resolveTenantId({});
4449
+ const subcommand = args[0] ?? '';
4450
+ if (subcommand === 'list') {
4451
+ const statusRaw = flags['status'];
4452
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
4453
+ const customerRaw = flags['customer'];
4454
+ const customer = typeof customerRaw === 'string' && customerRaw.trim() ? customerRaw.trim() : undefined;
4455
+ const limitRaw = flags['limit'];
4456
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
4457
+ if (!Number.isFinite(limit) || limit <= 0) {
4458
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4459
+ process.exit(1);
4460
+ }
4461
+ const opts = { limit, customer };
4462
+ if (status !== 'all') {
4463
+ if (!customerNotesModule.VALID_NOTE_STATES.has(status)) {
4464
+ console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4465
+ process.exit(1);
4466
+ }
4467
+ opts.status = status;
4468
+ }
4469
+ const results = customerNotesModule.loadCustomerNotes(hippoRoot, tenantId, opts);
4470
+ if (results.length === 0) {
4471
+ console.log('No customer notes.');
4472
+ return;
4473
+ }
4474
+ console.log(`Found ${results.length} customer notes:\n`);
4475
+ for (const n of results)
4476
+ printNoteRow(n);
4477
+ return;
4478
+ }
4479
+ if (subcommand === 'get') {
4480
+ const idRaw = args[1];
4481
+ if (!idRaw) {
4482
+ console.error('Usage: hippo note get <id>');
4483
+ process.exit(1);
4484
+ }
4485
+ const id = parsePositiveNoteId(idRaw);
4486
+ const n = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
4487
+ if (!n) {
4488
+ console.error(`Customer note ${id} not found.`);
4489
+ process.exit(1);
4490
+ }
4491
+ console.log(`Customer note #${n.id}`);
4492
+ console.log(` customer: ${n.customer}`);
4493
+ console.log(` status: ${n.status}`);
4494
+ console.log(` version: ${n.version}`);
4495
+ console.log(` note: ${n.note}`);
4496
+ if (n.changeSummary)
4497
+ console.log(` change_summary: ${n.changeSummary}`);
4498
+ if (n.supersededBy !== null)
4499
+ console.log(` superseded_by: #${n.supersededBy}`);
4500
+ if (n.supersededAt)
4501
+ console.log(` superseded_at: ${n.supersededAt}`);
4502
+ if (n.closedAt)
4503
+ console.log(` closed_at: ${n.closedAt}`);
4504
+ if (n.memoryId)
4505
+ console.log(` memory: ${n.memoryId}`);
4506
+ console.log(` created: ${n.createdAt}`);
4507
+ return;
4508
+ }
4509
+ if (subcommand === 'supersede') {
4510
+ const idRaw = args[1];
4511
+ if (!idRaw) {
4512
+ console.error('Usage: hippo note supersede <id> --text "<note>" [--change "<summary>"]');
4513
+ process.exit(1);
4514
+ }
4515
+ const id = parsePositiveNoteId(idRaw);
4516
+ const textRaw = flags['text'];
4517
+ if (typeof textRaw !== 'string' || !textRaw.trim()) {
4518
+ console.error('hippo note supersede requires --text "<note>" for the new version.');
4519
+ process.exit(1);
4520
+ }
4521
+ const existing = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
4522
+ if (!existing) {
4523
+ console.error(`Customer note ${id} not found.`);
4524
+ process.exit(1);
4525
+ }
4526
+ const changeRaw = flags['change'];
4527
+ try {
4528
+ const created = customerNotesModule.saveCustomerNote(hippoRoot, tenantId, {
4529
+ customer: existing.customer,
4530
+ note: textRaw,
4531
+ changeSummary: typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined,
4532
+ supersedesNoteId: id,
4533
+ extraTags: extractPathTags(process.cwd()),
4534
+ });
4535
+ console.log(`Customer note #${created.id} recorded (v${created.version}), superseding #${id}.`);
4536
+ if (created.memoryId)
4537
+ console.log(` memory: ${created.memoryId}`);
4538
+ }
4539
+ catch (e) {
4540
+ console.error(e.message);
4541
+ process.exit(1);
4542
+ }
4543
+ return;
4544
+ }
4545
+ if (subcommand === 'close') {
4546
+ const idRaw = args[1];
4547
+ if (!idRaw) {
4548
+ console.error('Usage: hippo note close <id>');
4549
+ process.exit(1);
4550
+ }
4551
+ const id = parsePositiveNoteId(idRaw);
4552
+ const closed = customerNotesModule.closeCustomerNote(hippoRoot, tenantId, id);
4553
+ console.log(`Customer note #${closed.id} closed.`);
4554
+ return;
4555
+ }
4556
+ // Default subcommand: new (create). Accept both `note new "<customer>"` and the
4557
+ // bare `note "<customer>"` form: for the `new` keyword the customer is args[1].
4558
+ const customer = subcommand === 'new' ? (args[1] ?? '') : subcommand;
4559
+ const textRaw = flags['text'];
4560
+ if (!customer || typeof textRaw !== 'string' || !textRaw.trim()) {
4561
+ noteUsage();
4562
+ process.exit(1);
4563
+ }
4564
+ try {
4565
+ const created = customerNotesModule.saveCustomerNote(hippoRoot, tenantId, {
4566
+ customer,
4567
+ note: textRaw,
4568
+ extraTags: extractPathTags(process.cwd()),
4569
+ });
4570
+ console.log(`Customer note recorded: #${created.id} (v${created.version}) for customer "${created.customer}"`);
4571
+ if (created.memoryId)
4572
+ console.log(` memory: ${created.memoryId}`);
4573
+ }
4574
+ catch (e) {
4575
+ console.error(e.message);
4576
+ process.exit(1);
4577
+ }
4578
+ }
3466
4579
  function cmdCurrent(hippoRoot, args, flags) {
3467
4580
  requireInit(hippoRoot);
3468
4581
  const subcommand = args[0] ?? 'show';
@@ -4880,6 +5993,24 @@ const VALID_AUDIT_OPS = new Set([
4880
5993
  'decision_create', // E2 decision first-class object — emitted by saveDecision
4881
5994
  'decision_supersede', // E2 — emitted by saveDecision when --supersedes resolves to an active decision row
4882
5995
  'decision_close', // E2 — emitted by closeDecision
5996
+ 'incident_open', // E2 incident first-class object — emitted by saveIncident
5997
+ 'incident_resolve', // E2 — emitted by resolveIncident (open -> resolved)
5998
+ 'incident_close', // E2 — emitted by closeIncident (open|resolved -> closed)
5999
+ 'process_create', // E2 process first-class object — emitted by saveProcess
6000
+ 'process_supersede', // E2 — emitted by saveProcess on a supersession
6001
+ 'process_close', // E2 — emitted by closeProcess
6002
+ 'policy_create', // E2 policy first-class object — emitted by savePolicy
6003
+ 'policy_supersede', // E2 — emitted by savePolicy on a supersession
6004
+ 'policy_close', // E2 — emitted by closePolicy
6005
+ 'skill_create', // E2 skill first-class object — emitted by saveSkill
6006
+ 'skill_supersede', // E2 — emitted by saveSkill on a supersession
6007
+ 'skill_close', // E2 — emitted by closeSkill
6008
+ 'project_brief_create', // E2 project_brief first-class object — emitted by saveProjectBrief
6009
+ 'project_brief_supersede', // E2 — emitted by saveProjectBrief on a supersession (incl. refresh)
6010
+ 'project_brief_close', // E2 — emitted by closeProjectBrief
6011
+ 'customer_note_create', // E2 customer_note first-class object — emitted by saveCustomerNote
6012
+ 'customer_note_supersede', // E2 — emitted by saveCustomerNote on a supersession
6013
+ 'customer_note_close', // E2 — emitted by closeCustomerNote
4883
6014
  ]);
4884
6015
  function formatAuditRow(ev) {
4885
6016
  const target = ev.targetId ?? '-';
@@ -5439,6 +6570,13 @@ Commands:
5439
6570
  --min-results <n> Minimum results regardless of budget (default: 1)
5440
6571
  --json Output as JSON
5441
6572
  --why Show match reasons and source annotations
6573
+ --hops <n> E3.2 multi-hop graph recall: also surface memories
6574
+ reached by walking the entities/relations graph <n>
6575
+ hops (0..3, default off) out from the lexical seeds.
6576
+ Graph hits are tagged [graph: Nhop <rel>]. Today the
6577
+ graph holds supersedes edges (E3.1); cross-object edges
6578
+ light up the same traversal once extracted.
6579
+ --max-neighbors <n> Per-hop fanout cap for --hops (1..200, default 25).
5442
6580
  --no-mmr Disable MMR diversity re-ranking
5443
6581
  --mmr-lambda <f> MMR balance 0..1 (default: 0.7, 1.0 = pure relevance)
5444
6582
  --evc-adaptive ACC-style: when top-K shows high inter-item overlap
@@ -5673,6 +6811,75 @@ Commands:
5673
6811
  List decisions (table is authoritative, survives decay)
5674
6812
  decide get <id> Show a decision by its table id
5675
6813
  decide close <id> Retire (close) an active decision by its table id
6814
+ incident "<incident>" Record an incident (first-class object + memory mirror)
6815
+ --context "<details>" What happened / surrounding detail
6816
+ --link <mem-id> Link a memory as evidence (repeatable)
6817
+ incident list [--status open|resolved|closed|all] [--limit N]
6818
+ List incidents (table is authoritative, survives decay)
6819
+ incident get <id> Show an incident by its table id
6820
+ incident resolve <id> Resolve an open incident (open -> resolved)
6821
+ --resolution "<text>" How it was resolved (required)
6822
+ incident close <id> Retire (close) an open or resolved incident by its table id
6823
+ process new "<name>" Record a process map (first-class object + memory mirror)
6824
+ --step "<text>" An ordered step (repeatable)
6825
+ --description "<text>" Optional summary of the process
6826
+ process list [--status active|superseded|closed|all] [--limit N]
6827
+ List processes (table is authoritative, survives decay)
6828
+ process get <id> Show a process (with its steps) by its table id
6829
+ process supersede <id> Record a new version that supersedes an active process
6830
+ --step "<text>" A step of the new version (repeatable, required)
6831
+ --change "<summary>" What changed in this version (the delta note)
6832
+ --description "<text>" Optional summary of the new version
6833
+ process close <id> Retire (close) an active process by its table id
6834
+ policy new "<name>" Record a policy (bi-temporal first-class object + mirror)
6835
+ --text "<rule>" The policy rule/statement (required)
6836
+ --from "<iso>" Effective-from date (default: now)
6837
+ --to "<iso>" Effective-to date (optional; open-ended if omitted)
6838
+ policy list [--status active|superseded|closed|all] [--limit N]
6839
+ List policies (table is authoritative, survives decay)
6840
+ policy get <id> Show a policy by its table id
6841
+ policy asof "<iso-date>" Show active policies in force at a valid-time
6842
+ --name "<policy>" Filter to one policy by name
6843
+ policy supersede <id> Record a new version that supersedes an active policy
6844
+ --text "<rule>" The new rule (required)
6845
+ --from "<iso>" New effective-from (default: now)
6846
+ --to "<iso>" New effective-to (optional)
6847
+ --change "<summary>" What changed in this version (the delta note)
6848
+ policy close <id> Retire (close) an active policy by its table id
6849
+ skill new "<name>" Record a skill (reusable agent-followable capability)
6850
+ --instructions "<txt>" The skill body (required)
6851
+ --trigger "<when>" Optional: when to apply this skill
6852
+ skill list [--status active|superseded|closed|all] [--limit N]
6853
+ List skills (table is authoritative, survives decay)
6854
+ skill get <id> Show a skill by its table id
6855
+ skill export Render active skills as an AGENTS.md/CLAUDE.md markdown block
6856
+ skill supersede <id> Record a new version that supersedes an active skill
6857
+ --instructions "<txt>" The new skill body (required)
6858
+ --trigger "<when>" Optional new trigger
6859
+ --change "<summary>" What changed in this version (the delta note)
6860
+ skill close <id> Retire (close) an active skill by its table id
6861
+ brief new "<repo>" Record a repo-scoped project brief
6862
+ --summary "<text>" The brief body (required)
6863
+ brief list [--status active|superseded|closed|all] [--repo "<repo>"] [--limit N]
6864
+ List project briefs (table is authoritative, survives decay)
6865
+ brief get <id> Show a project brief by its table id
6866
+ brief supersede <id> Record a new version that supersedes an active brief
6867
+ --summary "<text>" The new brief body (required)
6868
+ --change "<summary>" What changed in this version (the delta note)
6869
+ brief close <id> Retire (close) an active project brief by its table id
6870
+ brief refresh "<repo>" Auto-assemble the brief from the repo's receipts (path:<repo>)
6871
+ --dry-run Print the assembled brief without writing it
6872
+ note new "<customer>" Record a customer/account-scoped note
6873
+ --text "<note>" The note body (required)
6874
+ note list [--status active|superseded|closed|all] [--customer "<id>"] [--limit N]
6875
+ List customer notes (table is authoritative, survives decay)
6876
+ note get <id> Show a customer note by its table id
6877
+ note supersede <id> Record a new version that supersedes an active note
6878
+ --text "<note>" The new note body (required)
6879
+ --change "<summary>" What changed in this version (the delta note)
6880
+ note close <id> Retire (close) an active customer note by its table id
6881
+ graph extract Rebuild the entity/relation graph from consolidated objects
6882
+ (decisions/policies/customer-notes/project-briefs); idempotent
5676
6883
  invalidate "<pattern>" Actively weaken memories matching an old pattern
5677
6884
  --reason "<why>" Optional: what replaced it
5678
6885
  wm <sub> Working memory — bounded buffer for current state
@@ -5754,6 +6961,17 @@ Examples:
5754
6961
  hippo setup
5755
6962
  hippo hook install claude-code
5756
6963
  hippo decide "Use PostgreSQL for new services" --context "JSONB support"
6964
+ hippo incident "Prod outage: DB connection pool exhausted" --context "spike at 14:00"
6965
+ hippo process new "Release" --step "run tests" --step "bump version" --step "publish"
6966
+ hippo policy new "Data retention" --text "Delete logs after 90 days" --from 2026-01-01
6967
+ hippo policy asof 2026-03-01 --name "Data retention"
6968
+ hippo skill new "Run tests" --instructions "npm test before every commit" --trigger "before commit"
6969
+ hippo skill export
6970
+ hippo brief new "hippo" --summary "Agent-memory library; E2 first-class objects in progress"
6971
+ hippo brief refresh "hippo"
6972
+ hippo note new "Acme Corp" --text "Renewal call: wants SSO before Q3; champion is the VP Eng"
6973
+ hippo note list --customer "Acme Corp" --status active
6974
+ hippo graph extract
5757
6975
  hippo invalidate "REST API" --reason "migrated to GraphQL"
5758
6976
  hippo export memories.json
5759
6977
  hippo export --format markdown memories.md
@@ -6360,6 +7578,29 @@ async function main() {
6360
7578
  case 'decide':
6361
7579
  cmdDecide(hippoRoot, args, flags);
6362
7580
  break;
7581
+ case 'incident':
7582
+ cmdIncident(hippoRoot, args, flags);
7583
+ break;
7584
+ case 'process':
7585
+ cmdProcess(hippoRoot, args, flags);
7586
+ break;
7587
+ case 'policy':
7588
+ cmdPolicy(hippoRoot, args, flags);
7589
+ break;
7590
+ case 'skill':
7591
+ cmdSkill(hippoRoot, args, flags);
7592
+ break;
7593
+ case 'brief':
7594
+ case 'project-brief':
7595
+ cmdProjectBrief(hippoRoot, args, flags);
7596
+ break;
7597
+ case 'note':
7598
+ case 'customer-note':
7599
+ cmdCustomerNote(hippoRoot, args, flags);
7600
+ break;
7601
+ case 'graph':
7602
+ cmdGraph(hippoRoot, args, flags);
7603
+ break;
6363
7604
  case 'help':
6364
7605
  case '--help':
6365
7606
  case '-h':