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