hippo-memory 1.14.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 (106) 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 +1594 -229
  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 +1286 -472
  14. package/dist/db.js.map +1 -1
  15. package/dist/decisions.d.ts +91 -0
  16. package/dist/decisions.d.ts.map +1 -0
  17. package/dist/decisions.js +278 -0
  18. package/dist/decisions.js.map +1 -0
  19. package/dist/graph-extract.d.ts +39 -0
  20. package/dist/graph-extract.d.ts.map +1 -0
  21. package/dist/graph-extract.js +141 -0
  22. package/dist/graph-extract.js.map +1 -0
  23. package/dist/graph-recall.d.ts +41 -0
  24. package/dist/graph-recall.d.ts.map +1 -0
  25. package/dist/graph-recall.js +246 -0
  26. package/dist/graph-recall.js.map +1 -0
  27. package/dist/graph.d.ts +137 -0
  28. package/dist/graph.d.ts.map +1 -0
  29. package/dist/graph.js +433 -0
  30. package/dist/graph.js.map +1 -0
  31. package/dist/incidents.d.ts +100 -0
  32. package/dist/incidents.d.ts.map +1 -0
  33. package/dist/incidents.js +322 -0
  34. package/dist/incidents.js.map +1 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/memory.d.ts +6 -0
  40. package/dist/memory.d.ts.map +1 -1
  41. package/dist/memory.js +6 -0
  42. package/dist/memory.js.map +1 -1
  43. package/dist/policies.d.ts +149 -0
  44. package/dist/policies.d.ts.map +1 -0
  45. package/dist/policies.js +380 -0
  46. package/dist/policies.js.map +1 -0
  47. package/dist/processes.d.ts +104 -0
  48. package/dist/processes.d.ts.map +1 -0
  49. package/dist/processes.js +330 -0
  50. package/dist/processes.js.map +1 -0
  51. package/dist/project-briefs.d.ts +126 -0
  52. package/dist/project-briefs.d.ts.map +1 -0
  53. package/dist/project-briefs.js +453 -0
  54. package/dist/project-briefs.js.map +1 -0
  55. package/dist/search.d.ts +7 -0
  56. package/dist/search.d.ts.map +1 -1
  57. package/dist/search.js.map +1 -1
  58. package/dist/server.d.ts.map +1 -1
  59. package/dist/server.js +1181 -8
  60. package/dist/server.js.map +1 -1
  61. package/dist/skills.d.ts +98 -0
  62. package/dist/skills.d.ts.map +1 -0
  63. package/dist/skills.js +339 -0
  64. package/dist/skills.js.map +1 -0
  65. package/dist/src/audit.js.map +1 -1
  66. package/dist/src/cli.js +1594 -229
  67. package/dist/src/cli.js.map +1 -1
  68. package/dist/src/customer-notes.js +296 -0
  69. package/dist/src/customer-notes.js.map +1 -0
  70. package/dist/src/db.js +1286 -472
  71. package/dist/src/db.js.map +1 -1
  72. package/dist/src/decisions.js +278 -0
  73. package/dist/src/decisions.js.map +1 -0
  74. package/dist/src/graph-extract.js +141 -0
  75. package/dist/src/graph-extract.js.map +1 -0
  76. package/dist/src/graph-recall.js +246 -0
  77. package/dist/src/graph-recall.js.map +1 -0
  78. package/dist/src/graph.js +433 -0
  79. package/dist/src/graph.js.map +1 -0
  80. package/dist/src/incidents.js +322 -0
  81. package/dist/src/incidents.js.map +1 -0
  82. package/dist/src/index.js +1 -0
  83. package/dist/src/index.js.map +1 -1
  84. package/dist/src/memory.js +6 -0
  85. package/dist/src/memory.js.map +1 -1
  86. package/dist/src/policies.js +380 -0
  87. package/dist/src/policies.js.map +1 -0
  88. package/dist/src/processes.js +330 -0
  89. package/dist/src/processes.js.map +1 -0
  90. package/dist/src/project-briefs.js +453 -0
  91. package/dist/src/project-briefs.js.map +1 -0
  92. package/dist/src/search.js.map +1 -1
  93. package/dist/src/server.js +1181 -8
  94. package/dist/src/server.js.map +1 -1
  95. package/dist/src/skills.js +339 -0
  96. package/dist/src/skills.js.map +1 -0
  97. package/dist/src/version.js +1 -1
  98. package/dist/src/version.js.map +1 -1
  99. package/dist/version.d.ts +1 -1
  100. package/dist/version.d.ts.map +1 -1
  101. package/dist/version.js +1 -1
  102. package/dist/version.js.map +1 -1
  103. package/extensions/openclaw-plugin/openclaw.plugin.json +46 -46
  104. package/extensions/openclaw-plugin/package.json +14 -14
  105. package/openclaw.plugin.json +45 -45
  106. package/package.json +75 -75
package/dist/src/cli.js CHANGED
@@ -31,7 +31,7 @@ import * as os from 'os';
31
31
  import { fileURLToPath } from 'node:url';
32
32
  import { execFileSync, execSync, spawn } from 'child_process';
33
33
  import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
34
- import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, computeSchemaFit, Layer, DECISION_HALF_LIFE_DAYS, } from './memory.js';
34
+ import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, computeSchemaFit, Layer, } from './memory.js';
35
35
  import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, RECALL_DEFAULT_DENY_SCOPES, } from './store.js';
36
36
  import { markRetrieved, hybridSearch, physicsSearch, explainMatch, textOverlap } from './search.js';
37
37
  import { renderTraceContent, parseSteps } from './trace.js';
@@ -58,6 +58,14 @@ import { buildCorrectionLatency } from './correction-latency.js';
58
58
  import * as api from './api.js';
59
59
  import * as predictionsModule from './predictions.js';
60
60
  import { computePlanningFallacyOutput } from './predictions.js';
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';
61
69
  import { createHash } from 'node:crypto';
62
70
  import { detectAnchoring, hashQueryText, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from './recall-history.js';
63
71
  import { detectAvailabilityBias } from './availability.js';
@@ -91,6 +99,7 @@ import { runFeatureEval, formatResult, resultToBaseline, detectRegressions } fro
91
99
  import { refineStore } from './refine-llm.js';
92
100
  import { wmPush, wmRead, wmClear, wmFlush } from './working-memory.js';
93
101
  import { multihopSearch } from './multihop.js';
102
+ import { graphExpandRecall, MAX_HOPS, DEFAULT_MAX_NEIGHBORS } from './graph-recall.js';
94
103
  import { getReranker } from './rerankers/index.js';
95
104
  import { computeSalience } from './salience.js';
96
105
  import { computeAmbientState, renderAmbientSummary } from './ambient.js';
@@ -218,8 +227,8 @@ function parseArgs(argv) {
218
227
  i++;
219
228
  }
220
229
  else {
221
- // Check if it's a repeatable flag (tag, artifact)
222
- 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') {
223
232
  if (Array.isArray(flags[key])) {
224
233
  flags[key].push(next);
225
234
  }
@@ -805,6 +814,51 @@ async function cmdRecall(hippoRoot, query, flags) {
805
814
  includeSuperseded, asOf,
806
815
  });
807
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
+ }
808
862
  // ACC EVC-adaptive recall (RESEARCH.md §PFC.ACC). When the initial top-K is
809
863
  // dominated by lexically similar but distinct memories (high pairwise token
810
864
  // overlap = same topic, different facts = conflict), allocate extra retrieval
@@ -1322,6 +1376,9 @@ async function cmdRecall(hippoRoot, query, flags) {
1322
1376
  base.superseded = true;
1323
1377
  base.superseded_by = r.entry.superseded_by;
1324
1378
  }
1379
+ if (r.graphVia) {
1380
+ base.graphVia = r.graphVia;
1381
+ }
1325
1382
  if (showWhy) {
1326
1383
  const explanation = explainMatch(query, r);
1327
1384
  base.confidence = resolveConfidence(r.entry);
@@ -1435,8 +1492,9 @@ async function cmdRecall(hippoRoot, query, flags) {
1435
1492
  const isGlobal = isInitialized(globalRoot) && !localIndex.entries[e.id];
1436
1493
  const globalMark = isGlobal ? ' [global]' : '';
1437
1494
  const supersededMark = e.superseded_by ? ' [superseded]' : '';
1495
+ const graphMark = r.graphVia ? ` [graph: ${r.graphVia.hops}hop ${r.graphVia.relType}]` : '';
1438
1496
  const sourceMark = isGlobal ? ' [global]' : ' [local]';
1439
- 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)}`);
1440
1498
  console.log(` [${strengthBar}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`);
1441
1499
  if (showWhy) {
1442
1500
  const explanation = explainMatch(query, r);
@@ -3061,246 +3119,1461 @@ function cmdHandoff(hippoRoot, args, flags) {
3061
3119
  }
3062
3120
  return;
3063
3121
  }
3064
- if (flags['json']) {
3065
- console.log(JSON.stringify({ handoff }, null, 2));
3122
+ if (flags['json']) {
3123
+ console.log(JSON.stringify({ handoff }, null, 2));
3124
+ return;
3125
+ }
3126
+ printHandoff(handoff);
3127
+ return;
3128
+ }
3129
+ if (subcommand === 'show') {
3130
+ const idArg = args[1];
3131
+ if (!idArg) {
3132
+ console.error('Usage: hippo handoff show <id> [--json]');
3133
+ process.exit(1);
3134
+ }
3135
+ const handoffId = parseInt(idArg, 10);
3136
+ if (!Number.isFinite(handoffId) || handoffId <= 0) {
3137
+ console.error(`Invalid handoff ID: ${idArg}`);
3138
+ process.exit(1);
3139
+ }
3140
+ const handoff = loadHandoffById(hippoRoot, resolveTenantId({}), handoffId);
3141
+ if (!handoff) {
3142
+ if (flags['json']) {
3143
+ console.log(JSON.stringify({ handoff: null }));
3144
+ }
3145
+ else {
3146
+ console.log(`No handoff found with ID ${handoffId}.`);
3147
+ }
3148
+ return;
3149
+ }
3150
+ if (flags['json']) {
3151
+ console.log(JSON.stringify({ handoff }, null, 2));
3152
+ return;
3153
+ }
3154
+ printHandoff(handoff);
3155
+ return;
3156
+ }
3157
+ console.error('Usage: hippo handoff <create|latest|show>');
3158
+ process.exit(1);
3159
+ }
3160
+ // ---------------------------------------------------------------------------
3161
+ // E2 prediction first-class object (v0.31)
3162
+ // docs/plans/2026-05-26-e2-prediction-object.md
3163
+ // ---------------------------------------------------------------------------
3164
+ function cmdPredict(hippoRoot, args, flags) {
3165
+ requireInit(hippoRoot);
3166
+ const tenantId = resolveTenantId({});
3167
+ const subcommand = args[0] ?? '';
3168
+ if (subcommand === 'close') {
3169
+ const idRaw = args[1];
3170
+ if (!idRaw) {
3171
+ console.error('Usage: hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
3172
+ process.exit(1);
3173
+ }
3174
+ const id = parseInt(String(idRaw), 10);
3175
+ if (!Number.isFinite(id) || id <= 0) {
3176
+ console.error(`Invalid prediction id: "${idRaw}"`);
3177
+ process.exit(1);
3178
+ }
3179
+ const stateRaw = typeof flags['state'] === 'string' ? flags['state'].trim() : '';
3180
+ if (!predictionsModule.VALID_CLOSURE_STATES.has(stateRaw) || stateRaw === 'open') {
3181
+ console.error(`Invalid --state: "${stateRaw}". Must be one of: closed | closed-unknown.`);
3182
+ process.exit(1);
3183
+ }
3184
+ const actualRaw = flags['actual'];
3185
+ const actualValue = actualRaw !== undefined ? Number(actualRaw) : undefined;
3186
+ if (actualRaw !== undefined && !Number.isFinite(actualValue)) {
3187
+ console.error(`Invalid --actual: "${actualRaw}". Must be a number.`);
3188
+ process.exit(1);
3189
+ }
3190
+ const noteRaw = flags['note'];
3191
+ const closureNote = typeof noteRaw === 'string' ? noteRaw : undefined;
3192
+ const closed = predictionsModule.closePrediction(hippoRoot, tenantId, id, {
3193
+ closureState: stateRaw,
3194
+ actualValue,
3195
+ closureNote,
3196
+ });
3197
+ console.log(`Prediction ${closed.id} closed: state=${closed.closureState}${closed.actualValue !== null ? ` actual=${closed.actualValue}` : ''}`);
3198
+ return;
3199
+ }
3200
+ if (subcommand === 'list') {
3201
+ const classTagRaw = flags['class'];
3202
+ const classTag = typeof classTagRaw === 'string' ? classTagRaw.trim() : '';
3203
+ const statusRaw = flags['status'];
3204
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
3205
+ const limitRaw = flags['limit'];
3206
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
3207
+ if (!Number.isFinite(limit) || limit <= 0) {
3208
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3209
+ process.exit(1);
3210
+ }
3211
+ let results;
3212
+ if (status === 'open') {
3213
+ results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, {
3214
+ classTag: classTag || undefined,
3215
+ limit,
3216
+ });
3217
+ }
3218
+ else if (status === 'all') {
3219
+ // No closure-state filter; pull both via loadPredictionsByClass if class given
3220
+ if (classTag) {
3221
+ results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, { limit });
3222
+ }
3223
+ else {
3224
+ // No class filter + status=all = pull open + closed across all classes
3225
+ // (kept simple: report open via loadOpenPredictions; closed via two
3226
+ // class scans isn't symmetrical. v1 callers typically pass --class.)
3227
+ results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, { limit });
3228
+ }
3229
+ }
3230
+ else {
3231
+ if (!predictionsModule.VALID_CLOSURE_STATES.has(status)) {
3232
+ console.error(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
3233
+ process.exit(1);
3234
+ }
3235
+ if (classTag) {
3236
+ results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, {
3237
+ closureState: status,
3238
+ limit,
3239
+ });
3240
+ }
3241
+ else {
3242
+ // status filter without class — scan all classes is more complex; v1 requires --class for non-default status
3243
+ console.error('--status filter (non-open) requires --class to be set.');
3244
+ process.exit(1);
3245
+ }
3246
+ }
3247
+ if (results.length === 0) {
3248
+ console.log(classTag ? `No predictions in class "${classTag}".` : 'No predictions.');
3249
+ return;
3250
+ }
3251
+ console.log(`Found ${results.length} predictions:\n`);
3252
+ for (const p of results) {
3253
+ const estPart = p.estimateValue !== null ? ` estimate=${p.estimateValue}${p.estimateUnit ? ` ${p.estimateUnit}` : ''}` : '';
3254
+ const actPart = p.actualValue !== null ? ` actual=${p.actualValue}` : '';
3255
+ const tgtPart = p.targetDate ? ` target=${p.targetDate}` : '';
3256
+ console.log(`#${p.id} [${p.closureState}] class=${p.classTag}${estPart}${actPart}${tgtPart}`);
3257
+ console.log(` ${p.claimText}`);
3258
+ if (p.closureNote)
3259
+ console.log(` note: ${p.closureNote}`);
3260
+ }
3261
+ return;
3262
+ }
3263
+ if (subcommand === 'show') {
3264
+ const idRaw = args[1];
3265
+ if (!idRaw) {
3266
+ console.error('Usage: hippo predict show <id>');
3267
+ process.exit(1);
3268
+ }
3269
+ const id = parseInt(String(idRaw), 10);
3270
+ if (!Number.isFinite(id) || id <= 0) {
3271
+ console.error(`Invalid prediction id: "${idRaw}"`);
3272
+ process.exit(1);
3273
+ }
3274
+ const pred = predictionsModule.loadPredictionById(hippoRoot, tenantId, id);
3275
+ if (!pred) {
3276
+ console.error(`Prediction ${id} not found.`);
3277
+ process.exit(1);
3278
+ }
3279
+ console.log(`Prediction #${pred.id}`);
3280
+ console.log(` class: ${pred.classTag}`);
3281
+ console.log(` claim: ${pred.claimText}`);
3282
+ console.log(` state: ${pred.closureState}`);
3283
+ if (pred.estimateValue !== null)
3284
+ console.log(` estimate: ${pred.estimateValue}${pred.estimateUnit ? ' ' + pred.estimateUnit : ''}`);
3285
+ if (pred.targetDate)
3286
+ console.log(` target: ${pred.targetDate}`);
3287
+ if (pred.actualValue !== null)
3288
+ console.log(` actual: ${pred.actualValue}`);
3289
+ if (pred.closedAt)
3290
+ console.log(` closed: ${pred.closedAt}`);
3291
+ if (pred.closureNote)
3292
+ console.log(` note: ${pred.closureNote}`);
3293
+ if (pred.memoryId)
3294
+ console.log(` memory: ${pred.memoryId}`);
3295
+ console.log(` created: ${pred.createdAt}`);
3296
+ return;
3297
+ }
3298
+ if (subcommand === 'baserate') {
3299
+ // J3 reference-class / planning-fallacy detector
3300
+ const classTagRaw = flags['class'];
3301
+ if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3302
+ console.error('Usage: hippo predict baserate --class <c>');
3303
+ process.exit(1);
3304
+ }
3305
+ const baserate = predictionsModule.computePredictionBaserate(hippoRoot, tenantId, classTagRaw.trim());
3306
+ if (baserate.nClosed === 0) {
3307
+ console.log(`No closed predictions in class "${baserate.classTag}" yet.`);
3308
+ console.log(` Create one with: hippo predict "<claim>" --class ${baserate.classTag} --estimate N`);
3309
+ console.log(` Close it later: hippo predict close <id> --state closed --actual N`);
3310
+ return;
3311
+ }
3312
+ console.log(baserate.summary);
3313
+ console.log(` n_closed: ${baserate.nClosed}`);
3314
+ console.log(` n_ratio_eligible: ${baserate.nRatioEligible}`);
3315
+ if (baserate.meanEstimate !== null)
3316
+ console.log(` mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
3317
+ if (baserate.meanActual !== null)
3318
+ console.log(` mean_actual: ${baserate.meanActual.toFixed(3)}`);
3319
+ if (baserate.meanRatio !== null)
3320
+ console.log(` mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
3321
+ if (baserate.p50Ratio !== null)
3322
+ console.log(` p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
3323
+ if (baserate.mae !== null)
3324
+ console.log(` mae: ${baserate.mae.toFixed(3)}`);
3325
+ return;
3326
+ }
3327
+ // Default subcommand: create. args[0] is the claim text.
3328
+ const claimText = subcommand;
3329
+ if (!claimText) {
3330
+ console.error('Usage: hippo predict "<claim>" --class <c> [--estimate <v>] [--unit <u>] [--target <YYYY-MM-DD>]');
3331
+ console.error(' hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
3332
+ console.error(' hippo predict list [--class X] [--status open|closed|closed-unknown|all] [--limit N]');
3333
+ console.error(' hippo predict show <id>');
3334
+ process.exit(1);
3335
+ }
3336
+ const classTagRaw = flags['class'];
3337
+ if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3338
+ console.error('--class is required for prediction creation.');
3339
+ process.exit(1);
3340
+ }
3341
+ const classTag = classTagRaw.trim();
3342
+ const estimateRaw = flags['estimate'];
3343
+ const estimateValue = estimateRaw !== undefined ? Number(estimateRaw) : undefined;
3344
+ if (estimateRaw !== undefined && !Number.isFinite(estimateValue)) {
3345
+ console.error(`Invalid --estimate: "${estimateRaw}". Must be a number.`);
3346
+ process.exit(1);
3347
+ }
3348
+ const unitRaw = flags['unit'];
3349
+ const estimateUnit = typeof unitRaw === 'string' ? unitRaw : undefined;
3350
+ const targetRaw = flags['target'];
3351
+ const targetDate = typeof targetRaw === 'string' ? targetRaw : undefined;
3352
+ const created = predictionsModule.savePrediction(hippoRoot, tenantId, {
3353
+ classTag,
3354
+ claimText,
3355
+ estimateValue,
3356
+ estimateUnit,
3357
+ targetDate,
3358
+ });
3359
+ console.log(`Prediction recorded: #${created.id} class=${created.classTag}`);
3360
+ if (created.memoryId)
3361
+ console.log(` memory: ${created.memoryId}`);
3362
+ }
3363
+ function cmdDecide(hippoRoot, args, flags) {
3364
+ requireInit(hippoRoot);
3365
+ const tenantId = resolveTenantId({});
3366
+ const subcommand = args[0] ?? '';
3367
+ if (subcommand === 'list') {
3368
+ const statusRaw = flags['status'];
3369
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
3370
+ const limitRaw = flags['limit'];
3371
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
3372
+ if (!Number.isFinite(limit) || limit <= 0) {
3373
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3374
+ process.exit(1);
3375
+ }
3376
+ let results;
3377
+ if (status === 'all') {
3378
+ results = decisionsModule.loadDecisions(hippoRoot, tenantId, { limit });
3379
+ }
3380
+ else {
3381
+ if (!decisionsModule.VALID_DECISION_STATES.has(status)) {
3382
+ console.error(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
3383
+ process.exit(1);
3384
+ }
3385
+ results = decisionsModule.loadDecisions(hippoRoot, tenantId, {
3386
+ status: status,
3387
+ limit,
3388
+ });
3389
+ }
3390
+ if (results.length === 0) {
3391
+ console.log('No decisions.');
3392
+ return;
3393
+ }
3394
+ console.log(`Found ${results.length} decisions:\n`);
3395
+ for (const d of results) {
3396
+ const supPart = d.supersededBy !== null ? ` superseded_by=#${d.supersededBy}` : '';
3397
+ console.log(`#${d.id} [${d.status}]${supPart} memory=${d.memoryId ?? '-'}`);
3398
+ console.log(` ${d.decisionText}`);
3399
+ if (d.context)
3400
+ console.log(` context: ${d.context}`);
3401
+ }
3402
+ return;
3403
+ }
3404
+ if (subcommand === 'get') {
3405
+ const idRaw = args[1];
3406
+ if (!idRaw) {
3407
+ console.error('Usage: hippo decide get <id>');
3408
+ process.exit(1);
3409
+ }
3410
+ const id = parseInt(String(idRaw), 10);
3411
+ if (!Number.isFinite(id) || id <= 0) {
3412
+ console.error(`Invalid decision id: "${idRaw}"`);
3413
+ process.exit(1);
3414
+ }
3415
+ const decision = decisionsModule.loadDecisionById(hippoRoot, tenantId, id);
3416
+ if (!decision) {
3417
+ console.error(`Decision ${id} not found.`);
3418
+ process.exit(1);
3419
+ }
3420
+ console.log(`Decision #${decision.id}`);
3421
+ console.log(` status: ${decision.status}`);
3422
+ console.log(` text: ${decision.decisionText}`);
3423
+ if (decision.context)
3424
+ console.log(` context: ${decision.context}`);
3425
+ if (decision.supersededBy !== null)
3426
+ console.log(` superseded_by: #${decision.supersededBy}`);
3427
+ if (decision.supersededAt)
3428
+ console.log(` superseded_at: ${decision.supersededAt}`);
3429
+ if (decision.closedAt)
3430
+ console.log(` closed_at: ${decision.closedAt}`);
3431
+ if (decision.memoryId)
3432
+ console.log(` memory: ${decision.memoryId}`);
3433
+ console.log(` created: ${decision.createdAt}`);
3434
+ return;
3435
+ }
3436
+ if (subcommand === 'close') {
3437
+ const idRaw = args[1];
3438
+ if (!idRaw) {
3439
+ console.error('Usage: hippo decide close <id>');
3440
+ process.exit(1);
3441
+ }
3442
+ const id = parseInt(String(idRaw), 10);
3443
+ if (!Number.isFinite(id) || id <= 0) {
3444
+ console.error(`Invalid decision id: "${idRaw}"`);
3445
+ process.exit(1);
3446
+ }
3447
+ const closed = decisionsModule.closeDecision(hippoRoot, tenantId, id);
3448
+ console.log(`Decision #${closed.id} closed.`);
3449
+ return;
3450
+ }
3451
+ // Default subcommand: create. args[0] is the decision text.
3452
+ const decisionText = subcommand;
3453
+ if (!decisionText) {
3454
+ console.error('Usage: hippo decide "<decision>" [--context "<why>"] [--supersedes <memory-id>]');
3455
+ console.error(' hippo decide list [--status active|superseded|closed|all] [--limit N]');
3456
+ console.error(' hippo decide get <id>');
3457
+ console.error(' hippo decide close <id>');
3458
+ process.exit(1);
3459
+ }
3460
+ const contextRaw = flags['context'];
3461
+ const context = typeof contextRaw === 'string' && contextRaw ? contextRaw : undefined;
3462
+ // A value-less `--supersedes` (parseArgs stores boolean true) is a malformed
3463
+ // request: the user asked to supersede but gave no memory id. Reject it rather
3464
+ // than silently creating a non-superseding decision (codex review 2026-05-28).
3465
+ if (flags['supersedes'] === true) {
3466
+ console.error('--supersedes requires a memory id, e.g. hippo decide "<text>" --supersedes mem_abc123.');
3467
+ process.exit(1);
3468
+ }
3469
+ const supersedesMemId = typeof flags['supersedes'] === 'string' ? flags['supersedes'] : null;
3470
+ // Backward-compat: --supersedes takes a MEMORY id. Validate it exists and
3471
+ // resolve it to the active decision row (if any). Grill fix: commit the
3472
+ // canonical table create+supersede FIRST (inside saveDecision's SAVEPOINT),
3473
+ // weaken the old memory LAST (best-effort) so a memory-write failure cannot
3474
+ // leave the memory stale without the table reflecting the supersession.
3475
+ let supersedesDecisionId;
3476
+ let oldEntry = null;
3477
+ if (supersedesMemId) {
3478
+ oldEntry = readEntry(hippoRoot, supersedesMemId, tenantId) ?? null;
3479
+ if (!oldEntry) {
3480
+ console.error(`Memory ${supersedesMemId} not found.`);
3481
+ process.exit(1);
3482
+ }
3483
+ supersedesDecisionId =
3484
+ decisionsModule.resolveActiveDecisionIdByMemory(hippoRoot, tenantId, supersedesMemId) ?? undefined;
3485
+ }
3486
+ const decisionPathTags = extractPathTags(process.cwd());
3487
+ const created = decisionsModule.saveDecision(hippoRoot, tenantId, {
3488
+ decisionText,
3489
+ context,
3490
+ supersedesDecisionId,
3491
+ extraTags: decisionPathTags,
3492
+ });
3493
+ // Legacy memory-weaken (best-effort, LAST): half-life halved, marked stale +
3494
+ // 'superseded' tag. Preserves the exact pre-promotion behavior for the memory
3495
+ // mirror; the canonical table supersession already committed above.
3496
+ if (oldEntry) {
3497
+ // Best-effort: saveDecision already committed the canonical mutation (new
3498
+ // decision created + old row superseded). If this legacy memory-weaken
3499
+ // throws, do NOT fail the command — a retry would find no active decision
3500
+ // for the old memory and create a duplicate active successor. Warn instead
3501
+ // (codex review 2026-05-28).
3502
+ try {
3503
+ oldEntry.half_life_days = Math.max(1, Math.floor(oldEntry.half_life_days / 2));
3504
+ oldEntry.confidence = 'stale';
3505
+ if (!oldEntry.tags.includes('superseded'))
3506
+ oldEntry.tags.push('superseded');
3507
+ writeEntry(hippoRoot, oldEntry);
3508
+ }
3509
+ catch (e) {
3510
+ console.error(` warning: decision recorded and superseded, but failed to weaken the prior memory ${supersedesMemId}: ${e.message}`);
3511
+ }
3512
+ }
3513
+ console.log(`Decision recorded: #${created.id}`);
3514
+ if (created.memoryId)
3515
+ console.log(` memory: ${created.memoryId}`);
3516
+ if (supersedesMemId) {
3517
+ const tail = supersedesDecisionId !== undefined
3518
+ ? ` (decision #${supersedesDecisionId} superseded)`
3519
+ : ' (no active decision row; memory weakened only)';
3520
+ console.log(` supersedes memory: ${supersedesMemId}${tail}`);
3521
+ }
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.');
3066
4272
  return;
3067
4273
  }
3068
- printHandoff(handoff);
4274
+ console.log(`Found ${results.length} project briefs:\n`);
4275
+ for (const b of results)
4276
+ printBriefRow(b);
3069
4277
  return;
3070
4278
  }
3071
- if (subcommand === 'show') {
3072
- const idArg = args[1];
3073
- if (!idArg) {
3074
- console.error('Usage: hippo handoff show <id> [--json]');
4279
+ if (subcommand === 'refresh') {
4280
+ const repoRaw = args[1];
4281
+ if (!repoRaw) {
4282
+ console.error('Usage: hippo brief refresh "<repo>" [--dry-run]');
3075
4283
  process.exit(1);
3076
4284
  }
3077
- const handoffId = parseInt(idArg, 10);
3078
- if (!Number.isFinite(handoffId) || handoffId <= 0) {
3079
- console.error(`Invalid handoff ID: ${idArg}`);
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);
3080
4302
  process.exit(1);
3081
4303
  }
3082
- const handoff = loadHandoffById(hippoRoot, resolveTenantId({}), handoffId);
3083
- if (!handoff) {
3084
- if (flags['json']) {
3085
- console.log(JSON.stringify({ handoff: null }));
3086
- }
3087
- else {
3088
- console.log(`No handoff found with ID ${handoffId}.`);
3089
- }
3090
- return;
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);
3091
4311
  }
3092
- if (flags['json']) {
3093
- console.log(JSON.stringify({ handoff }, null, 2));
3094
- return;
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);
3095
4317
  }
3096
- printHandoff(handoff);
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}`);
3097
4334
  return;
3098
4335
  }
3099
- console.error('Usage: hippo handoff <create|latest|show>');
3100
- process.exit(1);
3101
- }
3102
- // ---------------------------------------------------------------------------
3103
- // E2 prediction first-class object (v0.31)
3104
- // docs/plans/2026-05-26-e2-prediction-object.md
3105
- // ---------------------------------------------------------------------------
3106
- function cmdPredict(hippoRoot, args, flags) {
3107
- requireInit(hippoRoot);
3108
- const tenantId = resolveTenantId({});
3109
- const subcommand = args[0] ?? '';
3110
- if (subcommand === 'close') {
4336
+ if (subcommand === 'supersede') {
3111
4337
  const idRaw = args[1];
3112
4338
  if (!idRaw) {
3113
- console.error('Usage: hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
4339
+ console.error('Usage: hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
3114
4340
  process.exit(1);
3115
4341
  }
3116
- const id = parseInt(String(idRaw), 10);
3117
- if (!Number.isFinite(id) || id <= 0) {
3118
- console.error(`Invalid prediction id: "${idRaw}"`);
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.');
3119
4346
  process.exit(1);
3120
4347
  }
3121
- const stateRaw = typeof flags['state'] === 'string' ? flags['state'].trim() : '';
3122
- if (!predictionsModule.VALID_CLOSURE_STATES.has(stateRaw) || stateRaw === 'open') {
3123
- console.error(`Invalid --state: "${stateRaw}". Must be one of: closed | closed-unknown.`);
4348
+ const existing = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
4349
+ if (!existing) {
4350
+ console.error(`Project brief ${id} not found.`);
3124
4351
  process.exit(1);
3125
4352
  }
3126
- const actualRaw = flags['actual'];
3127
- const actualValue = actualRaw !== undefined ? Number(actualRaw) : undefined;
3128
- if (actualRaw !== undefined && !Number.isFinite(actualValue)) {
3129
- console.error(`Invalid --actual: "${actualRaw}". Must be a number.`);
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);
3130
4368
  process.exit(1);
3131
4369
  }
3132
- const noteRaw = flags['note'];
3133
- const closureNote = typeof noteRaw === 'string' ? noteRaw : undefined;
3134
- const closed = predictionsModule.closePrediction(hippoRoot, tenantId, id, {
3135
- closureState: stateRaw,
3136
- actualValue,
3137
- closureNote,
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()),
3138
4396
  });
3139
- console.log(`Prediction ${closed.id} closed: state=${closed.closureState}${closed.actualValue !== null ? ` actual=${closed.actualValue}` : ''}`);
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
+ }
3140
4440
  return;
3141
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] ?? '';
3142
4449
  if (subcommand === 'list') {
3143
- const classTagRaw = flags['class'];
3144
- const classTag = typeof classTagRaw === 'string' ? classTagRaw.trim() : '';
3145
4450
  const statusRaw = flags['status'];
3146
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;
3147
4454
  const limitRaw = flags['limit'];
3148
4455
  const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
3149
4456
  if (!Number.isFinite(limit) || limit <= 0) {
3150
4457
  console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
3151
4458
  process.exit(1);
3152
4459
  }
3153
- let results;
3154
- if (status === 'open') {
3155
- results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, {
3156
- classTag: classTag || undefined,
3157
- limit,
3158
- });
3159
- }
3160
- else if (status === 'all') {
3161
- // No closure-state filter; pull both via loadPredictionsByClass if class given
3162
- if (classTag) {
3163
- results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, { limit });
3164
- }
3165
- else {
3166
- // No class filter + status=all = pull open + closed across all classes
3167
- // (kept simple: report open via loadOpenPredictions; closed via two
3168
- // class scans isn't symmetrical. v1 callers typically pass --class.)
3169
- results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, { limit });
3170
- }
3171
- }
3172
- else {
3173
- if (!predictionsModule.VALID_CLOSURE_STATES.has(status)) {
3174
- console.error(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
3175
- process.exit(1);
3176
- }
3177
- if (classTag) {
3178
- results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, {
3179
- closureState: status,
3180
- limit,
3181
- });
3182
- }
3183
- else {
3184
- // status filter without class — scan all classes is more complex; v1 requires --class for non-default status
3185
- console.error('--status filter (non-open) requires --class to be set.');
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.`);
3186
4464
  process.exit(1);
3187
4465
  }
4466
+ opts.status = status;
3188
4467
  }
4468
+ const results = customerNotesModule.loadCustomerNotes(hippoRoot, tenantId, opts);
3189
4469
  if (results.length === 0) {
3190
- console.log(classTag ? `No predictions in class "${classTag}".` : 'No predictions.');
4470
+ console.log('No customer notes.');
3191
4471
  return;
3192
4472
  }
3193
- console.log(`Found ${results.length} predictions:\n`);
3194
- for (const p of results) {
3195
- const estPart = p.estimateValue !== null ? ` estimate=${p.estimateValue}${p.estimateUnit ? ` ${p.estimateUnit}` : ''}` : '';
3196
- const actPart = p.actualValue !== null ? ` actual=${p.actualValue}` : '';
3197
- const tgtPart = p.targetDate ? ` target=${p.targetDate}` : '';
3198
- console.log(`#${p.id} [${p.closureState}] class=${p.classTag}${estPart}${actPart}${tgtPart}`);
3199
- console.log(` ${p.claimText}`);
3200
- if (p.closureNote)
3201
- console.log(` note: ${p.closureNote}`);
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);
3202
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}`);
3203
4506
  return;
3204
4507
  }
3205
- if (subcommand === 'show') {
4508
+ if (subcommand === 'supersede') {
3206
4509
  const idRaw = args[1];
3207
4510
  if (!idRaw) {
3208
- console.error('Usage: hippo predict show <id>');
4511
+ console.error('Usage: hippo note supersede <id> --text "<note>" [--change "<summary>"]');
3209
4512
  process.exit(1);
3210
4513
  }
3211
- const id = parseInt(String(idRaw), 10);
3212
- if (!Number.isFinite(id) || id <= 0) {
3213
- console.error(`Invalid prediction id: "${idRaw}"`);
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.');
3214
4518
  process.exit(1);
3215
4519
  }
3216
- const pred = predictionsModule.loadPredictionById(hippoRoot, tenantId, id);
3217
- if (!pred) {
3218
- console.error(`Prediction ${id} not found.`);
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);
3219
4540
  process.exit(1);
3220
4541
  }
3221
- console.log(`Prediction #${pred.id}`);
3222
- console.log(` class: ${pred.classTag}`);
3223
- console.log(` claim: ${pred.claimText}`);
3224
- console.log(` state: ${pred.closureState}`);
3225
- if (pred.estimateValue !== null)
3226
- console.log(` estimate: ${pred.estimateValue}${pred.estimateUnit ? ' ' + pred.estimateUnit : ''}`);
3227
- if (pred.targetDate)
3228
- console.log(` target: ${pred.targetDate}`);
3229
- if (pred.actualValue !== null)
3230
- console.log(` actual: ${pred.actualValue}`);
3231
- if (pred.closedAt)
3232
- console.log(` closed: ${pred.closedAt}`);
3233
- if (pred.closureNote)
3234
- console.log(` note: ${pred.closureNote}`);
3235
- if (pred.memoryId)
3236
- console.log(` memory: ${pred.memoryId}`);
3237
- console.log(` created: ${pred.createdAt}`);
3238
4542
  return;
3239
4543
  }
3240
- if (subcommand === 'baserate') {
3241
- // J3 reference-class / planning-fallacy detector
3242
- const classTagRaw = flags['class'];
3243
- if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3244
- console.error('Usage: hippo predict baserate --class <c>');
4544
+ if (subcommand === 'close') {
4545
+ const idRaw = args[1];
4546
+ if (!idRaw) {
4547
+ console.error('Usage: hippo note close <id>');
3245
4548
  process.exit(1);
3246
4549
  }
3247
- const baserate = predictionsModule.computePredictionBaserate(hippoRoot, tenantId, classTagRaw.trim());
3248
- if (baserate.nClosed === 0) {
3249
- console.log(`No closed predictions in class "${baserate.classTag}" yet.`);
3250
- console.log(` Create one with: hippo predict "<claim>" --class ${baserate.classTag} --estimate N`);
3251
- console.log(` Close it later: hippo predict close <id> --state closed --actual N`);
3252
- return;
3253
- }
3254
- console.log(baserate.summary);
3255
- console.log(` n_closed: ${baserate.nClosed}`);
3256
- console.log(` n_ratio_eligible: ${baserate.nRatioEligible}`);
3257
- if (baserate.meanEstimate !== null)
3258
- console.log(` mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
3259
- if (baserate.meanActual !== null)
3260
- console.log(` mean_actual: ${baserate.meanActual.toFixed(3)}`);
3261
- if (baserate.meanRatio !== null)
3262
- console.log(` mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
3263
- if (baserate.p50Ratio !== null)
3264
- console.log(` p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
3265
- if (baserate.mae !== null)
3266
- console.log(` mae: ${baserate.mae.toFixed(3)}`);
4550
+ const id = parsePositiveNoteId(idRaw);
4551
+ const closed = customerNotesModule.closeCustomerNote(hippoRoot, tenantId, id);
4552
+ console.log(`Customer note #${closed.id} closed.`);
3267
4553
  return;
3268
4554
  }
3269
- // Default subcommand: create. args[0] is the claim text.
3270
- const claimText = subcommand;
3271
- if (!claimText) {
3272
- console.error('Usage: hippo predict "<claim>" --class <c> [--estimate <v>] [--unit <u>] [--target <YYYY-MM-DD>]');
3273
- console.error(' hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
3274
- console.error(' hippo predict list [--class X] [--status open|closed|closed-unknown|all] [--limit N]');
3275
- console.error(' hippo predict show <id>');
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();
3276
4561
  process.exit(1);
3277
4562
  }
3278
- const classTagRaw = flags['class'];
3279
- if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3280
- console.error('--class is required for prediction creation.');
3281
- process.exit(1);
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}`);
3282
4572
  }
3283
- const classTag = classTagRaw.trim();
3284
- const estimateRaw = flags['estimate'];
3285
- const estimateValue = estimateRaw !== undefined ? Number(estimateRaw) : undefined;
3286
- if (estimateRaw !== undefined && !Number.isFinite(estimateValue)) {
3287
- console.error(`Invalid --estimate: "${estimateRaw}". Must be a number.`);
4573
+ catch (e) {
4574
+ console.error(e.message);
3288
4575
  process.exit(1);
3289
4576
  }
3290
- const unitRaw = flags['unit'];
3291
- const estimateUnit = typeof unitRaw === 'string' ? unitRaw : undefined;
3292
- const targetRaw = flags['target'];
3293
- const targetDate = typeof targetRaw === 'string' ? targetRaw : undefined;
3294
- const created = predictionsModule.savePrediction(hippoRoot, tenantId, {
3295
- classTag,
3296
- claimText,
3297
- estimateValue,
3298
- estimateUnit,
3299
- targetDate,
3300
- });
3301
- console.log(`Prediction recorded: #${created.id} class=${created.classTag}`);
3302
- if (created.memoryId)
3303
- console.log(` memory: ${created.memoryId}`);
3304
4577
  }
3305
4578
  function cmdCurrent(hippoRoot, args, flags) {
3306
4579
  requireInit(hippoRoot);
@@ -4716,6 +5989,27 @@ const VALID_AUDIT_OPS = new Set([
4716
5989
  'recall_anchor_detected_memory_dominance', // v0.33 / J1 — emitted by detector on R2 fire
4717
5990
  'recall_anchor_skipped_no_session', // v0.33 / J1 — telemetry: no sessionId, ring skipped
4718
5991
  'recall_availability_detected', // v1.13.x / J2 - emitted when availability/recency-bias hint fires
5992
+ 'decision_create', // E2 decision first-class object — emitted by saveDecision
5993
+ 'decision_supersede', // E2 — emitted by saveDecision when --supersedes resolves to an active decision row
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
4719
6013
  ]);
4720
6014
  function formatAuditRow(ev) {
4721
6015
  const target = ev.targetId ?? '-';
@@ -5275,6 +6569,13 @@ Commands:
5275
6569
  --min-results <n> Minimum results regardless of budget (default: 1)
5276
6570
  --json Output as JSON
5277
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).
5278
6579
  --no-mmr Disable MMR diversity re-ranking
5279
6580
  --mmr-lambda <f> MMR balance 0..1 (default: 0.7, 1.0 = pure relevance)
5280
6581
  --evc-adaptive ACC-style: when top-K shows high inter-item overlap
@@ -5502,9 +6803,82 @@ Commands:
5502
6803
  claude-code/opencode install SessionEnd+SessionStart;
5503
6804
  codex wraps the detected launcher in place
5504
6805
  hook uninstall <target> Remove hook
5505
- decide "<decision>" Record an architectural decision (90-day half-life)
6806
+ decide "<decision>" Record a decision (first-class object + memory mirror)
5506
6807
  --context "<why>" Why this decision was made
5507
- --supersedes <id> Supersede a previous decision (weakens it)
6808
+ --supersedes <mem-id> Supersede the decision backed by this memory id
6809
+ decide list [--status active|superseded|closed|all] [--limit N]
6810
+ List decisions (table is authoritative, survives decay)
6811
+ decide get <id> Show a decision by its table id
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
5508
6882
  invalidate "<pattern>" Actively weaken memories matching an old pattern
5509
6883
  --reason "<why>" Optional: what replaced it
5510
6884
  wm <sub> Working memory — bounded buffer for current state
@@ -5586,6 +6960,17 @@ Examples:
5586
6960
  hippo setup
5587
6961
  hippo hook install claude-code
5588
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
5589
6974
  hippo invalidate "REST API" --reason "migrated to GraphQL"
5590
6975
  hippo export memories.json
5591
6976
  hippo export --format markdown memories.md
@@ -6189,52 +7574,32 @@ async function main() {
6189
7574
  }
6190
7575
  break;
6191
7576
  }
6192
- case 'decide': {
6193
- requireInit(hippoRoot);
6194
- const text = args[0];
6195
- if (!text) {
6196
- console.error('Usage: hippo decide "<decision>" [--context "<why>"] [--supersedes <id>]');
6197
- process.exit(1);
6198
- }
6199
- const context = flags['context'] || '';
6200
- const supersedesId = flags['supersedes'] || null;
6201
- // Build content with context
6202
- const decisionContent = context ? `${text}\n\nContext: ${context}` : text;
6203
- // Handle supersession
6204
- if (supersedesId) {
6205
- const oldEntry = readEntry(hippoRoot, supersedesId, resolveTenantId({}));
6206
- if (!oldEntry) {
6207
- console.error(`Memory ${supersedesId} not found.`);
6208
- process.exit(1);
6209
- }
6210
- oldEntry.half_life_days = Math.max(1, Math.floor(oldEntry.half_life_days / 2));
6211
- oldEntry.confidence = 'stale';
6212
- if (!oldEntry.tags.includes('superseded'))
6213
- oldEntry.tags.push('superseded');
6214
- writeEntry(hippoRoot, oldEntry);
6215
- console.log(`Superseded ${supersedesId} (half-life halved, marked stale)`);
6216
- }
6217
- // Create decision memory
6218
- const mem = createMemory(decisionContent, {
6219
- tags: ['decision'],
6220
- layer: Layer.Semantic,
6221
- confidence: 'verified',
6222
- source: 'decision',
6223
- });
6224
- mem.half_life_days = DECISION_HALF_LIFE_DAYS;
6225
- // Auto-tag with path context
6226
- const decisionPathTags = extractPathTags(process.cwd());
6227
- for (const pt of decisionPathTags) {
6228
- if (!mem.tags.includes(pt))
6229
- mem.tags.push(pt);
6230
- }
6231
- writeEntry(hippoRoot, mem);
6232
- console.log(`Decision recorded: ${mem.id}`);
6233
- if (supersedesId) {
6234
- console.log(` Supersedes: ${supersedesId}`);
6235
- }
7577
+ case 'decide':
7578
+ cmdDecide(hippoRoot, args, flags);
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);
6236
7602
  break;
6237
- }
6238
7603
  case 'help':
6239
7604
  case '--help':
6240
7605
  case '-h':