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.
- package/README.md +862 -861
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1594 -229
- package/dist/cli.js.map +1 -1
- package/dist/customer-notes.d.ts +95 -0
- package/dist/customer-notes.d.ts.map +1 -0
- package/dist/customer-notes.js +296 -0
- package/dist/customer-notes.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1286 -472
- package/dist/db.js.map +1 -1
- package/dist/decisions.d.ts +91 -0
- package/dist/decisions.d.ts.map +1 -0
- package/dist/decisions.js +278 -0
- package/dist/decisions.js.map +1 -0
- package/dist/graph-extract.d.ts +39 -0
- package/dist/graph-extract.d.ts.map +1 -0
- package/dist/graph-extract.js +141 -0
- package/dist/graph-extract.js.map +1 -0
- package/dist/graph-recall.d.ts +41 -0
- package/dist/graph-recall.d.ts.map +1 -0
- package/dist/graph-recall.js +246 -0
- package/dist/graph-recall.js.map +1 -0
- package/dist/graph.d.ts +137 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +433 -0
- package/dist/graph.js.map +1 -0
- package/dist/incidents.d.ts +100 -0
- package/dist/incidents.d.ts.map +1 -0
- package/dist/incidents.js +322 -0
- package/dist/incidents.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +6 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +6 -0
- package/dist/memory.js.map +1 -1
- package/dist/policies.d.ts +149 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +380 -0
- package/dist/policies.js.map +1 -0
- package/dist/processes.d.ts +104 -0
- package/dist/processes.d.ts.map +1 -0
- package/dist/processes.js +330 -0
- package/dist/processes.js.map +1 -0
- package/dist/project-briefs.d.ts +126 -0
- package/dist/project-briefs.d.ts.map +1 -0
- package/dist/project-briefs.js +453 -0
- package/dist/project-briefs.js.map +1 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1181 -8
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +98 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +339 -0
- package/dist/skills.js.map +1 -0
- package/dist/src/audit.js.map +1 -1
- package/dist/src/cli.js +1594 -229
- package/dist/src/cli.js.map +1 -1
- package/dist/src/customer-notes.js +296 -0
- package/dist/src/customer-notes.js.map +1 -0
- package/dist/src/db.js +1286 -472
- package/dist/src/db.js.map +1 -1
- package/dist/src/decisions.js +278 -0
- package/dist/src/decisions.js.map +1 -0
- package/dist/src/graph-extract.js +141 -0
- package/dist/src/graph-extract.js.map +1 -0
- package/dist/src/graph-recall.js +246 -0
- package/dist/src/graph-recall.js.map +1 -0
- package/dist/src/graph.js +433 -0
- package/dist/src/graph.js.map +1 -0
- package/dist/src/incidents.js +322 -0
- package/dist/src/incidents.js.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/memory.js +6 -0
- package/dist/src/memory.js.map +1 -1
- package/dist/src/policies.js +380 -0
- package/dist/src/policies.js.map +1 -0
- package/dist/src/processes.js +330 -0
- package/dist/src/processes.js.map +1 -0
- package/dist/src/project-briefs.js +453 -0
- package/dist/src/project-briefs.js.map +1 -0
- package/dist/src/search.js.map +1 -1
- package/dist/src/server.js +1181 -8
- package/dist/src/server.js.map +1 -1
- package/dist/src/skills.js +339 -0
- package/dist/src/skills.js.map +1 -0
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +46 -46
- package/extensions/openclaw-plugin/package.json +14 -14
- package/openclaw.plugin.json +45 -45
- 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,
|
|
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
|
-
|
|
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 === '
|
|
3072
|
-
const
|
|
3073
|
-
if (!
|
|
3074
|
-
console.error('Usage: hippo
|
|
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
|
|
3078
|
-
|
|
3079
|
-
|
|
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
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
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
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4339
|
+
console.error('Usage: hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
|
|
3114
4340
|
process.exit(1);
|
|
3115
4341
|
}
|
|
3116
|
-
const id =
|
|
3117
|
-
|
|
3118
|
-
|
|
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
|
|
3122
|
-
if (!
|
|
3123
|
-
console.error(`
|
|
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
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
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
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
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(`
|
|
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
|
-
|
|
3154
|
-
if (status
|
|
3155
|
-
|
|
3156
|
-
|
|
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(
|
|
4470
|
+
console.log('No customer notes.');
|
|
3191
4471
|
return;
|
|
3192
4472
|
}
|
|
3193
|
-
console.log(`Found ${results.length}
|
|
3194
|
-
for (const
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
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 === '
|
|
4508
|
+
if (subcommand === 'supersede') {
|
|
3206
4509
|
const idRaw = args[1];
|
|
3207
4510
|
if (!idRaw) {
|
|
3208
|
-
console.error('Usage: hippo
|
|
4511
|
+
console.error('Usage: hippo note supersede <id> --text "<note>" [--change "<summary>"]');
|
|
3209
4512
|
process.exit(1);
|
|
3210
4513
|
}
|
|
3211
|
-
const id =
|
|
3212
|
-
|
|
3213
|
-
|
|
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
|
|
3217
|
-
if (!
|
|
3218
|
-
console.error(`
|
|
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 === '
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
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
|
|
3248
|
-
|
|
3249
|
-
|
|
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.
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
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
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
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
|
-
|
|
3284
|
-
|
|
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
|
|
6806
|
+
decide "<decision>" Record a decision (first-class object + memory mirror)
|
|
5506
6807
|
--context "<why>" Why this decision was made
|
|
5507
|
-
--supersedes <id>
|
|
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
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
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':
|