hippo-memory 1.15.0 → 1.16.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 +1243 -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 +39 -0
  16. package/dist/graph-extract.d.ts.map +1 -0
  17. package/dist/graph-extract.js +141 -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 +1243 -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 +141 -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/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,1061 @@ 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
+ console.log(`Graph extracted: ${result.entities} entities (${byType}) + ${result.relations} supersedes relations.`);
4437
+ if (result.truncated.length > 0) {
4438
+ console.error(`WARNING: under-extracted (hit the per-type cap): ${result.truncated.join(', ')}. The graph is incomplete for those types.`);
4439
+ }
4440
+ return;
4441
+ }
4442
+ console.error('Usage: hippo graph extract (rebuild the entity/relation graph from consolidated decisions/policies/customer-notes/project-briefs)');
4443
+ process.exit(1);
4444
+ }
4445
+ function cmdCustomerNote(hippoRoot, args, flags) {
4446
+ requireInit(hippoRoot);
4447
+ const tenantId = resolveTenantId({});
4448
+ const subcommand = args[0] ?? '';
4449
+ if (subcommand === 'list') {
4450
+ const statusRaw = flags['status'];
4451
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
4452
+ const customerRaw = flags['customer'];
4453
+ const customer = typeof customerRaw === 'string' && customerRaw.trim() ? customerRaw.trim() : undefined;
4454
+ const limitRaw = flags['limit'];
4455
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
4456
+ if (!Number.isFinite(limit) || limit <= 0) {
4457
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
4458
+ process.exit(1);
4459
+ }
4460
+ const opts = { limit, customer };
4461
+ if (status !== 'all') {
4462
+ if (!customerNotesModule.VALID_NOTE_STATES.has(status)) {
4463
+ console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
4464
+ process.exit(1);
4465
+ }
4466
+ opts.status = status;
4467
+ }
4468
+ const results = customerNotesModule.loadCustomerNotes(hippoRoot, tenantId, opts);
4469
+ if (results.length === 0) {
4470
+ console.log('No customer notes.');
4471
+ return;
4472
+ }
4473
+ console.log(`Found ${results.length} customer notes:\n`);
4474
+ for (const n of results)
4475
+ printNoteRow(n);
4476
+ return;
4477
+ }
4478
+ if (subcommand === 'get') {
4479
+ const idRaw = args[1];
4480
+ if (!idRaw) {
4481
+ console.error('Usage: hippo note get <id>');
4482
+ process.exit(1);
4483
+ }
4484
+ const id = parsePositiveNoteId(idRaw);
4485
+ const n = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
4486
+ if (!n) {
4487
+ console.error(`Customer note ${id} not found.`);
4488
+ process.exit(1);
4489
+ }
4490
+ console.log(`Customer note #${n.id}`);
4491
+ console.log(` customer: ${n.customer}`);
4492
+ console.log(` status: ${n.status}`);
4493
+ console.log(` version: ${n.version}`);
4494
+ console.log(` note: ${n.note}`);
4495
+ if (n.changeSummary)
4496
+ console.log(` change_summary: ${n.changeSummary}`);
4497
+ if (n.supersededBy !== null)
4498
+ console.log(` superseded_by: #${n.supersededBy}`);
4499
+ if (n.supersededAt)
4500
+ console.log(` superseded_at: ${n.supersededAt}`);
4501
+ if (n.closedAt)
4502
+ console.log(` closed_at: ${n.closedAt}`);
4503
+ if (n.memoryId)
4504
+ console.log(` memory: ${n.memoryId}`);
4505
+ console.log(` created: ${n.createdAt}`);
4506
+ return;
4507
+ }
4508
+ if (subcommand === 'supersede') {
4509
+ const idRaw = args[1];
4510
+ if (!idRaw) {
4511
+ console.error('Usage: hippo note supersede <id> --text "<note>" [--change "<summary>"]');
4512
+ process.exit(1);
4513
+ }
4514
+ const id = parsePositiveNoteId(idRaw);
4515
+ const textRaw = flags['text'];
4516
+ if (typeof textRaw !== 'string' || !textRaw.trim()) {
4517
+ console.error('hippo note supersede requires --text "<note>" for the new version.');
4518
+ process.exit(1);
4519
+ }
4520
+ const existing = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
4521
+ if (!existing) {
4522
+ console.error(`Customer note ${id} not found.`);
4523
+ process.exit(1);
4524
+ }
4525
+ const changeRaw = flags['change'];
4526
+ try {
4527
+ const created = customerNotesModule.saveCustomerNote(hippoRoot, tenantId, {
4528
+ customer: existing.customer,
4529
+ note: textRaw,
4530
+ changeSummary: typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined,
4531
+ supersedesNoteId: id,
4532
+ extraTags: extractPathTags(process.cwd()),
4533
+ });
4534
+ console.log(`Customer note #${created.id} recorded (v${created.version}), superseding #${id}.`);
4535
+ if (created.memoryId)
4536
+ console.log(` memory: ${created.memoryId}`);
4537
+ }
4538
+ catch (e) {
4539
+ console.error(e.message);
4540
+ process.exit(1);
4541
+ }
4542
+ return;
4543
+ }
4544
+ if (subcommand === 'close') {
4545
+ const idRaw = args[1];
4546
+ if (!idRaw) {
4547
+ console.error('Usage: hippo note close <id>');
4548
+ process.exit(1);
4549
+ }
4550
+ const id = parsePositiveNoteId(idRaw);
4551
+ const closed = customerNotesModule.closeCustomerNote(hippoRoot, tenantId, id);
4552
+ console.log(`Customer note #${closed.id} closed.`);
4553
+ return;
4554
+ }
4555
+ // Default subcommand: new (create). Accept both `note new "<customer>"` and the
4556
+ // bare `note "<customer>"` form: for the `new` keyword the customer is args[1].
4557
+ const customer = subcommand === 'new' ? (args[1] ?? '') : subcommand;
4558
+ const textRaw = flags['text'];
4559
+ if (!customer || typeof textRaw !== 'string' || !textRaw.trim()) {
4560
+ noteUsage();
4561
+ process.exit(1);
4562
+ }
4563
+ try {
4564
+ const created = customerNotesModule.saveCustomerNote(hippoRoot, tenantId, {
4565
+ customer,
4566
+ note: textRaw,
4567
+ extraTags: extractPathTags(process.cwd()),
4568
+ });
4569
+ console.log(`Customer note recorded: #${created.id} (v${created.version}) for customer "${created.customer}"`);
4570
+ if (created.memoryId)
4571
+ console.log(` memory: ${created.memoryId}`);
4572
+ }
4573
+ catch (e) {
4574
+ console.error(e.message);
4575
+ process.exit(1);
4576
+ }
4577
+ }
3466
4578
  function cmdCurrent(hippoRoot, args, flags) {
3467
4579
  requireInit(hippoRoot);
3468
4580
  const subcommand = args[0] ?? 'show';
@@ -4880,6 +5992,24 @@ const VALID_AUDIT_OPS = new Set([
4880
5992
  'decision_create', // E2 decision first-class object — emitted by saveDecision
4881
5993
  'decision_supersede', // E2 — emitted by saveDecision when --supersedes resolves to an active decision row
4882
5994
  'decision_close', // E2 — emitted by closeDecision
5995
+ 'incident_open', // E2 incident first-class object — emitted by saveIncident
5996
+ 'incident_resolve', // E2 — emitted by resolveIncident (open -> resolved)
5997
+ 'incident_close', // E2 — emitted by closeIncident (open|resolved -> closed)
5998
+ 'process_create', // E2 process first-class object — emitted by saveProcess
5999
+ 'process_supersede', // E2 — emitted by saveProcess on a supersession
6000
+ 'process_close', // E2 — emitted by closeProcess
6001
+ 'policy_create', // E2 policy first-class object — emitted by savePolicy
6002
+ 'policy_supersede', // E2 — emitted by savePolicy on a supersession
6003
+ 'policy_close', // E2 — emitted by closePolicy
6004
+ 'skill_create', // E2 skill first-class object — emitted by saveSkill
6005
+ 'skill_supersede', // E2 — emitted by saveSkill on a supersession
6006
+ 'skill_close', // E2 — emitted by closeSkill
6007
+ 'project_brief_create', // E2 project_brief first-class object — emitted by saveProjectBrief
6008
+ 'project_brief_supersede', // E2 — emitted by saveProjectBrief on a supersession (incl. refresh)
6009
+ 'project_brief_close', // E2 — emitted by closeProjectBrief
6010
+ 'customer_note_create', // E2 customer_note first-class object — emitted by saveCustomerNote
6011
+ 'customer_note_supersede', // E2 — emitted by saveCustomerNote on a supersession
6012
+ 'customer_note_close', // E2 — emitted by closeCustomerNote
4883
6013
  ]);
4884
6014
  function formatAuditRow(ev) {
4885
6015
  const target = ev.targetId ?? '-';
@@ -5439,6 +6569,13 @@ Commands:
5439
6569
  --min-results <n> Minimum results regardless of budget (default: 1)
5440
6570
  --json Output as JSON
5441
6571
  --why Show match reasons and source annotations
6572
+ --hops <n> E3.2 multi-hop graph recall: also surface memories
6573
+ reached by walking the entities/relations graph <n>
6574
+ hops (0..3, default off) out from the lexical seeds.
6575
+ Graph hits are tagged [graph: Nhop <rel>]. Today the
6576
+ graph holds supersedes edges (E3.1); cross-object edges
6577
+ light up the same traversal once extracted.
6578
+ --max-neighbors <n> Per-hop fanout cap for --hops (1..200, default 25).
5442
6579
  --no-mmr Disable MMR diversity re-ranking
5443
6580
  --mmr-lambda <f> MMR balance 0..1 (default: 0.7, 1.0 = pure relevance)
5444
6581
  --evc-adaptive ACC-style: when top-K shows high inter-item overlap
@@ -5673,6 +6810,75 @@ Commands:
5673
6810
  List decisions (table is authoritative, survives decay)
5674
6811
  decide get <id> Show a decision by its table id
5675
6812
  decide close <id> Retire (close) an active decision by its table id
6813
+ incident "<incident>" Record an incident (first-class object + memory mirror)
6814
+ --context "<details>" What happened / surrounding detail
6815
+ --link <mem-id> Link a memory as evidence (repeatable)
6816
+ incident list [--status open|resolved|closed|all] [--limit N]
6817
+ List incidents (table is authoritative, survives decay)
6818
+ incident get <id> Show an incident by its table id
6819
+ incident resolve <id> Resolve an open incident (open -> resolved)
6820
+ --resolution "<text>" How it was resolved (required)
6821
+ incident close <id> Retire (close) an open or resolved incident by its table id
6822
+ process new "<name>" Record a process map (first-class object + memory mirror)
6823
+ --step "<text>" An ordered step (repeatable)
6824
+ --description "<text>" Optional summary of the process
6825
+ process list [--status active|superseded|closed|all] [--limit N]
6826
+ List processes (table is authoritative, survives decay)
6827
+ process get <id> Show a process (with its steps) by its table id
6828
+ process supersede <id> Record a new version that supersedes an active process
6829
+ --step "<text>" A step of the new version (repeatable, required)
6830
+ --change "<summary>" What changed in this version (the delta note)
6831
+ --description "<text>" Optional summary of the new version
6832
+ process close <id> Retire (close) an active process by its table id
6833
+ policy new "<name>" Record a policy (bi-temporal first-class object + mirror)
6834
+ --text "<rule>" The policy rule/statement (required)
6835
+ --from "<iso>" Effective-from date (default: now)
6836
+ --to "<iso>" Effective-to date (optional; open-ended if omitted)
6837
+ policy list [--status active|superseded|closed|all] [--limit N]
6838
+ List policies (table is authoritative, survives decay)
6839
+ policy get <id> Show a policy by its table id
6840
+ policy asof "<iso-date>" Show active policies in force at a valid-time
6841
+ --name "<policy>" Filter to one policy by name
6842
+ policy supersede <id> Record a new version that supersedes an active policy
6843
+ --text "<rule>" The new rule (required)
6844
+ --from "<iso>" New effective-from (default: now)
6845
+ --to "<iso>" New effective-to (optional)
6846
+ --change "<summary>" What changed in this version (the delta note)
6847
+ policy close <id> Retire (close) an active policy by its table id
6848
+ skill new "<name>" Record a skill (reusable agent-followable capability)
6849
+ --instructions "<txt>" The skill body (required)
6850
+ --trigger "<when>" Optional: when to apply this skill
6851
+ skill list [--status active|superseded|closed|all] [--limit N]
6852
+ List skills (table is authoritative, survives decay)
6853
+ skill get <id> Show a skill by its table id
6854
+ skill export Render active skills as an AGENTS.md/CLAUDE.md markdown block
6855
+ skill supersede <id> Record a new version that supersedes an active skill
6856
+ --instructions "<txt>" The new skill body (required)
6857
+ --trigger "<when>" Optional new trigger
6858
+ --change "<summary>" What changed in this version (the delta note)
6859
+ skill close <id> Retire (close) an active skill by its table id
6860
+ brief new "<repo>" Record a repo-scoped project brief
6861
+ --summary "<text>" The brief body (required)
6862
+ brief list [--status active|superseded|closed|all] [--repo "<repo>"] [--limit N]
6863
+ List project briefs (table is authoritative, survives decay)
6864
+ brief get <id> Show a project brief by its table id
6865
+ brief supersede <id> Record a new version that supersedes an active brief
6866
+ --summary "<text>" The new brief body (required)
6867
+ --change "<summary>" What changed in this version (the delta note)
6868
+ brief close <id> Retire (close) an active project brief by its table id
6869
+ brief refresh "<repo>" Auto-assemble the brief from the repo's receipts (path:<repo>)
6870
+ --dry-run Print the assembled brief without writing it
6871
+ note new "<customer>" Record a customer/account-scoped note
6872
+ --text "<note>" The note body (required)
6873
+ note list [--status active|superseded|closed|all] [--customer "<id>"] [--limit N]
6874
+ List customer notes (table is authoritative, survives decay)
6875
+ note get <id> Show a customer note by its table id
6876
+ note supersede <id> Record a new version that supersedes an active note
6877
+ --text "<note>" The new note body (required)
6878
+ --change "<summary>" What changed in this version (the delta note)
6879
+ note close <id> Retire (close) an active customer note by its table id
6880
+ graph extract Rebuild the entity/relation graph from consolidated objects
6881
+ (decisions/policies/customer-notes/project-briefs); idempotent
5676
6882
  invalidate "<pattern>" Actively weaken memories matching an old pattern
5677
6883
  --reason "<why>" Optional: what replaced it
5678
6884
  wm <sub> Working memory — bounded buffer for current state
@@ -5754,6 +6960,17 @@ Examples:
5754
6960
  hippo setup
5755
6961
  hippo hook install claude-code
5756
6962
  hippo decide "Use PostgreSQL for new services" --context "JSONB support"
6963
+ hippo incident "Prod outage: DB connection pool exhausted" --context "spike at 14:00"
6964
+ hippo process new "Release" --step "run tests" --step "bump version" --step "publish"
6965
+ hippo policy new "Data retention" --text "Delete logs after 90 days" --from 2026-01-01
6966
+ hippo policy asof 2026-03-01 --name "Data retention"
6967
+ hippo skill new "Run tests" --instructions "npm test before every commit" --trigger "before commit"
6968
+ hippo skill export
6969
+ hippo brief new "hippo" --summary "Agent-memory library; E2 first-class objects in progress"
6970
+ hippo brief refresh "hippo"
6971
+ hippo note new "Acme Corp" --text "Renewal call: wants SSO before Q3; champion is the VP Eng"
6972
+ hippo note list --customer "Acme Corp" --status active
6973
+ hippo graph extract
5757
6974
  hippo invalidate "REST API" --reason "migrated to GraphQL"
5758
6975
  hippo export memories.json
5759
6976
  hippo export --format markdown memories.md
@@ -6360,6 +7577,29 @@ async function main() {
6360
7577
  case 'decide':
6361
7578
  cmdDecide(hippoRoot, args, flags);
6362
7579
  break;
7580
+ case 'incident':
7581
+ cmdIncident(hippoRoot, args, flags);
7582
+ break;
7583
+ case 'process':
7584
+ cmdProcess(hippoRoot, args, flags);
7585
+ break;
7586
+ case 'policy':
7587
+ cmdPolicy(hippoRoot, args, flags);
7588
+ break;
7589
+ case 'skill':
7590
+ cmdSkill(hippoRoot, args, flags);
7591
+ break;
7592
+ case 'brief':
7593
+ case 'project-brief':
7594
+ cmdProjectBrief(hippoRoot, args, flags);
7595
+ break;
7596
+ case 'note':
7597
+ case 'customer-note':
7598
+ cmdCustomerNote(hippoRoot, args, flags);
7599
+ break;
7600
+ case 'graph':
7601
+ cmdGraph(hippoRoot, args, flags);
7602
+ break;
6363
7603
  case 'help':
6364
7604
  case '--help':
6365
7605
  case '-h':