hippo-memory 1.57.0 → 1.59.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 +24 -1
- package/dist/agent-memories/apply.d.ts +1 -1
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/agent-memories/legacy.js +4 -1
- package/dist/api-errors.d.ts +27 -0
- package/dist/api-errors.js +37 -0
- package/dist/api.d.ts +5 -5
- package/dist/api.js +40 -47
- package/dist/audit.d.ts +5 -1
- package/dist/audit.js +13 -0
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +7 -5
- package/dist/capture-contract.d.ts +47 -0
- package/dist/capture-contract.js +49 -0
- package/dist/capture-error.js +2 -1
- package/dist/capture.d.ts +0 -13
- package/dist/capture.js +5 -66
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.js +7 -0
- package/dist/cli/projects.d.ts +4 -0
- package/dist/cli/projects.js +90 -0
- package/dist/cli/shared.js +23 -13
- package/dist/cli/sleep.js +5 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +486 -397
- package/dist/client.js +9 -0
- package/dist/codex-patch.js +1 -1
- package/dist/compaction-record.d.ts +1 -1
- package/dist/compaction-record.js +3 -2
- package/dist/config.d.ts +5 -0
- package/dist/config.js +17 -0
- package/dist/connectors/github/dlq.js +5 -2
- package/dist/connectors/github/octokit-client.js +4 -2
- package/dist/connectors/slack/dlq.js +6 -2
- package/dist/connectors/slack/web-client.js +7 -5
- package/dist/consolidate.d.ts +10 -0
- package/dist/consolidate.js +48 -35
- package/dist/customer-notes.js +14 -13
- package/dist/dag.js +7 -4
- package/dist/dashboard.js +1 -1
- package/dist/db.d.ts +12 -0
- package/dist/db.js +62 -1
- package/dist/decisions.js +9 -8
- package/dist/dedupe.js +1 -1
- package/dist/doctor.js +28 -0
- package/dist/dormant.d.ts +2 -2
- package/dist/embedding-provider.js +3 -3
- package/dist/embeddings.d.ts +4 -4
- package/dist/embeddings.js +72 -16
- package/dist/extract.js +19 -18
- package/dist/http-retry.d.ts +21 -0
- package/dist/http-retry.js +50 -0
- package/dist/http-util.d.ts +8 -0
- package/dist/http-util.js +10 -0
- package/dist/importers.d.ts +2 -0
- package/dist/importers.js +16 -5
- package/dist/incidents.js +11 -10
- package/dist/judgment.js +10 -17
- package/dist/log.d.ts +25 -0
- package/dist/log.js +48 -0
- package/dist/mcp/server.js +52 -24
- package/dist/mcp/tool-args.d.ts +21 -0
- package/dist/mcp/tool-args.js +80 -0
- package/dist/memory.js +3 -2
- package/dist/overlap-index.d.ts +7 -0
- package/dist/overlap-index.js +38 -0
- package/dist/pilot-arm.d.ts +9 -0
- package/dist/pilot-arm.js +47 -0
- package/dist/policies.js +12 -11
- package/dist/predictions.js +9 -8
- package/dist/processes.js +14 -13
- package/dist/project-briefs.js +16 -15
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +25 -1
- package/dist/project-merge.d.ts +52 -0
- package/dist/project-merge.js +168 -0
- package/dist/raw-archive.js +7 -6
- package/dist/recall-scope.d.ts +5 -4
- package/dist/recall-scope.js +7 -5
- package/dist/refine-llm.js +3 -2
- package/dist/reject-flow.js +6 -9
- package/dist/rejection.d.ts +2 -1
- package/dist/rejection.js +2 -1
- package/dist/rerankers/clef.d.ts +29 -0
- package/dist/rerankers/clef.js +182 -0
- package/dist/rerankers/index.js +3 -0
- package/dist/rerankers/jev.d.ts +11 -0
- package/dist/rerankers/jev.js +10 -5
- package/dist/rerankers/types.d.ts +16 -0
- package/dist/search.js +14 -2
- package/dist/secret-detect.d.ts +13 -1
- package/dist/secret-detect.js +33 -1
- package/dist/server.d.ts +9 -2
- package/dist/server.js +188 -411
- package/dist/session-digest.js +2 -1
- package/dist/shared.js +7 -6
- package/dist/skills.js +15 -14
- package/dist/store.js +10 -10
- package/dist/token-ledger.d.ts +4 -2
- package/dist/token-ledger.js +2 -2
- 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 +5 -2
- package/dist/connectors/slack/ratelimit.d.ts +0 -9
- package/dist/connectors/slack/ratelimit.js +0 -18
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* hippo rejections
|
|
21
21
|
* hippo unreject <digest-prefix>
|
|
22
22
|
* hippo dormant [<query>] [--limit <n>] [--json] | restore <id> | forget <id>
|
|
23
|
+
* hippo projects [--json] | merge <from> <into> [--apply] | repair [--apply]
|
|
23
24
|
* hippo tokens [--days <n>] [--json] [--global]
|
|
24
25
|
* hippo doctor [--json]
|
|
25
26
|
* hippo inspect <id>
|
|
@@ -59,7 +60,8 @@ import { resolveEmbeddingProvider } from './embedding-provider.js';
|
|
|
59
60
|
import { loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
|
|
60
61
|
import { computeSystemEnergy, vecNorm } from './physics.js';
|
|
61
62
|
import { loadConfig } from './config.js';
|
|
62
|
-
import {
|
|
63
|
+
import { ensurePilotArm, hashArm, readPilotArm } from './pilot-arm.js';
|
|
64
|
+
import { openHippoDb, closeHippoDb, withSharedStoreHandles, HOOK_DB_WAIT_MS, isSqliteBusy, noteStoreBusy } from './db.js';
|
|
63
65
|
import { runDoctor, formatDoctor } from './doctor.js';
|
|
64
66
|
import { buildSupportBundle, TAIL_MAX_LINES } from './support-bundle.js';
|
|
65
67
|
import { PACKAGE_VERSION } from './version.js';
|
|
@@ -88,6 +90,7 @@ import { listApiKeys } from './auth.js';
|
|
|
88
90
|
import { buildProvenanceCoverage } from './provenance-coverage.js';
|
|
89
91
|
import { buildCorrectionLatency } from './correction-latency.js';
|
|
90
92
|
import * as api from './api.js';
|
|
93
|
+
import { vetSecrets } from './secret-detect.js';
|
|
91
94
|
import * as predictionsModule from './predictions.js';
|
|
92
95
|
import { computePlanningFallacyOutput } from './predictions.js';
|
|
93
96
|
import * as decisionsModule from './decisions.js';
|
|
@@ -133,6 +136,7 @@ import { MAX_HOPS, DEFAULT_MAX_NEIGHBORS } from './graph-recall.js';
|
|
|
133
136
|
import { getReranker } from './rerankers/index.js';
|
|
134
137
|
import { rankRecall } from './recall-pipeline.js';
|
|
135
138
|
import { JEV_DEFAULT_TOP_K } from './rerankers/jev.js';
|
|
139
|
+
import { isClefModel } from './rerankers/clef.js';
|
|
136
140
|
import { computeSalience } from './salience.js';
|
|
137
141
|
import { renderAmbientSummary } from './ambient.js';
|
|
138
142
|
import { assembleCost, assembleHeading, contextCost, contextHeading, contextLine, crossProjectHeading, crossProjectLine, drillCost, handoffText, printedTokens, sessionTrailText, settleTokens, snapshotText, } from './context-render.js';
|
|
@@ -143,6 +147,8 @@ import { backfillChannel } from './connectors/slack/backfill.js';
|
|
|
143
147
|
import { slackHistoryFetcher } from './connectors/slack/web-client.js';
|
|
144
148
|
import { addWorkspace as addSlackWorkspace, listWorkspaces as listSlackWorkspaces, removeWorkspace as removeSlackWorkspace, } from './connectors/slack/workspaces.js';
|
|
145
149
|
import { cmdGithub, printGithubBackfillUsage } from './connectors/github/cli-impl.js';
|
|
150
|
+
import { log } from './log.js';
|
|
151
|
+
import { printError } from './cli/output.js';
|
|
146
152
|
import { parseLimitFlag, parseCountFlag, parseBudgetFlag, emitCliAudit, requireInit, runChurnStaleForRepo, runViaServerIfAvailable, fmt, recallEntryText, recallHeading, printAgentImport, hippoBlock, installCodexMemoryHooks, setupDailySchedule, parseAsOfFlag, engineFlags, collectHandoffEvidence, logSessionEndImport, appendSessionEndCloseLog, printActiveTaskSnapshot, printSessionEvents, printHandoff, cardStringFlag, hostSessionId, resetHookInjection, captureConsole, hookStoreRoot, withLedgerDb, learnFromRepo, HOOK_MARKERS, HOOKS, resolveAuthRoot, } from './cli/shared.js';
|
|
147
153
|
// ---------------------------------------------------------------------------
|
|
148
154
|
// Helpers
|
|
@@ -151,7 +157,7 @@ import { parseLimitFlag, parseCountFlag, parseBudgetFlag, emitCliAudit, requireI
|
|
|
151
157
|
// and as off under === true (`--pin=true` would not pin), so parseArgs and main() refuse one.
|
|
152
158
|
// tests/cli-parse-flag-equals.test.ts fails when a switch read is missing from this set.
|
|
153
159
|
export const BOOLEAN_FLAGS = new Set([
|
|
154
|
-
'agents', 'all', 'all-tenants', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'churn', 'continuity',
|
|
160
|
+
'agents', 'all', 'all-tenants', 'apply', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'churn', 'continuity',
|
|
155
161
|
'cross-project', 'dry-run', 'equal-sources', 'error', 'evc-adaptive', 'extract',
|
|
156
162
|
'filter-conflicts', 'fix', 'force', 'forget', 'git', 'global', 'good', 'graph-stream',
|
|
157
163
|
'help', 'include-logs', 'include-superseded', 'inferred', 'json', 'last-session', 'multihop', 'no-hooks',
|
|
@@ -179,7 +185,7 @@ export const KNOWN_FLAGS = new Set([
|
|
|
179
185
|
]);
|
|
180
186
|
// Commands that delete or hide memories: an unknown flag here stops the run instead of being ignored.
|
|
181
187
|
const DESTRUCTIVE_COMMANDS = new Set([
|
|
182
|
-
'audit', 'dedup', 'forget', 'invalidate', 'reject', 'resolve', 'sleep', 'supersede',
|
|
188
|
+
'audit', 'dedup', 'forget', 'invalidate', 'projects', 'reject', 'resolve', 'sleep', 'supersede',
|
|
183
189
|
]);
|
|
184
190
|
// Commands that honour --dry-run. Any other command would ignore it and run for real.
|
|
185
191
|
const DRY_RUN_COMMANDS = new Set([
|
|
@@ -635,18 +641,18 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
635
641
|
// 'archived' is an internal sentinel set only inside archiveRawMemory's transaction.
|
|
636
642
|
const userVisibleKinds = ['distilled', 'superseded'];
|
|
637
643
|
if (kindFlag !== undefined && !userVisibleKinds.includes(kindFlag)) {
|
|
638
|
-
|
|
639
|
-
|
|
644
|
+
printError(`Invalid --kind: "${kindFlagRaw}". Must be one of: ${userVisibleKinds.join(', ')}`);
|
|
645
|
+
printError(`(kind='raw' is reserved for ingestion connectors; kind='archived' is internal.)`);
|
|
640
646
|
process.exit(1);
|
|
641
647
|
}
|
|
642
648
|
const ownerRaw = typeof flags['owner'] === 'string' ? flags['owner'] : null;
|
|
643
649
|
const ownerCheck = validateOwner(ownerRaw, { strict: isStrictOwnerEnv() });
|
|
644
650
|
if (!ownerCheck.ok) {
|
|
645
|
-
|
|
651
|
+
printError(ownerCheck.message);
|
|
646
652
|
process.exit(1);
|
|
647
653
|
}
|
|
648
654
|
if (ownerCheck.message)
|
|
649
|
-
|
|
655
|
+
printError(ownerCheck.message);
|
|
650
656
|
const ownerFlag = ownerCheck.value ?? null;
|
|
651
657
|
const artifactRefFlag = typeof flags['artifact-ref'] === 'string' ? flags['artifact-ref'] : null;
|
|
652
658
|
const scopeForEnvelope = typeof flags['scope'] === 'string' ? flags['scope'].trim() || null : null;
|
|
@@ -695,6 +701,8 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
695
701
|
console.log(` Tags: ${entry.tags.join(', ')}`);
|
|
696
702
|
if (entry.pinned)
|
|
697
703
|
console.log(' Pinned (no decay)');
|
|
704
|
+
for (const w of vetSecrets(entry.content, entry.tags, false).warnings)
|
|
705
|
+
printError(`Warning: ${w}`);
|
|
698
706
|
void embedMemory(targetRoot, entry);
|
|
699
707
|
const config = loadConfig(targetRoot);
|
|
700
708
|
const shouldExtract = flags['extract'] || config.extraction.enabled === true;
|
|
@@ -705,31 +713,31 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
705
713
|
const facts = await extractFacts(entry.content, {
|
|
706
714
|
apiKey,
|
|
707
715
|
model: config.extraction.model,
|
|
708
|
-
onError: (msg) =>
|
|
716
|
+
onError: (msg) => printError(` (extraction failed: ${msg})`),
|
|
709
717
|
});
|
|
710
718
|
if (facts.length > 0) {
|
|
711
719
|
storeExtractedFacts(targetRoot, entry, facts);
|
|
712
|
-
|
|
720
|
+
printError(` extracted ${facts.length} fact(s)`);
|
|
713
721
|
}
|
|
714
722
|
}
|
|
715
723
|
catch (err) {
|
|
716
724
|
// Extraction is best-effort: report it, never block remember.
|
|
717
|
-
|
|
725
|
+
printError(` (extraction failed: ${err instanceof Error ? err.message : String(err)})`);
|
|
718
726
|
}
|
|
719
727
|
}
|
|
720
728
|
else if (shouldExtract && !apiKey) {
|
|
721
|
-
|
|
729
|
+
printError(' (extraction skipped: ANTHROPIC_API_KEY not set)');
|
|
722
730
|
}
|
|
723
731
|
}
|
|
724
732
|
function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
725
733
|
requireInit(hippoRoot);
|
|
726
734
|
const old = readEntry(hippoRoot, oldId, resolveTenantId({}));
|
|
727
735
|
if (!old) {
|
|
728
|
-
|
|
736
|
+
printError(`Error: memory ${oldId} not found.`);
|
|
729
737
|
process.exit(1);
|
|
730
738
|
}
|
|
731
739
|
if (old.superseded_by) {
|
|
732
|
-
|
|
740
|
+
printError(`Error: memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
|
|
733
741
|
process.exit(1);
|
|
734
742
|
}
|
|
735
743
|
const layer = typeof flags['layer'] === 'string' ? flags['layer'] : undefined;
|
|
@@ -761,7 +769,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
761
769
|
}
|
|
762
770
|
catch (err) {
|
|
763
771
|
if (err instanceof RejectedValueError) {
|
|
764
|
-
|
|
772
|
+
printError(`Error: ${err.message}`);
|
|
765
773
|
process.exit(1);
|
|
766
774
|
}
|
|
767
775
|
throw err;
|
|
@@ -773,7 +781,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
773
781
|
}
|
|
774
782
|
function failWith(message) {
|
|
775
783
|
return () => {
|
|
776
|
-
|
|
784
|
+
printError(message);
|
|
777
785
|
process.exit(1);
|
|
778
786
|
};
|
|
779
787
|
}
|
|
@@ -835,7 +843,7 @@ function parseRerankerFlag(flags) {
|
|
|
835
843
|
return {};
|
|
836
844
|
const topK = flags['reranker-top-k'] !== undefined
|
|
837
845
|
? parseInt(String(flags['reranker-top-k']), 10)
|
|
838
|
-
: name === 'jev' ? JEV_DEFAULT_TOP_K : 50;
|
|
846
|
+
: name === 'jev' || isClefModel(name) ? JEV_DEFAULT_TOP_K : 50;
|
|
839
847
|
return { value: { fn, topK } };
|
|
840
848
|
}
|
|
841
849
|
function parseSalienceFlag(flags) {
|
|
@@ -913,7 +921,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
913
921
|
const entryText = (r) => recallEntryText(r, query, showWhy, primaryIsGlobal || (globalOn && !localIndex.entries[r.entry.id]));
|
|
914
922
|
const printCost = (r) => printedTokens(entryText(r));
|
|
915
923
|
const entryBudget = Math.max(0, budget - printedTokens(recallHeading(budget, budget, query)));
|
|
916
|
-
const rank = await rankRecall({ hippoRoot, globalRoot: globalRoot !== hippoRoot && globalOn ? globalRoot : undefined, tenantId, note: (line) =>
|
|
924
|
+
const rank = await rankRecall({ hippoRoot, globalRoot: globalRoot !== hippoRoot && globalOn ? globalRoot : undefined, tenantId, note: (line) => printError(line) }, {
|
|
917
925
|
query, budget: entryBudget, cost: printCost, limit, why: showWhy, includeSuperseded, asOf,
|
|
918
926
|
explicitScope: recallExplicitScope, activeScope: recallActiveScope,
|
|
919
927
|
search: { ...engineFlags(flags, config), multihop: flags['multihop'] === true || config.multihop.enabled, graphStream, minResults, explain: false },
|
|
@@ -1277,7 +1285,7 @@ function noteScopeHidden(hippoRoot, globalRoot, query, tenantId, requested) {
|
|
|
1277
1285
|
// Window-capped, so the count is a floor on large stores; fine for a "why is my row missing" hint.
|
|
1278
1286
|
const hidden = probe.filter((e) => !api.passesCliRecallScopeFilter(e.scope ?? null, requested)).length;
|
|
1279
1287
|
if (hidden > 0) {
|
|
1280
|
-
|
|
1288
|
+
printError(`[note] ${hidden} candidate${hidden === 1 ? '' : 's'} hidden by recall scope policy (pass an explicit --scope to inspect).`);
|
|
1281
1289
|
}
|
|
1282
1290
|
}
|
|
1283
1291
|
async function cmdExplain(hippoRoot, query, flags) {
|
|
@@ -1437,7 +1445,7 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
|
|
|
1437
1445
|
baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
|
1438
1446
|
}
|
|
1439
1447
|
catch {
|
|
1440
|
-
|
|
1448
|
+
printError(`Warning: eval baseline ${baselinePath} is unreadable; running without it.`);
|
|
1441
1449
|
}
|
|
1442
1450
|
}
|
|
1443
1451
|
const result = await runFeatureEval(version);
|
|
@@ -1462,11 +1470,11 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
|
|
|
1462
1470
|
return;
|
|
1463
1471
|
}
|
|
1464
1472
|
if (!corpusPath) {
|
|
1465
|
-
|
|
1473
|
+
printError('Usage: hippo eval <corpus.json> OR hippo eval --suite [--save-baseline] OR hippo eval --bootstrap');
|
|
1466
1474
|
process.exit(1);
|
|
1467
1475
|
}
|
|
1468
1476
|
if (!fs.existsSync(corpusPath)) {
|
|
1469
|
-
|
|
1477
|
+
printError(`Corpus file not found: ${corpusPath}`);
|
|
1470
1478
|
process.exit(1);
|
|
1471
1479
|
}
|
|
1472
1480
|
let cases;
|
|
@@ -1477,7 +1485,7 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
|
|
|
1477
1485
|
throw new Error('Corpus JSON must be an array or { cases: [...] }');
|
|
1478
1486
|
}
|
|
1479
1487
|
catch (err) {
|
|
1480
|
-
|
|
1488
|
+
printError(`Failed to read corpus: ${err instanceof Error ? err.message : err}`);
|
|
1481
1489
|
process.exit(1);
|
|
1482
1490
|
}
|
|
1483
1491
|
const globalRoot = getGlobalRoot();
|
|
@@ -1535,12 +1543,12 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
|
|
|
1535
1543
|
}
|
|
1536
1544
|
}
|
|
1537
1545
|
if (minMrr !== null && summary.meanMrr < minMrr) {
|
|
1538
|
-
|
|
1546
|
+
printError(`MRR ${fmt(summary.meanMrr, 4)} below threshold ${minMrr}`);
|
|
1539
1547
|
process.exit(1);
|
|
1540
1548
|
}
|
|
1541
1549
|
if (comparePath) {
|
|
1542
1550
|
if (!fs.existsSync(comparePath)) {
|
|
1543
|
-
|
|
1551
|
+
printError(`Baseline file not found: ${comparePath}`);
|
|
1544
1552
|
process.exit(1);
|
|
1545
1553
|
}
|
|
1546
1554
|
let baseline;
|
|
@@ -1548,14 +1556,14 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
|
|
|
1548
1556
|
baseline = JSON.parse(fs.readFileSync(comparePath, 'utf8'));
|
|
1549
1557
|
}
|
|
1550
1558
|
catch (err) {
|
|
1551
|
-
|
|
1559
|
+
printError(`Failed to parse baseline: ${err instanceof Error ? err.message : err}`);
|
|
1552
1560
|
process.exit(1);
|
|
1553
1561
|
}
|
|
1554
1562
|
const cmp = compareSummaries(baseline, summary);
|
|
1555
1563
|
if (asJson) {
|
|
1556
1564
|
// The main JSON output already emitted; append comparison to stderr so
|
|
1557
1565
|
// both can be captured independently.
|
|
1558
|
-
|
|
1566
|
+
printError(JSON.stringify({ compare: cmp }, null, 2));
|
|
1559
1567
|
}
|
|
1560
1568
|
else {
|
|
1561
1569
|
console.log();
|
|
@@ -1592,11 +1600,11 @@ function cmdTraceRecord(hippoRoot, flags) {
|
|
|
1592
1600
|
const outcome = String(flags['outcome'] ?? '').trim();
|
|
1593
1601
|
const validOutcomes = ['success', 'failure', 'partial'];
|
|
1594
1602
|
if (!task || !stepsJson || !outcome) {
|
|
1595
|
-
|
|
1603
|
+
printError('Usage: hippo trace record --task <t> --steps <json> --outcome <success|failure|partial> [--session <id>] [--tag <t>]');
|
|
1596
1604
|
process.exit(1);
|
|
1597
1605
|
}
|
|
1598
1606
|
if (!validOutcomes.includes(outcome)) {
|
|
1599
|
-
|
|
1607
|
+
printError(`Invalid outcome: "${outcome}". Must be one of: ${validOutcomes.join(', ')}.`);
|
|
1600
1608
|
process.exit(1);
|
|
1601
1609
|
}
|
|
1602
1610
|
let steps;
|
|
@@ -1604,7 +1612,7 @@ function cmdTraceRecord(hippoRoot, flags) {
|
|
|
1604
1612
|
steps = parseSteps(stepsJson);
|
|
1605
1613
|
}
|
|
1606
1614
|
catch (err) {
|
|
1607
|
-
|
|
1615
|
+
printError(String(err instanceof Error ? err.message : err));
|
|
1608
1616
|
process.exit(1);
|
|
1609
1617
|
}
|
|
1610
1618
|
const sessionId = String(flags['session'] ?? '').trim() || null;
|
|
@@ -1644,7 +1652,7 @@ function cmdTrace(hippoRoot, id, flags) {
|
|
|
1644
1652
|
sourceLabel = 'global';
|
|
1645
1653
|
}
|
|
1646
1654
|
if (!entry) {
|
|
1647
|
-
|
|
1655
|
+
printError(`Memory not found: ${id}`);
|
|
1648
1656
|
process.exit(1);
|
|
1649
1657
|
}
|
|
1650
1658
|
const now = evalNow();
|
|
@@ -1739,7 +1747,7 @@ async function cmdRefine(hippoRoot, flags) {
|
|
|
1739
1747
|
requireInit(hippoRoot);
|
|
1740
1748
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1741
1749
|
if (!apiKey) {
|
|
1742
|
-
|
|
1750
|
+
printError('hippo refine needs ANTHROPIC_API_KEY in the environment.');
|
|
1743
1751
|
process.exit(1);
|
|
1744
1752
|
}
|
|
1745
1753
|
const dryRun = Boolean(flags['dry-run']);
|
|
@@ -1774,7 +1782,7 @@ function cmdDedup(hippoRoot, flags) {
|
|
|
1774
1782
|
requireInit(hippoRoot);
|
|
1775
1783
|
const dryRun = Boolean(flags['dry-run']);
|
|
1776
1784
|
if (flags['threshold'] !== undefined) {
|
|
1777
|
-
|
|
1785
|
+
printError('hippo dedup: --threshold is ignored; a duplicate is the same text apart from spacing.');
|
|
1778
1786
|
}
|
|
1779
1787
|
const entries = loadAllEntries(hippoRoot);
|
|
1780
1788
|
console.log(`Scanning ${entries.length} memories for duplicates (same text apart from spacing)${dryRun ? ' (dry run)' : ''}...\n`);
|
|
@@ -1826,14 +1834,15 @@ function cmdLastSleep(flags) {
|
|
|
1826
1834
|
content = fs.readFileSync(logPath, 'utf8');
|
|
1827
1835
|
}
|
|
1828
1836
|
catch {
|
|
1837
|
+
// Removed or locked since the exists check: there is nothing to show this session.
|
|
1829
1838
|
return;
|
|
1830
1839
|
}
|
|
1831
1840
|
if (content.trim().length > 0) {
|
|
1832
|
-
|
|
1841
|
+
printError('=== Previous session hippo consolidation ===');
|
|
1833
1842
|
process.stderr.write(content);
|
|
1834
1843
|
if (!content.endsWith('\n'))
|
|
1835
|
-
|
|
1836
|
-
|
|
1844
|
+
printError();
|
|
1845
|
+
printError('===========================================');
|
|
1837
1846
|
}
|
|
1838
1847
|
if (!flags['keep']) {
|
|
1839
1848
|
try {
|
|
@@ -1884,6 +1893,7 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
1884
1893
|
payload = JSON.parse(stdinText.trim());
|
|
1885
1894
|
}
|
|
1886
1895
|
catch {
|
|
1896
|
+
// Malformed JSON is handled as a null payload by the fail-closed check below.
|
|
1887
1897
|
payload = null;
|
|
1888
1898
|
}
|
|
1889
1899
|
if (!payload || typeof payload !== 'object') {
|
|
@@ -1908,6 +1918,10 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
1908
1918
|
}
|
|
1909
1919
|
}
|
|
1910
1920
|
}
|
|
1921
|
+
// A compaction follows a prompt or SessionStart that booked the arm, so this only reads it.
|
|
1922
|
+
if (!suppressOutput && payloadSessionId !== null && inPilotHoldout(hippoRoot, resolveTenantId({}), payloadSessionId, false)) {
|
|
1923
|
+
suppressOutput = true;
|
|
1924
|
+
}
|
|
1911
1925
|
if (!suppressOutput) {
|
|
1912
1926
|
const tenantId = resolveTenantId({});
|
|
1913
1927
|
const snapshot = loadFreshActiveTaskSnapshot(hippoRoot, tenantId, { maxAgeMs: COMPACT_RESUME_MAX_AGE_MS });
|
|
@@ -1930,7 +1944,7 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
1930
1944
|
}
|
|
1931
1945
|
}
|
|
1932
1946
|
catch (err) {
|
|
1933
|
-
|
|
1947
|
+
log.warn(`hippo compact-resume: trail skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
1934
1948
|
}
|
|
1935
1949
|
// Printed in one write so the ledger books exactly the text the model is handed.
|
|
1936
1950
|
const text = captureConsole(() => {
|
|
@@ -1953,7 +1967,7 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
1953
1967
|
}
|
|
1954
1968
|
catch (err) {
|
|
1955
1969
|
// Empty stdout on any store error, never a crashed SessionStart; the reason goes to stderr, which the model never sees.
|
|
1956
|
-
|
|
1970
|
+
log.warn(`hippo compact-resume: skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
1957
1971
|
}
|
|
1958
1972
|
process.exit(0);
|
|
1959
1973
|
}
|
|
@@ -2192,7 +2206,7 @@ function cmdCodexRun(hippoRoot, args) {
|
|
|
2192
2206
|
}
|
|
2193
2207
|
const child = spawnRealCodex(metadata.realCodexPath, args, process.cwd());
|
|
2194
2208
|
child.on('error', (err) => {
|
|
2195
|
-
|
|
2209
|
+
printError(`Failed to launch Codex: ${err.message}`);
|
|
2196
2210
|
process.exit(1);
|
|
2197
2211
|
});
|
|
2198
2212
|
child.on('exit', async (code, signal) => {
|
|
@@ -2239,6 +2253,7 @@ function cmdCodexRun(hippoRoot, args) {
|
|
|
2239
2253
|
process.kill(process.pid, signal);
|
|
2240
2254
|
}
|
|
2241
2255
|
catch {
|
|
2256
|
+
// Cannot re-raise the child's signal on this platform; a non-zero exit still reports the failure.
|
|
2242
2257
|
process.exit(1);
|
|
2243
2258
|
}
|
|
2244
2259
|
return;
|
|
@@ -2409,6 +2424,7 @@ function cmdStatus(hippoRoot) {
|
|
|
2409
2424
|
return resolveEmbeddingProvider(hippoRoot);
|
|
2410
2425
|
}
|
|
2411
2426
|
catch {
|
|
2427
|
+
// Status reports a bad provider config as "misconfigured" below instead of failing.
|
|
2412
2428
|
return null;
|
|
2413
2429
|
}
|
|
2414
2430
|
})();
|
|
@@ -2485,7 +2501,7 @@ function cmdOutcome(hippoRoot, flags) {
|
|
|
2485
2501
|
const good = Boolean(flags['good']);
|
|
2486
2502
|
const bad = Boolean(flags['bad']);
|
|
2487
2503
|
if (!good && !bad) {
|
|
2488
|
-
|
|
2504
|
+
printError('Specify --good or --bad');
|
|
2489
2505
|
process.exit(1);
|
|
2490
2506
|
}
|
|
2491
2507
|
// Behavior fix (v1.11.3): cmdOutcome used to bypass api.outcome and do its
|
|
@@ -2528,7 +2544,7 @@ function cmdForget(hippoRoot, id, flags) {
|
|
|
2528
2544
|
if (flags['archive'] === true) {
|
|
2529
2545
|
const reason = typeof flags['reason'] === 'string' ? flags['reason'] : null;
|
|
2530
2546
|
if (!reason) {
|
|
2531
|
-
|
|
2547
|
+
printError(ARCHIVE_REASON_REQUIRED);
|
|
2532
2548
|
process.exit(1);
|
|
2533
2549
|
}
|
|
2534
2550
|
try {
|
|
@@ -2536,7 +2552,7 @@ function cmdForget(hippoRoot, id, flags) {
|
|
|
2536
2552
|
console.log(`Archived ${id}`);
|
|
2537
2553
|
}
|
|
2538
2554
|
catch (err) {
|
|
2539
|
-
|
|
2555
|
+
printError(`Could not archive ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2540
2556
|
process.exit(1);
|
|
2541
2557
|
}
|
|
2542
2558
|
return;
|
|
@@ -2550,16 +2566,16 @@ function cmdForget(hippoRoot, id, flags) {
|
|
|
2550
2566
|
if (/append-only/i.test(msg)) {
|
|
2551
2567
|
// The delete was refused by the append-only trigger — this is a raw
|
|
2552
2568
|
// memory, not a missing one. Point the user at the archive path.
|
|
2553
|
-
|
|
2569
|
+
printError(rawForgetRefusal(id));
|
|
2554
2570
|
}
|
|
2555
2571
|
else if (api.isDormant(ctx, id)) {
|
|
2556
2572
|
// Sleep moved it to the dormant store: it is not in active memory, so
|
|
2557
2573
|
// point at the command that owns it.
|
|
2558
|
-
|
|
2574
|
+
printError(`${id} is dormant, not in active memory. Delete it for good: hippo dormant forget ${id} ` +
|
|
2559
2575
|
`(or bring it back: hippo dormant restore ${id})`);
|
|
2560
2576
|
}
|
|
2561
2577
|
else {
|
|
2562
|
-
|
|
2578
|
+
printError(`Memory not found: ${id}`);
|
|
2563
2579
|
}
|
|
2564
2580
|
process.exit(1);
|
|
2565
2581
|
}
|
|
@@ -2572,15 +2588,15 @@ function previewForget(hippoRoot, id, archive) {
|
|
|
2572
2588
|
requireInit(hippoRoot);
|
|
2573
2589
|
const entry = readEntry(hippoRoot, id, resolveTenantId({}));
|
|
2574
2590
|
if (!entry) {
|
|
2575
|
-
|
|
2591
|
+
printError(`Memory not found: ${id}`);
|
|
2576
2592
|
process.exit(1);
|
|
2577
2593
|
}
|
|
2578
2594
|
if (!archive && entry.kind === 'raw') {
|
|
2579
|
-
|
|
2595
|
+
printError(rawForgetRefusal(id));
|
|
2580
2596
|
process.exit(1);
|
|
2581
2597
|
}
|
|
2582
2598
|
if (archive && entry.kind !== 'raw') {
|
|
2583
|
-
|
|
2599
|
+
printError(`Could not archive ${id}: memory ${id} is not raw (kind=${entry.kind})`);
|
|
2584
2600
|
process.exit(1);
|
|
2585
2601
|
}
|
|
2586
2602
|
const snippet = entry.content.length > 80 ? `${entry.content.slice(0, 80)}...` : entry.content;
|
|
@@ -2590,7 +2606,7 @@ function cmdInspect(hippoRoot, id) {
|
|
|
2590
2606
|
requireInit(hippoRoot);
|
|
2591
2607
|
const entry = readEntry(hippoRoot, id, resolveTenantId({}));
|
|
2592
2608
|
if (!entry) {
|
|
2593
|
-
|
|
2609
|
+
printError(`Memory not found: ${id}`);
|
|
2594
2610
|
process.exit(1);
|
|
2595
2611
|
}
|
|
2596
2612
|
const now = evalNow();
|
|
@@ -2653,7 +2669,7 @@ function cmdResolve(hippoRoot, args, flags) {
|
|
|
2653
2669
|
// Accept "42" or "conflict_42"
|
|
2654
2670
|
const conflictId = parseInt(rawId.replace(/^conflict_/, ''), 10);
|
|
2655
2671
|
if (isNaN(conflictId)) {
|
|
2656
|
-
|
|
2672
|
+
printError('Usage: hippo resolve <conflict_id> --keep <memory_id> [--forget]');
|
|
2657
2673
|
process.exit(1);
|
|
2658
2674
|
}
|
|
2659
2675
|
const tenantId = resolveTenantId({});
|
|
@@ -2663,7 +2679,7 @@ function cmdResolve(hippoRoot, args, flags) {
|
|
|
2663
2679
|
const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId);
|
|
2664
2680
|
const conflict = conflicts.find((c) => c.id === conflictId);
|
|
2665
2681
|
if (!conflict) {
|
|
2666
|
-
|
|
2682
|
+
printError(`Conflict ${conflictId} not found or already resolved.`);
|
|
2667
2683
|
process.exit(1);
|
|
2668
2684
|
}
|
|
2669
2685
|
console.log(`Conflict ${conflictId}:`);
|
|
@@ -2697,7 +2713,7 @@ function cmdResolve(hippoRoot, args, flags) {
|
|
|
2697
2713
|
reason: reasonFlag,
|
|
2698
2714
|
});
|
|
2699
2715
|
if (!result) {
|
|
2700
|
-
|
|
2716
|
+
printError(`Could not resolve conflict ${conflictId}. Check the ID and --keep value.`);
|
|
2701
2717
|
process.exit(1);
|
|
2702
2718
|
}
|
|
2703
2719
|
const action = rejectLoser
|
|
@@ -2720,20 +2736,20 @@ function cmdReject(hippoRoot, args, flags) {
|
|
|
2720
2736
|
// content, so reason is its only human-readable identity.
|
|
2721
2737
|
const reason = typeof flags['reason'] === 'string' ? flags['reason'].trim() : '';
|
|
2722
2738
|
if (!reason) {
|
|
2723
|
-
|
|
2739
|
+
printError('hippo reject requires --reason "<why>" (the tombstone stores no content; reason is its only identity).');
|
|
2724
2740
|
process.exit(1);
|
|
2725
2741
|
}
|
|
2726
2742
|
const valueFlag = typeof flags['value'] === 'string' ? flags['value'] : undefined;
|
|
2727
2743
|
const memoryId = args[0];
|
|
2728
2744
|
if (!memoryId && valueFlag === undefined) {
|
|
2729
|
-
|
|
2730
|
-
|
|
2745
|
+
printError('Usage: hippo reject <memory-id> --reason "<why>"');
|
|
2746
|
+
printError(' or: hippo reject --value "<text>" --reason "<why>"');
|
|
2731
2747
|
process.exit(1);
|
|
2732
2748
|
}
|
|
2733
2749
|
if (memoryId && valueFlag !== undefined) {
|
|
2734
2750
|
// Ambiguous ask: silently preferring one form would ignore the other
|
|
2735
2751
|
// without feedback (code-review round-1 low).
|
|
2736
|
-
|
|
2752
|
+
printError('hippo reject takes EITHER a memory id OR --value, not both.');
|
|
2737
2753
|
process.exit(1);
|
|
2738
2754
|
}
|
|
2739
2755
|
try {
|
|
@@ -2763,7 +2779,7 @@ function cmdReject(hippoRoot, args, flags) {
|
|
|
2763
2779
|
}
|
|
2764
2780
|
}
|
|
2765
2781
|
catch (err) {
|
|
2766
|
-
|
|
2782
|
+
printError(`Could not reject: ${err instanceof Error ? err.message : String(err)}`);
|
|
2767
2783
|
process.exit(1);
|
|
2768
2784
|
}
|
|
2769
2785
|
}
|
|
@@ -2797,18 +2813,18 @@ function cmdUnreject(hippoRoot, args, flags) {
|
|
|
2797
2813
|
const tenantId = resolveTenantId({});
|
|
2798
2814
|
const digestOrPrefix = (args[0] ?? '').trim();
|
|
2799
2815
|
if (!digestOrPrefix) {
|
|
2800
|
-
|
|
2816
|
+
printError('Usage: hippo unreject <digest-or-prefix>');
|
|
2801
2817
|
process.exit(1);
|
|
2802
2818
|
}
|
|
2803
2819
|
const outcome = unrejectValue(root, tenantId, digestOrPrefix, 'cli');
|
|
2804
2820
|
if (outcome.status === 'not_found') {
|
|
2805
|
-
|
|
2821
|
+
printError(`No rejected value matches "${digestOrPrefix}". Run \`hippo rejections\` to list tombstones.`);
|
|
2806
2822
|
process.exit(1);
|
|
2807
2823
|
}
|
|
2808
2824
|
if (outcome.status === 'ambiguous') {
|
|
2809
|
-
|
|
2825
|
+
printError(`"${digestOrPrefix}" matches ${outcome.candidates.length} tombstones. Use a longer prefix:`);
|
|
2810
2826
|
for (const c of outcome.candidates) {
|
|
2811
|
-
|
|
2827
|
+
printError(` ${c.digest.slice(0, 16)}... ${c.reason ?? 'none given'}`);
|
|
2812
2828
|
}
|
|
2813
2829
|
process.exit(1);
|
|
2814
2830
|
}
|
|
@@ -2831,7 +2847,7 @@ function cmdDormant(hippoRoot, args, flags) {
|
|
|
2831
2847
|
if (sub === 'restore' || sub === 'forget') {
|
|
2832
2848
|
const id = (args[1] ?? '').trim();
|
|
2833
2849
|
if (!id) {
|
|
2834
|
-
|
|
2850
|
+
printError(`Usage: hippo dormant ${sub} <id>`);
|
|
2835
2851
|
process.exit(1);
|
|
2836
2852
|
}
|
|
2837
2853
|
try {
|
|
@@ -2846,10 +2862,10 @@ function cmdDormant(hippoRoot, args, flags) {
|
|
|
2846
2862
|
}
|
|
2847
2863
|
catch (err) {
|
|
2848
2864
|
if (err instanceof RejectedValueError) {
|
|
2849
|
-
|
|
2865
|
+
printError(`Cannot restore ${id}: its value was rejected (${err.reason ?? 'no reason given'}). Run \`hippo unreject\` first to allow it.`);
|
|
2850
2866
|
}
|
|
2851
2867
|
else {
|
|
2852
|
-
|
|
2868
|
+
printError(`Could not ${sub} ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2853
2869
|
}
|
|
2854
2870
|
process.exit(1);
|
|
2855
2871
|
}
|
|
@@ -2894,7 +2910,7 @@ function cmdQuarantine(hippoRoot, args, flags) {
|
|
|
2894
2910
|
if (sub === 'approve' || sub === 'reject') {
|
|
2895
2911
|
const id = (args[1] ?? '').trim();
|
|
2896
2912
|
if (!id) {
|
|
2897
|
-
|
|
2913
|
+
printError(`Usage: hippo quarantine ${sub} <id>`);
|
|
2898
2914
|
process.exit(1);
|
|
2899
2915
|
}
|
|
2900
2916
|
try {
|
|
@@ -2908,7 +2924,7 @@ function cmdQuarantine(hippoRoot, args, flags) {
|
|
|
2908
2924
|
}
|
|
2909
2925
|
}
|
|
2910
2926
|
catch (err) {
|
|
2911
|
-
|
|
2927
|
+
printError(`Could not ${sub} ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2912
2928
|
process.exit(1);
|
|
2913
2929
|
}
|
|
2914
2930
|
return;
|
|
@@ -3025,7 +3041,7 @@ function cmdSnapshot(hippoRoot, args, flags) {
|
|
|
3025
3041
|
const nextStep = String(flags['next-step'] ?? '').trim();
|
|
3026
3042
|
const sessionId = String(flags['session'] ?? flags['id'] ?? '').trim();
|
|
3027
3043
|
if (!task || !summary || !nextStep) {
|
|
3028
|
-
|
|
3044
|
+
printError('Usage: hippo snapshot save --task <task> --summary <summary> --next-step <step> [--source <source>] [--session <session-id>]');
|
|
3029
3045
|
process.exit(1);
|
|
3030
3046
|
}
|
|
3031
3047
|
const snapshot = saveActiveTaskSnapshot(hippoRoot, resolveTenantId({}), {
|
|
@@ -3070,7 +3086,7 @@ function cmdSnapshot(hippoRoot, args, flags) {
|
|
|
3070
3086
|
printActiveTaskSnapshot(snapshot);
|
|
3071
3087
|
return;
|
|
3072
3088
|
}
|
|
3073
|
-
|
|
3089
|
+
printError('Usage: hippo snapshot <save|show|clear>');
|
|
3074
3090
|
process.exit(1);
|
|
3075
3091
|
}
|
|
3076
3092
|
function cmdSession(hippoRoot, args, flags) {
|
|
@@ -3083,7 +3099,7 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
3083
3099
|
const eventType = String(flags['type'] ?? 'note').trim();
|
|
3084
3100
|
const content = String(flags['content'] ?? '').trim();
|
|
3085
3101
|
if (!sessionId || !content) {
|
|
3086
|
-
|
|
3102
|
+
printError('Usage: hippo session log --id <session-id> --content <text> [--type <type>] [--task <task>] [--source <source>]');
|
|
3087
3103
|
process.exit(1);
|
|
3088
3104
|
}
|
|
3089
3105
|
const event = appendSessionEvent(hippoRoot, resolveTenantId({}), {
|
|
@@ -3135,11 +3151,11 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
3135
3151
|
const outcomeRaw = String(flags['outcome'] ?? '').trim();
|
|
3136
3152
|
const summary = String(flags['summary'] ?? '').trim();
|
|
3137
3153
|
if (!sessionId) {
|
|
3138
|
-
|
|
3154
|
+
printError('Usage: hippo session complete --session <session-id> --outcome <success|failure|partial> [--summary "..."]');
|
|
3139
3155
|
process.exit(1);
|
|
3140
3156
|
}
|
|
3141
3157
|
if (!isHandoffOutcome(outcomeRaw)) {
|
|
3142
|
-
|
|
3158
|
+
printError(`Invalid outcome: "${outcomeRaw}". Must be one of: success, failure, partial.`);
|
|
3143
3159
|
process.exit(1);
|
|
3144
3160
|
}
|
|
3145
3161
|
const outcome = outcomeRaw;
|
|
@@ -3206,7 +3222,7 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
3206
3222
|
console.log(lines.join('\n'));
|
|
3207
3223
|
return;
|
|
3208
3224
|
}
|
|
3209
|
-
|
|
3225
|
+
printError('Usage: hippo session <log|show|latest|resume|complete>');
|
|
3210
3226
|
process.exit(1);
|
|
3211
3227
|
}
|
|
3212
3228
|
function cmdHandoff(hippoRoot, args, flags) {
|
|
@@ -3215,12 +3231,12 @@ function cmdHandoff(hippoRoot, args, flags) {
|
|
|
3215
3231
|
if (subcommand === 'create') {
|
|
3216
3232
|
const summary = String(flags['summary'] ?? '').trim();
|
|
3217
3233
|
if (!summary) {
|
|
3218
|
-
|
|
3234
|
+
printError('Usage: hippo handoff create --summary "..." [--next "..."] [--session <id>] [--task <id>] [--artifact <path>...] [--constraint <text>...] [--outcome <success|failure|partial>] [--target-runtime <name>] [--card-id <id>] [--tests <pass|fail|unknown>]');
|
|
3219
3235
|
process.exit(1);
|
|
3220
3236
|
}
|
|
3221
3237
|
const outcomeRaw = flags['outcome'];
|
|
3222
3238
|
if (outcomeRaw !== undefined && !isHandoffOutcome(outcomeRaw)) {
|
|
3223
|
-
|
|
3239
|
+
printError(`Invalid outcome: "${String(outcomeRaw)}". Must be one of: success, failure, partial.`);
|
|
3224
3240
|
process.exit(1);
|
|
3225
3241
|
}
|
|
3226
3242
|
const sessionId = String(flags['session'] ?? flags['id'] ?? '').trim() || `fallback-${Date.now()}-${process.pid}`;
|
|
@@ -3238,7 +3254,7 @@ function cmdHandoff(hippoRoot, args, flags) {
|
|
|
3238
3254
|
for (const name of ['target-runtime', 'card-id']) {
|
|
3239
3255
|
// parseArgs turns a value-less flag into `true`; refuse rather than store "true".
|
|
3240
3256
|
if (flags[name] === true) {
|
|
3241
|
-
|
|
3257
|
+
printError(`--${name} needs a value`);
|
|
3242
3258
|
process.exit(1);
|
|
3243
3259
|
}
|
|
3244
3260
|
}
|
|
@@ -3303,12 +3319,12 @@ function cmdHandoff(hippoRoot, args, flags) {
|
|
|
3303
3319
|
if (subcommand === 'show') {
|
|
3304
3320
|
const idArg = args[1];
|
|
3305
3321
|
if (!idArg) {
|
|
3306
|
-
|
|
3322
|
+
printError('Usage: hippo handoff show <id> [--json]');
|
|
3307
3323
|
process.exit(1);
|
|
3308
3324
|
}
|
|
3309
3325
|
const handoffId = parseInt(idArg, 10);
|
|
3310
3326
|
if (!Number.isFinite(handoffId) || handoffId <= 0) {
|
|
3311
|
-
|
|
3327
|
+
printError(`Invalid handoff ID: ${idArg}`);
|
|
3312
3328
|
process.exit(1);
|
|
3313
3329
|
}
|
|
3314
3330
|
const handoff = loadHandoffById(hippoRoot, resolveTenantId({}), handoffId);
|
|
@@ -3328,7 +3344,7 @@ function cmdHandoff(hippoRoot, args, flags) {
|
|
|
3328
3344
|
printHandoff(handoff);
|
|
3329
3345
|
return;
|
|
3330
3346
|
}
|
|
3331
|
-
|
|
3347
|
+
printError('Usage: hippo handoff <create|latest|show>');
|
|
3332
3348
|
process.exit(1);
|
|
3333
3349
|
}
|
|
3334
3350
|
// Mirrors ARCHIVE_REASON_REQUIRED so the block message can't drift from its usage line.
|
|
@@ -3381,7 +3397,7 @@ function cardRunFlag(flags) {
|
|
|
3381
3397
|
return undefined;
|
|
3382
3398
|
const n = Number(raw);
|
|
3383
3399
|
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n <= 0) {
|
|
3384
|
-
|
|
3400
|
+
printError(`Invalid --run: "${raw}" (expected a positive integer).`);
|
|
3385
3401
|
process.exit(1);
|
|
3386
3402
|
}
|
|
3387
3403
|
return n;
|
|
@@ -3414,7 +3430,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3414
3430
|
for (const key of Object.keys(flags)) {
|
|
3415
3431
|
if (!allowedFlags.includes(key)) {
|
|
3416
3432
|
const valid = allowedFlags.length > 0 ? allowedFlags.map((f) => `--${f}`).join(', ') : '(none)';
|
|
3417
|
-
|
|
3433
|
+
printError(`Unknown flag --${key} for hippo card ${subcommand}. Valid flags: ${valid}`);
|
|
3418
3434
|
process.exit(1);
|
|
3419
3435
|
}
|
|
3420
3436
|
}
|
|
@@ -3422,7 +3438,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3422
3438
|
if (subcommand === 'create') {
|
|
3423
3439
|
const title = cardStringFlag(flags, 'title') ?? '';
|
|
3424
3440
|
if (!title) {
|
|
3425
|
-
|
|
3441
|
+
printError('Usage: hippo card create --title "..." [--repo <name>] [--contract <text>] [--budget <n>] [--depends-on <id>...]');
|
|
3426
3442
|
process.exit(1);
|
|
3427
3443
|
}
|
|
3428
3444
|
const repo = cardStringFlag(flags, 'repo') || undefined;
|
|
@@ -3431,14 +3447,14 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3431
3447
|
let budget;
|
|
3432
3448
|
if (budgetRaw !== undefined) {
|
|
3433
3449
|
if (!/^\d+$/.test(budgetRaw)) {
|
|
3434
|
-
|
|
3450
|
+
printError(`Invalid budget: "${budgetRaw}" (expected a positive integer)`);
|
|
3435
3451
|
process.exit(1);
|
|
3436
3452
|
}
|
|
3437
3453
|
budget = Number(budgetRaw);
|
|
3438
3454
|
}
|
|
3439
3455
|
const dependsOnFlag = flags['depends-on'];
|
|
3440
3456
|
if (dependsOnFlag === true) {
|
|
3441
|
-
|
|
3457
|
+
printError('--depends-on requires a value');
|
|
3442
3458
|
process.exit(1);
|
|
3443
3459
|
}
|
|
3444
3460
|
const dependsOn = Array.isArray(dependsOnFlag) ? dependsOnFlag : [];
|
|
@@ -3447,7 +3463,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3447
3463
|
card = createCard(hippoRoot, tenantId, { title, repo, contract, budget, dependsOn });
|
|
3448
3464
|
}
|
|
3449
3465
|
catch (error) {
|
|
3450
|
-
|
|
3466
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
3451
3467
|
process.exit(1);
|
|
3452
3468
|
}
|
|
3453
3469
|
console.log(`Created card ${card.id} (status: ${card.status})`);
|
|
@@ -3456,12 +3472,12 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3456
3472
|
if (subcommand === 'show') {
|
|
3457
3473
|
const id = args[1];
|
|
3458
3474
|
if (!id) {
|
|
3459
|
-
|
|
3475
|
+
printError('Usage: hippo card show <id> [--json]');
|
|
3460
3476
|
process.exit(1);
|
|
3461
3477
|
}
|
|
3462
3478
|
const detail = loadCardDetail(hippoRoot, tenantId, id);
|
|
3463
3479
|
if (!detail) {
|
|
3464
|
-
|
|
3480
|
+
printError(`No card found with id ${id}.`);
|
|
3465
3481
|
process.exit(1);
|
|
3466
3482
|
}
|
|
3467
3483
|
if (flags['json']) {
|
|
@@ -3474,7 +3490,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3474
3490
|
if (subcommand === 'list') {
|
|
3475
3491
|
const status = cardStringFlag(flags, 'status');
|
|
3476
3492
|
if (status !== undefined && !isCardStatus(status)) {
|
|
3477
|
-
|
|
3493
|
+
printError(`Invalid status: "${status}".`);
|
|
3478
3494
|
process.exit(1);
|
|
3479
3495
|
}
|
|
3480
3496
|
const cards = listCards(hippoRoot, tenantId, { status });
|
|
@@ -3495,7 +3511,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3495
3511
|
const id = args[1];
|
|
3496
3512
|
const runtime = cardStringFlag(flags, 'runtime') ?? '';
|
|
3497
3513
|
if (!id || !runtime) {
|
|
3498
|
-
|
|
3514
|
+
printError('Usage: hippo card claim <id> --runtime <name> [--session <id>]');
|
|
3499
3515
|
process.exit(1);
|
|
3500
3516
|
}
|
|
3501
3517
|
const sessionId = cardStringFlag(flags, 'session') || undefined;
|
|
@@ -3504,11 +3520,11 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3504
3520
|
card = claimCard(hippoRoot, tenantId, id, runtime, sessionId);
|
|
3505
3521
|
}
|
|
3506
3522
|
catch (error) {
|
|
3507
|
-
|
|
3523
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
3508
3524
|
process.exit(1);
|
|
3509
3525
|
}
|
|
3510
3526
|
if (!card) {
|
|
3511
|
-
|
|
3527
|
+
printError(`Could not claim card ${id} (not ready/blocked, or already claimed).`);
|
|
3512
3528
|
process.exit(1);
|
|
3513
3529
|
}
|
|
3514
3530
|
console.log(`Claimed card ${card.id} for ${runtime} (run ${card.runId}, lease until ${card.leaseUntil})`);
|
|
@@ -3518,7 +3534,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3518
3534
|
const id = args[1];
|
|
3519
3535
|
const runId = cardRunFlag(flags);
|
|
3520
3536
|
if (!id || runId === undefined) {
|
|
3521
|
-
|
|
3537
|
+
printError('Usage: hippo card heartbeat <id> --run <n>');
|
|
3522
3538
|
process.exit(1);
|
|
3523
3539
|
}
|
|
3524
3540
|
let card;
|
|
@@ -3526,11 +3542,11 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3526
3542
|
card = heartbeatCard(hippoRoot, tenantId, id, runId);
|
|
3527
3543
|
}
|
|
3528
3544
|
catch (error) {
|
|
3529
|
-
|
|
3545
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
3530
3546
|
process.exit(1);
|
|
3531
3547
|
}
|
|
3532
3548
|
if (!card) {
|
|
3533
|
-
|
|
3549
|
+
printError(`Could not heartbeat card ${id} (${cardRefusal(hippoRoot, tenantId, id)}).`);
|
|
3534
3550
|
process.exit(1);
|
|
3535
3551
|
}
|
|
3536
3552
|
console.log(`Heartbeat card ${card.id}: lease until ${card.leaseUntil}`);
|
|
@@ -3540,7 +3556,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3540
3556
|
const id = args[1];
|
|
3541
3557
|
const reason = cardStringFlag(flags, 'reason') ?? '';
|
|
3542
3558
|
if (!id || !reason) {
|
|
3543
|
-
|
|
3559
|
+
printError(CARD_BLOCK_REASON_REQUIRED);
|
|
3544
3560
|
process.exit(1);
|
|
3545
3561
|
}
|
|
3546
3562
|
const runId = cardRunFlag(flags);
|
|
@@ -3549,12 +3565,12 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3549
3565
|
card = blockCard(hippoRoot, tenantId, id, reason, runId);
|
|
3550
3566
|
}
|
|
3551
3567
|
catch (error) {
|
|
3552
|
-
|
|
3568
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
3553
3569
|
process.exit(1);
|
|
3554
3570
|
}
|
|
3555
3571
|
if (!card) {
|
|
3556
3572
|
const why = runId === undefined ? 'not running' : cardRefusal(hippoRoot, tenantId, id);
|
|
3557
|
-
|
|
3573
|
+
printError(`Could not block card ${id} (${why}).`);
|
|
3558
3574
|
process.exit(1);
|
|
3559
3575
|
}
|
|
3560
3576
|
console.log(`Blocked card ${card.id}`);
|
|
@@ -3563,7 +3579,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3563
3579
|
if (subcommand === 'review') {
|
|
3564
3580
|
const id = args[1];
|
|
3565
3581
|
if (!id) {
|
|
3566
|
-
|
|
3582
|
+
printError('Usage: hippo card review <id> [--run <n>]');
|
|
3567
3583
|
process.exit(1);
|
|
3568
3584
|
}
|
|
3569
3585
|
const runId = cardRunFlag(flags);
|
|
@@ -3572,12 +3588,12 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3572
3588
|
card = reviewCard(hippoRoot, tenantId, id, runId);
|
|
3573
3589
|
}
|
|
3574
3590
|
catch (error) {
|
|
3575
|
-
|
|
3591
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
3576
3592
|
process.exit(1);
|
|
3577
3593
|
}
|
|
3578
3594
|
if (!card) {
|
|
3579
3595
|
const why = runId === undefined ? 'not running' : cardRefusal(hippoRoot, tenantId, id);
|
|
3580
|
-
|
|
3596
|
+
printError(`Could not move card ${id} to review (${why}).`);
|
|
3581
3597
|
process.exit(1);
|
|
3582
3598
|
}
|
|
3583
3599
|
console.log(`Card ${card.id} moved to review`);
|
|
@@ -3587,7 +3603,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3587
3603
|
const id = args[1];
|
|
3588
3604
|
const outcomeRaw = flags['outcome'];
|
|
3589
3605
|
if (!id || !isHandoffOutcome(outcomeRaw)) {
|
|
3590
|
-
|
|
3606
|
+
printError('Usage: hippo card complete <id> --outcome <success|failure|partial> [--run <n>]');
|
|
3591
3607
|
process.exit(1);
|
|
3592
3608
|
}
|
|
3593
3609
|
const runId = cardRunFlag(flags);
|
|
@@ -3596,12 +3612,12 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3596
3612
|
result = completeCard(hippoRoot, tenantId, id, outcomeRaw, runId);
|
|
3597
3613
|
}
|
|
3598
3614
|
catch (error) {
|
|
3599
|
-
|
|
3615
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
3600
3616
|
process.exit(1);
|
|
3601
3617
|
}
|
|
3602
3618
|
if (!result) {
|
|
3603
3619
|
const why = runId === undefined ? 'not in review' : cardRefusal(hippoRoot, tenantId, id);
|
|
3604
|
-
|
|
3620
|
+
printError(`Could not complete card ${id} (${why}).`);
|
|
3605
3621
|
process.exit(1);
|
|
3606
3622
|
}
|
|
3607
3623
|
console.log(`Completed card ${result.card.id} (status: ${result.card.status})`);
|
|
@@ -3612,7 +3628,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3612
3628
|
}
|
|
3613
3629
|
if (subcommand === 'reclaim') {
|
|
3614
3630
|
if (args.length > 1) {
|
|
3615
|
-
|
|
3631
|
+
printError('Usage: hippo card reclaim (sweeps every expired lease; use hippo card block <id> for one card)');
|
|
3616
3632
|
process.exit(1);
|
|
3617
3633
|
}
|
|
3618
3634
|
const ids = reclaimExpiredCards(hippoRoot, tenantId);
|
|
@@ -3628,18 +3644,18 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3628
3644
|
if (subcommand === 'comment') {
|
|
3629
3645
|
const id = args[1];
|
|
3630
3646
|
if (!id) {
|
|
3631
|
-
|
|
3647
|
+
printError('Usage: hippo card comment <id> --body "..." [--author <name>]');
|
|
3632
3648
|
process.exit(1);
|
|
3633
3649
|
}
|
|
3634
3650
|
// Only show and comment look the card up directly; claim/heartbeat/block/review/complete throw from the store instead.
|
|
3635
3651
|
const card = loadCard(hippoRoot, tenantId, id);
|
|
3636
3652
|
if (!card) {
|
|
3637
|
-
|
|
3653
|
+
printError(`No card found with id ${id}.`);
|
|
3638
3654
|
process.exit(1);
|
|
3639
3655
|
}
|
|
3640
3656
|
const body = cardStringFlag(flags, 'body') ?? '';
|
|
3641
3657
|
if (!body) {
|
|
3642
|
-
|
|
3658
|
+
printError('Usage: hippo card comment <id> --body "..." [--author <name>]');
|
|
3643
3659
|
process.exit(1);
|
|
3644
3660
|
}
|
|
3645
3661
|
const author = cardStringFlag(flags, 'author') || 'cli';
|
|
@@ -3647,7 +3663,7 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
3647
3663
|
console.log(`Added comment ${comment.id} to card ${id}`);
|
|
3648
3664
|
return;
|
|
3649
3665
|
}
|
|
3650
|
-
|
|
3666
|
+
printError('Usage: hippo card <create|show|list|claim|heartbeat|block|review|complete|reclaim|comment>');
|
|
3651
3667
|
process.exit(1);
|
|
3652
3668
|
}
|
|
3653
3669
|
// ---------------------------------------------------------------------------
|
|
@@ -3661,23 +3677,23 @@ function cmdPredict(hippoRoot, args, flags) {
|
|
|
3661
3677
|
if (subcommand === 'close') {
|
|
3662
3678
|
const idRaw = args[1];
|
|
3663
3679
|
if (!idRaw) {
|
|
3664
|
-
|
|
3680
|
+
printError('Usage: hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
|
|
3665
3681
|
process.exit(1);
|
|
3666
3682
|
}
|
|
3667
3683
|
const id = parseInt(String(idRaw), 10);
|
|
3668
3684
|
if (!Number.isFinite(id) || id <= 0) {
|
|
3669
|
-
|
|
3685
|
+
printError(`Invalid prediction id: "${idRaw}"`);
|
|
3670
3686
|
process.exit(1);
|
|
3671
3687
|
}
|
|
3672
3688
|
const stateRaw = typeof flags['state'] === 'string' ? flags['state'].trim() : '';
|
|
3673
3689
|
if (!predictionsModule.VALID_CLOSURE_STATES.has(stateRaw) || stateRaw === 'open') {
|
|
3674
|
-
|
|
3690
|
+
printError(`Invalid --state: "${stateRaw}". Must be one of: closed | closed-unknown.`);
|
|
3675
3691
|
process.exit(1);
|
|
3676
3692
|
}
|
|
3677
3693
|
const actualRaw = flags['actual'];
|
|
3678
3694
|
const actualValue = actualRaw !== undefined ? Number(actualRaw) : undefined;
|
|
3679
3695
|
if (actualRaw !== undefined && !Number.isFinite(actualValue)) {
|
|
3680
|
-
|
|
3696
|
+
printError(`Invalid --actual: "${actualRaw}". Must be a number.`);
|
|
3681
3697
|
process.exit(1);
|
|
3682
3698
|
}
|
|
3683
3699
|
const noteRaw = flags['note'];
|
|
@@ -3698,7 +3714,7 @@ function cmdPredict(hippoRoot, args, flags) {
|
|
|
3698
3714
|
const limitRaw = flags['limit'];
|
|
3699
3715
|
const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
|
|
3700
3716
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
3701
|
-
|
|
3717
|
+
printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
|
|
3702
3718
|
process.exit(1);
|
|
3703
3719
|
}
|
|
3704
3720
|
let results;
|
|
@@ -3722,7 +3738,7 @@ function cmdPredict(hippoRoot, args, flags) {
|
|
|
3722
3738
|
}
|
|
3723
3739
|
else {
|
|
3724
3740
|
if (!predictionsModule.VALID_CLOSURE_STATES.has(status)) {
|
|
3725
|
-
|
|
3741
|
+
printError(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
|
|
3726
3742
|
process.exit(1);
|
|
3727
3743
|
}
|
|
3728
3744
|
if (classTag) {
|
|
@@ -3733,7 +3749,7 @@ function cmdPredict(hippoRoot, args, flags) {
|
|
|
3733
3749
|
}
|
|
3734
3750
|
else {
|
|
3735
3751
|
// status filter without class — scan all classes is more complex; v1 requires --class for non-default status
|
|
3736
|
-
|
|
3752
|
+
printError('--status filter (non-open) requires --class to be set.');
|
|
3737
3753
|
process.exit(1);
|
|
3738
3754
|
}
|
|
3739
3755
|
}
|
|
@@ -3756,17 +3772,17 @@ function cmdPredict(hippoRoot, args, flags) {
|
|
|
3756
3772
|
if (subcommand === 'show') {
|
|
3757
3773
|
const idRaw = args[1];
|
|
3758
3774
|
if (!idRaw) {
|
|
3759
|
-
|
|
3775
|
+
printError('Usage: hippo predict show <id>');
|
|
3760
3776
|
process.exit(1);
|
|
3761
3777
|
}
|
|
3762
3778
|
const id = parseInt(String(idRaw), 10);
|
|
3763
3779
|
if (!Number.isFinite(id) || id <= 0) {
|
|
3764
|
-
|
|
3780
|
+
printError(`Invalid prediction id: "${idRaw}"`);
|
|
3765
3781
|
process.exit(1);
|
|
3766
3782
|
}
|
|
3767
3783
|
const pred = predictionsModule.loadPredictionById(hippoRoot, tenantId, id);
|
|
3768
3784
|
if (!pred) {
|
|
3769
|
-
|
|
3785
|
+
printError(`Prediction ${id} not found.`);
|
|
3770
3786
|
process.exit(1);
|
|
3771
3787
|
}
|
|
3772
3788
|
console.log(`Prediction #${pred.id}`);
|
|
@@ -3792,7 +3808,7 @@ function cmdPredict(hippoRoot, args, flags) {
|
|
|
3792
3808
|
// J3 reference-class / planning-fallacy detector
|
|
3793
3809
|
const classTagRaw = flags['class'];
|
|
3794
3810
|
if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
|
|
3795
|
-
|
|
3811
|
+
printError('Usage: hippo predict baserate --class <c>');
|
|
3796
3812
|
process.exit(1);
|
|
3797
3813
|
}
|
|
3798
3814
|
const baserate = predictionsModule.computePredictionBaserate(hippoRoot, tenantId, classTagRaw.trim());
|
|
@@ -3820,22 +3836,22 @@ function cmdPredict(hippoRoot, args, flags) {
|
|
|
3820
3836
|
// Default subcommand: create. args[0] is the claim text.
|
|
3821
3837
|
const claimText = subcommand;
|
|
3822
3838
|
if (!claimText) {
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3839
|
+
printError('Usage: hippo predict "<claim>" --class <c> [--estimate <v>] [--unit <u>] [--target <YYYY-MM-DD>]');
|
|
3840
|
+
printError(' hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
|
|
3841
|
+
printError(' hippo predict list [--class X] [--status open|closed|closed-unknown|all] [--limit N]');
|
|
3842
|
+
printError(' hippo predict show <id>');
|
|
3827
3843
|
process.exit(1);
|
|
3828
3844
|
}
|
|
3829
3845
|
const classTagRaw = flags['class'];
|
|
3830
3846
|
if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
|
|
3831
|
-
|
|
3847
|
+
printError('--class is required for prediction creation.');
|
|
3832
3848
|
process.exit(1);
|
|
3833
3849
|
}
|
|
3834
3850
|
const classTag = classTagRaw.trim();
|
|
3835
3851
|
const estimateRaw = flags['estimate'];
|
|
3836
3852
|
const estimateValue = estimateRaw !== undefined ? Number(estimateRaw) : undefined;
|
|
3837
3853
|
if (estimateRaw !== undefined && !Number.isFinite(estimateValue)) {
|
|
3838
|
-
|
|
3854
|
+
printError(`Invalid --estimate: "${estimateRaw}". Must be a number.`);
|
|
3839
3855
|
process.exit(1);
|
|
3840
3856
|
}
|
|
3841
3857
|
const unitRaw = flags['unit'];
|
|
@@ -3863,7 +3879,7 @@ function cmdDecide(hippoRoot, args, flags) {
|
|
|
3863
3879
|
const limitRaw = flags['limit'];
|
|
3864
3880
|
const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
|
|
3865
3881
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
3866
|
-
|
|
3882
|
+
printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
|
|
3867
3883
|
process.exit(1);
|
|
3868
3884
|
}
|
|
3869
3885
|
let results;
|
|
@@ -3872,7 +3888,7 @@ function cmdDecide(hippoRoot, args, flags) {
|
|
|
3872
3888
|
}
|
|
3873
3889
|
else {
|
|
3874
3890
|
if (!decisionsModule.VALID_DECISION_STATES.has(status)) {
|
|
3875
|
-
|
|
3891
|
+
printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
|
|
3876
3892
|
process.exit(1);
|
|
3877
3893
|
}
|
|
3878
3894
|
results = decisionsModule.loadDecisions(hippoRoot, tenantId, {
|
|
@@ -3897,17 +3913,17 @@ function cmdDecide(hippoRoot, args, flags) {
|
|
|
3897
3913
|
if (subcommand === 'get') {
|
|
3898
3914
|
const idRaw = args[1];
|
|
3899
3915
|
if (!idRaw) {
|
|
3900
|
-
|
|
3916
|
+
printError('Usage: hippo decide get <id>');
|
|
3901
3917
|
process.exit(1);
|
|
3902
3918
|
}
|
|
3903
3919
|
const id = parseInt(String(idRaw), 10);
|
|
3904
3920
|
if (!Number.isFinite(id) || id <= 0) {
|
|
3905
|
-
|
|
3921
|
+
printError(`Invalid decision id: "${idRaw}"`);
|
|
3906
3922
|
process.exit(1);
|
|
3907
3923
|
}
|
|
3908
3924
|
const decision = decisionsModule.loadDecisionById(hippoRoot, tenantId, id);
|
|
3909
3925
|
if (!decision) {
|
|
3910
|
-
|
|
3926
|
+
printError(`Decision ${id} not found.`);
|
|
3911
3927
|
process.exit(1);
|
|
3912
3928
|
}
|
|
3913
3929
|
console.log(`Decision #${decision.id}`);
|
|
@@ -3929,12 +3945,12 @@ function cmdDecide(hippoRoot, args, flags) {
|
|
|
3929
3945
|
if (subcommand === 'close') {
|
|
3930
3946
|
const idRaw = args[1];
|
|
3931
3947
|
if (!idRaw) {
|
|
3932
|
-
|
|
3948
|
+
printError('Usage: hippo decide close <id>');
|
|
3933
3949
|
process.exit(1);
|
|
3934
3950
|
}
|
|
3935
3951
|
const id = parseInt(String(idRaw), 10);
|
|
3936
3952
|
if (!Number.isFinite(id) || id <= 0) {
|
|
3937
|
-
|
|
3953
|
+
printError(`Invalid decision id: "${idRaw}"`);
|
|
3938
3954
|
process.exit(1);
|
|
3939
3955
|
}
|
|
3940
3956
|
const closed = decisionsModule.closeDecision(hippoRoot, tenantId, id);
|
|
@@ -3944,10 +3960,10 @@ function cmdDecide(hippoRoot, args, flags) {
|
|
|
3944
3960
|
// Default subcommand: create. args[0] is the decision text.
|
|
3945
3961
|
const decisionText = subcommand;
|
|
3946
3962
|
if (!decisionText) {
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3963
|
+
printError('Usage: hippo decide "<decision>" [--context "<why>"] [--supersedes <memory-id>]');
|
|
3964
|
+
printError(' hippo decide list [--status active|superseded|closed|all] [--limit N]');
|
|
3965
|
+
printError(' hippo decide get <id>');
|
|
3966
|
+
printError(' hippo decide close <id>');
|
|
3951
3967
|
process.exit(1);
|
|
3952
3968
|
}
|
|
3953
3969
|
const contextRaw = flags['context'];
|
|
@@ -3956,7 +3972,7 @@ function cmdDecide(hippoRoot, args, flags) {
|
|
|
3956
3972
|
// request: the user asked to supersede but gave no memory id. Reject it rather
|
|
3957
3973
|
// than silently creating a non-superseding decision (codex review 2026-05-28).
|
|
3958
3974
|
if (flags['supersedes'] === true) {
|
|
3959
|
-
|
|
3975
|
+
printError('--supersedes requires a memory id, e.g. hippo decide "<text>" --supersedes mem_abc123.');
|
|
3960
3976
|
process.exit(1);
|
|
3961
3977
|
}
|
|
3962
3978
|
const supersedesMemId = typeof flags['supersedes'] === 'string' ? flags['supersedes'] : null;
|
|
@@ -3970,7 +3986,7 @@ function cmdDecide(hippoRoot, args, flags) {
|
|
|
3970
3986
|
if (supersedesMemId) {
|
|
3971
3987
|
oldEntry = readEntry(hippoRoot, supersedesMemId, tenantId) ?? null;
|
|
3972
3988
|
if (!oldEntry) {
|
|
3973
|
-
|
|
3989
|
+
printError(`Memory ${supersedesMemId} not found.`);
|
|
3974
3990
|
process.exit(1);
|
|
3975
3991
|
}
|
|
3976
3992
|
supersedesDecisionId =
|
|
@@ -4000,7 +4016,7 @@ function cmdDecide(hippoRoot, args, flags) {
|
|
|
4000
4016
|
writeEntry(hippoRoot, oldEntry);
|
|
4001
4017
|
}
|
|
4002
4018
|
catch (e) {
|
|
4003
|
-
|
|
4019
|
+
printError(` warning: decision recorded and superseded, but failed to weaken the prior memory ${supersedesMemId}: ${e.message}`);
|
|
4004
4020
|
}
|
|
4005
4021
|
}
|
|
4006
4022
|
console.log(`Decision recorded: #${created.id}`);
|
|
@@ -4021,7 +4037,7 @@ function parsePositiveIncidentId(idRaw) {
|
|
|
4021
4037
|
const s = String(idRaw ?? '').trim();
|
|
4022
4038
|
const id = parseInt(s, 10);
|
|
4023
4039
|
if (!/^\d+$/.test(s) || id <= 0) {
|
|
4024
|
-
|
|
4040
|
+
printError(`Invalid incident id: "${idRaw}" (expected a positive integer).`);
|
|
4025
4041
|
process.exit(1);
|
|
4026
4042
|
}
|
|
4027
4043
|
return id;
|
|
@@ -4036,7 +4052,7 @@ function cmdIncident(hippoRoot, args, flags) {
|
|
|
4036
4052
|
const limitRaw = flags['limit'];
|
|
4037
4053
|
const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
|
|
4038
4054
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
4039
|
-
|
|
4055
|
+
printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
|
|
4040
4056
|
process.exit(1);
|
|
4041
4057
|
}
|
|
4042
4058
|
let results;
|
|
@@ -4045,7 +4061,7 @@ function cmdIncident(hippoRoot, args, flags) {
|
|
|
4045
4061
|
}
|
|
4046
4062
|
else {
|
|
4047
4063
|
if (!incidentsModule.VALID_INCIDENT_STATES.has(status)) {
|
|
4048
|
-
|
|
4064
|
+
printError(`Invalid --status: "${status}". Must be one of: open | resolved | closed | all.`);
|
|
4049
4065
|
process.exit(1);
|
|
4050
4066
|
}
|
|
4051
4067
|
results = incidentsModule.loadIncidents(hippoRoot, tenantId, {
|
|
@@ -4070,13 +4086,13 @@ function cmdIncident(hippoRoot, args, flags) {
|
|
|
4070
4086
|
if (subcommand === 'get') {
|
|
4071
4087
|
const idRaw = args[1];
|
|
4072
4088
|
if (!idRaw) {
|
|
4073
|
-
|
|
4089
|
+
printError('Usage: hippo incident get <id>');
|
|
4074
4090
|
process.exit(1);
|
|
4075
4091
|
}
|
|
4076
4092
|
const id = parsePositiveIncidentId(idRaw);
|
|
4077
4093
|
const incident = incidentsModule.loadIncidentById(hippoRoot, tenantId, id);
|
|
4078
4094
|
if (!incident) {
|
|
4079
|
-
|
|
4095
|
+
printError(`Incident ${id} not found.`);
|
|
4080
4096
|
process.exit(1);
|
|
4081
4097
|
}
|
|
4082
4098
|
console.log(`Incident #${incident.id}`);
|
|
@@ -4101,13 +4117,13 @@ function cmdIncident(hippoRoot, args, flags) {
|
|
|
4101
4117
|
if (subcommand === 'resolve') {
|
|
4102
4118
|
const idRaw = args[1];
|
|
4103
4119
|
if (!idRaw) {
|
|
4104
|
-
|
|
4120
|
+
printError('Usage: hippo incident resolve <id> --resolution "<text>"');
|
|
4105
4121
|
process.exit(1);
|
|
4106
4122
|
}
|
|
4107
4123
|
const id = parsePositiveIncidentId(idRaw);
|
|
4108
4124
|
const resolutionRaw = flags['resolution'];
|
|
4109
4125
|
if (typeof resolutionRaw !== 'string' || !resolutionRaw.trim()) {
|
|
4110
|
-
|
|
4126
|
+
printError('--resolution requires a non-empty value, e.g. hippo incident resolve <id> --resolution "root cause fixed".');
|
|
4111
4127
|
process.exit(1);
|
|
4112
4128
|
}
|
|
4113
4129
|
const resolved = incidentsModule.resolveIncident(hippoRoot, tenantId, id, resolutionRaw);
|
|
@@ -4117,7 +4133,7 @@ function cmdIncident(hippoRoot, args, flags) {
|
|
|
4117
4133
|
if (subcommand === 'close') {
|
|
4118
4134
|
const idRaw = args[1];
|
|
4119
4135
|
if (!idRaw) {
|
|
4120
|
-
|
|
4136
|
+
printError('Usage: hippo incident close <id>');
|
|
4121
4137
|
process.exit(1);
|
|
4122
4138
|
}
|
|
4123
4139
|
const id = parsePositiveIncidentId(idRaw);
|
|
@@ -4130,11 +4146,11 @@ function cmdIncident(hippoRoot, args, flags) {
|
|
|
4130
4146
|
// `open` keyword the text is args[1], otherwise args[0] IS the text.
|
|
4131
4147
|
const incidentText = subcommand === 'open' ? (args[1] ?? '') : subcommand;
|
|
4132
4148
|
if (!incidentText) {
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4149
|
+
printError('Usage: hippo incident "<incident>" [--context "<details>"] [--link <memory-id>]...');
|
|
4150
|
+
printError(' hippo incident list [--status open|resolved|closed|all] [--limit N]');
|
|
4151
|
+
printError(' hippo incident get <id>');
|
|
4152
|
+
printError(' hippo incident resolve <id> --resolution "<text>"');
|
|
4153
|
+
printError(' hippo incident close <id>');
|
|
4138
4154
|
process.exit(1);
|
|
4139
4155
|
}
|
|
4140
4156
|
const contextRaw = flags['context'];
|
|
@@ -4150,7 +4166,7 @@ function cmdIncident(hippoRoot, args, flags) {
|
|
|
4150
4166
|
linkedMemoryIds = [linkRaw];
|
|
4151
4167
|
}
|
|
4152
4168
|
else if (linkRaw === true) {
|
|
4153
|
-
|
|
4169
|
+
printError('--link requires a memory id, e.g. hippo incident "<text>" --link mem_abc123.');
|
|
4154
4170
|
process.exit(1);
|
|
4155
4171
|
}
|
|
4156
4172
|
const incidentPathTags = extractPathTags(process.cwd());
|
|
@@ -4176,7 +4192,7 @@ function parsePositiveProcessId(idRaw) {
|
|
|
4176
4192
|
const s = String(idRaw ?? '').trim();
|
|
4177
4193
|
const id = parseInt(s, 10);
|
|
4178
4194
|
if (!/^\d+$/.test(s) || id <= 0) {
|
|
4179
|
-
|
|
4195
|
+
printError(`Invalid process id: "${idRaw}" (expected a positive integer).`);
|
|
4180
4196
|
process.exit(1);
|
|
4181
4197
|
}
|
|
4182
4198
|
return id;
|
|
@@ -4189,7 +4205,7 @@ function collectProcessSteps(stepRaw) {
|
|
|
4189
4205
|
if (typeof stepRaw === 'string')
|
|
4190
4206
|
return [stepRaw];
|
|
4191
4207
|
if (stepRaw === true) {
|
|
4192
|
-
|
|
4208
|
+
printError('--step requires a value, e.g. hippo process new "<name>" --step "do X".');
|
|
4193
4209
|
process.exit(1);
|
|
4194
4210
|
}
|
|
4195
4211
|
return [];
|
|
@@ -4204,7 +4220,7 @@ function cmdProcess(hippoRoot, args, flags) {
|
|
|
4204
4220
|
const limitRaw = flags['limit'];
|
|
4205
4221
|
const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
|
|
4206
4222
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
4207
|
-
|
|
4223
|
+
printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
|
|
4208
4224
|
process.exit(1);
|
|
4209
4225
|
}
|
|
4210
4226
|
let results;
|
|
@@ -4213,7 +4229,7 @@ function cmdProcess(hippoRoot, args, flags) {
|
|
|
4213
4229
|
}
|
|
4214
4230
|
else {
|
|
4215
4231
|
if (!processesModule.VALID_PROCESS_STATES.has(status)) {
|
|
4216
|
-
|
|
4232
|
+
printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
|
|
4217
4233
|
process.exit(1);
|
|
4218
4234
|
}
|
|
4219
4235
|
results = processesModule.loadProcesses(hippoRoot, tenantId, {
|
|
@@ -4237,13 +4253,13 @@ function cmdProcess(hippoRoot, args, flags) {
|
|
|
4237
4253
|
if (subcommand === 'get') {
|
|
4238
4254
|
const idRaw = args[1];
|
|
4239
4255
|
if (!idRaw) {
|
|
4240
|
-
|
|
4256
|
+
printError('Usage: hippo process get <id>');
|
|
4241
4257
|
process.exit(1);
|
|
4242
4258
|
}
|
|
4243
4259
|
const id = parsePositiveProcessId(idRaw);
|
|
4244
4260
|
const proc = processesModule.loadProcessById(hippoRoot, tenantId, id);
|
|
4245
4261
|
if (!proc) {
|
|
4246
|
-
|
|
4262
|
+
printError(`Process ${id} not found.`);
|
|
4247
4263
|
process.exit(1);
|
|
4248
4264
|
}
|
|
4249
4265
|
console.log(`Process #${proc.id}`);
|
|
@@ -4272,13 +4288,13 @@ function cmdProcess(hippoRoot, args, flags) {
|
|
|
4272
4288
|
if (subcommand === 'supersede') {
|
|
4273
4289
|
const idRaw = args[1];
|
|
4274
4290
|
if (!idRaw) {
|
|
4275
|
-
|
|
4291
|
+
printError('Usage: hippo process supersede <id> --step "<text>" [--step ...] [--change "<summary>"] [--description "<text>"]');
|
|
4276
4292
|
process.exit(1);
|
|
4277
4293
|
}
|
|
4278
4294
|
const id = parsePositiveProcessId(idRaw);
|
|
4279
4295
|
const steps = collectProcessSteps(flags['step']);
|
|
4280
4296
|
if (steps.length === 0) {
|
|
4281
|
-
|
|
4297
|
+
printError('hippo process supersede requires at least one --step "<text>" for the new version.');
|
|
4282
4298
|
process.exit(1);
|
|
4283
4299
|
}
|
|
4284
4300
|
// A supersession is a new version of the SAME process, so the new row reuses
|
|
@@ -4287,7 +4303,7 @@ function cmdProcess(hippoRoot, args, flags) {
|
|
|
4287
4303
|
// preflight is the authoritative active-state check.
|
|
4288
4304
|
const existing = processesModule.loadProcessById(hippoRoot, tenantId, id);
|
|
4289
4305
|
if (!existing) {
|
|
4290
|
-
|
|
4306
|
+
printError(`Process ${id} not found.`);
|
|
4291
4307
|
process.exit(1);
|
|
4292
4308
|
}
|
|
4293
4309
|
const changeRaw = flags['change'];
|
|
@@ -4311,7 +4327,7 @@ function cmdProcess(hippoRoot, args, flags) {
|
|
|
4311
4327
|
if (subcommand === 'close') {
|
|
4312
4328
|
const idRaw = args[1];
|
|
4313
4329
|
if (!idRaw) {
|
|
4314
|
-
|
|
4330
|
+
printError('Usage: hippo process close <id>');
|
|
4315
4331
|
process.exit(1);
|
|
4316
4332
|
}
|
|
4317
4333
|
const id = parsePositiveProcessId(idRaw);
|
|
@@ -4324,11 +4340,11 @@ function cmdProcess(hippoRoot, args, flags) {
|
|
|
4324
4340
|
// `new` keyword the name is args[1], otherwise args[0] IS the name.
|
|
4325
4341
|
const processName = subcommand === 'new' ? (args[1] ?? '') : subcommand;
|
|
4326
4342
|
if (!processName) {
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4343
|
+
printError('Usage: hippo process new "<name>" --step "<text>" [--step ...] [--description "<text>"]');
|
|
4344
|
+
printError(' hippo process list [--status active|superseded|closed|all] [--limit N]');
|
|
4345
|
+
printError(' hippo process get <id>');
|
|
4346
|
+
printError(' hippo process supersede <id> --step "<text>" [--change "<summary>"]');
|
|
4347
|
+
printError(' hippo process close <id>');
|
|
4332
4348
|
process.exit(1);
|
|
4333
4349
|
}
|
|
4334
4350
|
const steps = collectProcessSteps(flags['step']);
|
|
@@ -4351,7 +4367,7 @@ function parsePositivePolicyId(idRaw) {
|
|
|
4351
4367
|
const s = String(idRaw ?? '').trim();
|
|
4352
4368
|
const id = parseInt(s, 10);
|
|
4353
4369
|
if (!/^\d+$/.test(s) || id <= 0) {
|
|
4354
|
-
|
|
4370
|
+
printError(`Invalid policy id: "${idRaw}" (expected a positive integer).`);
|
|
4355
4371
|
process.exit(1);
|
|
4356
4372
|
}
|
|
4357
4373
|
return id;
|
|
@@ -4373,7 +4389,7 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4373
4389
|
const limitRaw = flags['limit'];
|
|
4374
4390
|
const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
|
|
4375
4391
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
4376
|
-
|
|
4392
|
+
printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
|
|
4377
4393
|
process.exit(1);
|
|
4378
4394
|
}
|
|
4379
4395
|
let results;
|
|
@@ -4382,7 +4398,7 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4382
4398
|
}
|
|
4383
4399
|
else {
|
|
4384
4400
|
if (!policiesModule.VALID_POLICY_STATES.has(status)) {
|
|
4385
|
-
|
|
4401
|
+
printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
|
|
4386
4402
|
process.exit(1);
|
|
4387
4403
|
}
|
|
4388
4404
|
results = policiesModule.loadPolicies(hippoRoot, tenantId, {
|
|
@@ -4402,7 +4418,7 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4402
4418
|
if (subcommand === 'asof') {
|
|
4403
4419
|
const dateRaw = args[1];
|
|
4404
4420
|
if (!dateRaw) {
|
|
4405
|
-
|
|
4421
|
+
printError('Usage: hippo policy asof <iso-date> [--name "<policy>"]');
|
|
4406
4422
|
process.exit(1);
|
|
4407
4423
|
}
|
|
4408
4424
|
const nameRaw = flags['name'];
|
|
@@ -4412,7 +4428,7 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4412
4428
|
results = policiesModule.loadPoliciesAsOf(hippoRoot, tenantId, dateRaw, { name });
|
|
4413
4429
|
}
|
|
4414
4430
|
catch (e) {
|
|
4415
|
-
|
|
4431
|
+
printError(e.message);
|
|
4416
4432
|
process.exit(1);
|
|
4417
4433
|
}
|
|
4418
4434
|
if (results.length === 0) {
|
|
@@ -4427,13 +4443,13 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4427
4443
|
if (subcommand === 'get') {
|
|
4428
4444
|
const idRaw = args[1];
|
|
4429
4445
|
if (!idRaw) {
|
|
4430
|
-
|
|
4446
|
+
printError('Usage: hippo policy get <id>');
|
|
4431
4447
|
process.exit(1);
|
|
4432
4448
|
}
|
|
4433
4449
|
const id = parsePositivePolicyId(idRaw);
|
|
4434
4450
|
const p = policiesModule.loadPolicyById(hippoRoot, tenantId, id);
|
|
4435
4451
|
if (!p) {
|
|
4436
|
-
|
|
4452
|
+
printError(`Policy ${id} not found.`);
|
|
4437
4453
|
process.exit(1);
|
|
4438
4454
|
}
|
|
4439
4455
|
console.log(`Policy #${p.id}`);
|
|
@@ -4459,18 +4475,18 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4459
4475
|
if (subcommand === 'supersede') {
|
|
4460
4476
|
const idRaw = args[1];
|
|
4461
4477
|
if (!idRaw) {
|
|
4462
|
-
|
|
4478
|
+
printError('Usage: hippo policy supersede <id> --text "<rule>" [--from <iso>] [--to <iso>] [--change "<summary>"]');
|
|
4463
4479
|
process.exit(1);
|
|
4464
4480
|
}
|
|
4465
4481
|
const id = parsePositivePolicyId(idRaw);
|
|
4466
4482
|
const textRaw = flags['text'];
|
|
4467
4483
|
if (typeof textRaw !== 'string' || !textRaw.trim()) {
|
|
4468
|
-
|
|
4484
|
+
printError('hippo policy supersede requires --text "<rule>" for the new version.');
|
|
4469
4485
|
process.exit(1);
|
|
4470
4486
|
}
|
|
4471
4487
|
const existing = policiesModule.loadPolicyById(hippoRoot, tenantId, id);
|
|
4472
4488
|
if (!existing) {
|
|
4473
|
-
|
|
4489
|
+
printError(`Policy ${id} not found.`);
|
|
4474
4490
|
process.exit(1);
|
|
4475
4491
|
}
|
|
4476
4492
|
const fromRaw = flags['from'];
|
|
@@ -4491,7 +4507,7 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4491
4507
|
console.log(` memory: ${created.memoryId}`);
|
|
4492
4508
|
}
|
|
4493
4509
|
catch (e) {
|
|
4494
|
-
|
|
4510
|
+
printError(e.message);
|
|
4495
4511
|
process.exit(1);
|
|
4496
4512
|
}
|
|
4497
4513
|
return;
|
|
@@ -4499,7 +4515,7 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4499
4515
|
if (subcommand === 'close') {
|
|
4500
4516
|
const idRaw = args[1];
|
|
4501
4517
|
if (!idRaw) {
|
|
4502
|
-
|
|
4518
|
+
printError('Usage: hippo policy close <id>');
|
|
4503
4519
|
process.exit(1);
|
|
4504
4520
|
}
|
|
4505
4521
|
const id = parsePositivePolicyId(idRaw);
|
|
@@ -4512,12 +4528,12 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4512
4528
|
const policyName = subcommand === 'new' ? (args[1] ?? '') : subcommand;
|
|
4513
4529
|
const textRaw = flags['text'];
|
|
4514
4530
|
if (!policyName || typeof textRaw !== 'string' || !textRaw.trim()) {
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4531
|
+
printError('Usage: hippo policy new "<name>" --text "<rule>" [--from <iso>] [--to <iso>]');
|
|
4532
|
+
printError(' hippo policy list [--status active|superseded|closed|all] [--limit N]');
|
|
4533
|
+
printError(' hippo policy get <id>');
|
|
4534
|
+
printError(' hippo policy asof <iso-date> [--name "<policy>"]');
|
|
4535
|
+
printError(' hippo policy supersede <id> --text "<rule>" [--from] [--to] [--change "<summary>"]');
|
|
4536
|
+
printError(' hippo policy close <id>');
|
|
4521
4537
|
process.exit(1);
|
|
4522
4538
|
}
|
|
4523
4539
|
const fromRaw = flags['from'];
|
|
@@ -4536,7 +4552,7 @@ function cmdPolicy(hippoRoot, args, flags) {
|
|
|
4536
4552
|
console.log(` memory: ${created.memoryId}`);
|
|
4537
4553
|
}
|
|
4538
4554
|
catch (e) {
|
|
4539
|
-
|
|
4555
|
+
printError(e.message);
|
|
4540
4556
|
process.exit(1);
|
|
4541
4557
|
}
|
|
4542
4558
|
}
|
|
@@ -4546,7 +4562,7 @@ function parsePositiveSkillId(idRaw) {
|
|
|
4546
4562
|
const s = String(idRaw ?? '').trim();
|
|
4547
4563
|
const id = parseInt(s, 10);
|
|
4548
4564
|
if (!/^\d+$/.test(s) || id <= 0) {
|
|
4549
|
-
|
|
4565
|
+
printError(`Invalid skill id: "${idRaw}" (expected a positive integer).`);
|
|
4550
4566
|
process.exit(1);
|
|
4551
4567
|
}
|
|
4552
4568
|
return id;
|
|
@@ -4568,7 +4584,7 @@ function cmdSkill(hippoRoot, args, flags) {
|
|
|
4568
4584
|
const limitRaw = flags['limit'];
|
|
4569
4585
|
const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
|
|
4570
4586
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
4571
|
-
|
|
4587
|
+
printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
|
|
4572
4588
|
process.exit(1);
|
|
4573
4589
|
}
|
|
4574
4590
|
let results;
|
|
@@ -4577,7 +4593,7 @@ function cmdSkill(hippoRoot, args, flags) {
|
|
|
4577
4593
|
}
|
|
4578
4594
|
else {
|
|
4579
4595
|
if (!skillsModule.VALID_SKILL_STATES.has(status)) {
|
|
4580
|
-
|
|
4596
|
+
printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
|
|
4581
4597
|
process.exit(1);
|
|
4582
4598
|
}
|
|
4583
4599
|
results = skillsModule.loadSkills(hippoRoot, tenantId, {
|
|
@@ -4606,13 +4622,13 @@ function cmdSkill(hippoRoot, args, flags) {
|
|
|
4606
4622
|
if (subcommand === 'get') {
|
|
4607
4623
|
const idRaw = args[1];
|
|
4608
4624
|
if (!idRaw) {
|
|
4609
|
-
|
|
4625
|
+
printError('Usage: hippo skill get <id>');
|
|
4610
4626
|
process.exit(1);
|
|
4611
4627
|
}
|
|
4612
4628
|
const id = parsePositiveSkillId(idRaw);
|
|
4613
4629
|
const s = skillsModule.loadSkillById(hippoRoot, tenantId, id);
|
|
4614
4630
|
if (!s) {
|
|
4615
|
-
|
|
4631
|
+
printError(`Skill ${id} not found.`);
|
|
4616
4632
|
process.exit(1);
|
|
4617
4633
|
}
|
|
4618
4634
|
console.log(`Skill #${s.id}`);
|
|
@@ -4638,18 +4654,18 @@ function cmdSkill(hippoRoot, args, flags) {
|
|
|
4638
4654
|
if (subcommand === 'supersede') {
|
|
4639
4655
|
const idRaw = args[1];
|
|
4640
4656
|
if (!idRaw) {
|
|
4641
|
-
|
|
4657
|
+
printError('Usage: hippo skill supersede <id> --instructions "<text>" [--trigger "<when>"] [--change "<summary>"]');
|
|
4642
4658
|
process.exit(1);
|
|
4643
4659
|
}
|
|
4644
4660
|
const id = parsePositiveSkillId(idRaw);
|
|
4645
4661
|
const instrRaw = flags['instructions'];
|
|
4646
4662
|
if (typeof instrRaw !== 'string' || !instrRaw.trim()) {
|
|
4647
|
-
|
|
4663
|
+
printError('hippo skill supersede requires --instructions "<text>" for the new version.');
|
|
4648
4664
|
process.exit(1);
|
|
4649
4665
|
}
|
|
4650
4666
|
const existing = skillsModule.loadSkillById(hippoRoot, tenantId, id);
|
|
4651
4667
|
if (!existing) {
|
|
4652
|
-
|
|
4668
|
+
printError(`Skill ${id} not found.`);
|
|
4653
4669
|
process.exit(1);
|
|
4654
4670
|
}
|
|
4655
4671
|
const trigRaw = flags['trigger'];
|
|
@@ -4668,7 +4684,7 @@ function cmdSkill(hippoRoot, args, flags) {
|
|
|
4668
4684
|
console.log(` memory: ${created.memoryId}`);
|
|
4669
4685
|
}
|
|
4670
4686
|
catch (e) {
|
|
4671
|
-
|
|
4687
|
+
printError(e.message);
|
|
4672
4688
|
process.exit(1);
|
|
4673
4689
|
}
|
|
4674
4690
|
return;
|
|
@@ -4676,7 +4692,7 @@ function cmdSkill(hippoRoot, args, flags) {
|
|
|
4676
4692
|
if (subcommand === 'close') {
|
|
4677
4693
|
const idRaw = args[1];
|
|
4678
4694
|
if (!idRaw) {
|
|
4679
|
-
|
|
4695
|
+
printError('Usage: hippo skill close <id>');
|
|
4680
4696
|
process.exit(1);
|
|
4681
4697
|
}
|
|
4682
4698
|
const id = parsePositiveSkillId(idRaw);
|
|
@@ -4689,12 +4705,12 @@ function cmdSkill(hippoRoot, args, flags) {
|
|
|
4689
4705
|
const skillName = subcommand === 'new' ? (args[1] ?? '') : subcommand;
|
|
4690
4706
|
const instrRaw = flags['instructions'];
|
|
4691
4707
|
if (!skillName || typeof instrRaw !== 'string' || !instrRaw.trim()) {
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4708
|
+
printError('Usage: hippo skill new "<name>" --instructions "<text>" [--trigger "<when>"]');
|
|
4709
|
+
printError(' hippo skill list [--status active|superseded|closed|all] [--limit N]');
|
|
4710
|
+
printError(' hippo skill get <id>');
|
|
4711
|
+
printError(' hippo skill export (render active skills as an AGENTS.md/CLAUDE.md block)');
|
|
4712
|
+
printError(' hippo skill supersede <id> --instructions "<text>" [--trigger] [--change "<summary>"]');
|
|
4713
|
+
printError(' hippo skill close <id>');
|
|
4698
4714
|
process.exit(1);
|
|
4699
4715
|
}
|
|
4700
4716
|
const trigRaw = flags['trigger'];
|
|
@@ -4710,7 +4726,7 @@ function cmdSkill(hippoRoot, args, flags) {
|
|
|
4710
4726
|
console.log(` memory: ${created.memoryId}`);
|
|
4711
4727
|
}
|
|
4712
4728
|
catch (e) {
|
|
4713
|
-
|
|
4729
|
+
printError(e.message);
|
|
4714
4730
|
process.exit(1);
|
|
4715
4731
|
}
|
|
4716
4732
|
}
|
|
@@ -4718,7 +4734,7 @@ function parsePositiveBriefId(idRaw) {
|
|
|
4718
4734
|
const s = String(idRaw ?? '').trim();
|
|
4719
4735
|
const id = parseInt(s, 10);
|
|
4720
4736
|
if (!/^\d+$/.test(s) || id <= 0) {
|
|
4721
|
-
|
|
4737
|
+
printError(`Invalid brief id: "${idRaw}" (expected a positive integer).`);
|
|
4722
4738
|
process.exit(1);
|
|
4723
4739
|
}
|
|
4724
4740
|
return id;
|
|
@@ -4729,12 +4745,12 @@ function printBriefRow(b) {
|
|
|
4729
4745
|
console.log(` change: ${b.changeSummary}`);
|
|
4730
4746
|
}
|
|
4731
4747
|
function briefUsage() {
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4748
|
+
printError('Usage: hippo brief new "<repo>" --summary "<text>"');
|
|
4749
|
+
printError(' hippo brief list [--status active|superseded|closed|all] [--repo "<repo>"] [--limit N]');
|
|
4750
|
+
printError(' hippo brief get <id>');
|
|
4751
|
+
printError(' hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
|
|
4752
|
+
printError(' hippo brief close <id>');
|
|
4753
|
+
printError(' hippo brief refresh "<repo>" [--dry-run] (auto-assemble the brief from the repo\'s receipts)');
|
|
4738
4754
|
}
|
|
4739
4755
|
function cmdProjectBrief(hippoRoot, args, flags) {
|
|
4740
4756
|
requireInit(hippoRoot);
|
|
@@ -4748,13 +4764,13 @@ function cmdProjectBrief(hippoRoot, args, flags) {
|
|
|
4748
4764
|
const limitRaw = flags['limit'];
|
|
4749
4765
|
const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
|
|
4750
4766
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
4751
|
-
|
|
4767
|
+
printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
|
|
4752
4768
|
process.exit(1);
|
|
4753
4769
|
}
|
|
4754
4770
|
const opts = { limit, repo };
|
|
4755
4771
|
if (status !== 'all') {
|
|
4756
4772
|
if (!briefsModule.VALID_BRIEF_STATES.has(status)) {
|
|
4757
|
-
|
|
4773
|
+
printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
|
|
4758
4774
|
process.exit(1);
|
|
4759
4775
|
}
|
|
4760
4776
|
opts.status = status;
|
|
@@ -4772,14 +4788,14 @@ function cmdProjectBrief(hippoRoot, args, flags) {
|
|
|
4772
4788
|
if (subcommand === 'refresh') {
|
|
4773
4789
|
const repoRaw = args[1];
|
|
4774
4790
|
if (!repoRaw) {
|
|
4775
|
-
|
|
4791
|
+
printError('Usage: hippo brief refresh "<repo>" [--dry-run]');
|
|
4776
4792
|
process.exit(1);
|
|
4777
4793
|
}
|
|
4778
4794
|
const dryRun = Boolean(flags['dry-run']);
|
|
4779
4795
|
try {
|
|
4780
4796
|
if (dryRun) {
|
|
4781
4797
|
const { markdown, receiptCount } = briefsModule.assembleBriefFromReceipts(hippoRoot, tenantId, repoRaw);
|
|
4782
|
-
|
|
4798
|
+
printError(`(dry-run: assembled from ${receiptCount} receipt(s); brief NOT written)`);
|
|
4783
4799
|
console.log(markdown);
|
|
4784
4800
|
return;
|
|
4785
4801
|
}
|
|
@@ -4791,7 +4807,7 @@ function cmdProjectBrief(hippoRoot, args, flags) {
|
|
|
4791
4807
|
console.log(` memory: ${created.memoryId}`);
|
|
4792
4808
|
}
|
|
4793
4809
|
catch (e) {
|
|
4794
|
-
|
|
4810
|
+
printError(e.message);
|
|
4795
4811
|
process.exit(1);
|
|
4796
4812
|
}
|
|
4797
4813
|
return;
|
|
@@ -4799,13 +4815,13 @@ function cmdProjectBrief(hippoRoot, args, flags) {
|
|
|
4799
4815
|
if (subcommand === 'get') {
|
|
4800
4816
|
const idRaw = args[1];
|
|
4801
4817
|
if (!idRaw) {
|
|
4802
|
-
|
|
4818
|
+
printError('Usage: hippo brief get <id>');
|
|
4803
4819
|
process.exit(1);
|
|
4804
4820
|
}
|
|
4805
4821
|
const id = parsePositiveBriefId(idRaw);
|
|
4806
4822
|
const b = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
|
|
4807
4823
|
if (!b) {
|
|
4808
|
-
|
|
4824
|
+
printError(`Project brief ${id} not found.`);
|
|
4809
4825
|
process.exit(1);
|
|
4810
4826
|
}
|
|
4811
4827
|
console.log(`Project brief #${b.id}`);
|
|
@@ -4829,18 +4845,18 @@ function cmdProjectBrief(hippoRoot, args, flags) {
|
|
|
4829
4845
|
if (subcommand === 'supersede') {
|
|
4830
4846
|
const idRaw = args[1];
|
|
4831
4847
|
if (!idRaw) {
|
|
4832
|
-
|
|
4848
|
+
printError('Usage: hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
|
|
4833
4849
|
process.exit(1);
|
|
4834
4850
|
}
|
|
4835
4851
|
const id = parsePositiveBriefId(idRaw);
|
|
4836
4852
|
const summaryRaw = flags['summary'];
|
|
4837
4853
|
if (typeof summaryRaw !== 'string' || !summaryRaw.trim()) {
|
|
4838
|
-
|
|
4854
|
+
printError('hippo brief supersede requires --summary "<text>" for the new version.');
|
|
4839
4855
|
process.exit(1);
|
|
4840
4856
|
}
|
|
4841
4857
|
const existing = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
|
|
4842
4858
|
if (!existing) {
|
|
4843
|
-
|
|
4859
|
+
printError(`Project brief ${id} not found.`);
|
|
4844
4860
|
process.exit(1);
|
|
4845
4861
|
}
|
|
4846
4862
|
const changeRaw = flags['change'];
|
|
@@ -4857,7 +4873,7 @@ function cmdProjectBrief(hippoRoot, args, flags) {
|
|
|
4857
4873
|
console.log(` memory: ${created.memoryId}`);
|
|
4858
4874
|
}
|
|
4859
4875
|
catch (e) {
|
|
4860
|
-
|
|
4876
|
+
printError(e.message);
|
|
4861
4877
|
process.exit(1);
|
|
4862
4878
|
}
|
|
4863
4879
|
return;
|
|
@@ -4865,7 +4881,7 @@ function cmdProjectBrief(hippoRoot, args, flags) {
|
|
|
4865
4881
|
if (subcommand === 'close') {
|
|
4866
4882
|
const idRaw = args[1];
|
|
4867
4883
|
if (!idRaw) {
|
|
4868
|
-
|
|
4884
|
+
printError('Usage: hippo brief close <id>');
|
|
4869
4885
|
process.exit(1);
|
|
4870
4886
|
}
|
|
4871
4887
|
const id = parsePositiveBriefId(idRaw);
|
|
@@ -4892,7 +4908,7 @@ function cmdProjectBrief(hippoRoot, args, flags) {
|
|
|
4892
4908
|
console.log(` memory: ${created.memoryId}`);
|
|
4893
4909
|
}
|
|
4894
4910
|
catch (e) {
|
|
4895
|
-
|
|
4911
|
+
printError(e.message);
|
|
4896
4912
|
process.exit(1);
|
|
4897
4913
|
}
|
|
4898
4914
|
}
|
|
@@ -4900,7 +4916,7 @@ function parsePositiveNoteId(idRaw) {
|
|
|
4900
4916
|
const s = String(idRaw ?? '').trim();
|
|
4901
4917
|
const id = parseInt(s, 10);
|
|
4902
4918
|
if (!/^\d+$/.test(s) || id <= 0) {
|
|
4903
|
-
|
|
4919
|
+
printError(`Invalid note id: "${idRaw}" (expected a positive integer).`);
|
|
4904
4920
|
process.exit(1);
|
|
4905
4921
|
}
|
|
4906
4922
|
return id;
|
|
@@ -4911,11 +4927,11 @@ function printNoteRow(n) {
|
|
|
4911
4927
|
console.log(` change: ${n.changeSummary}`);
|
|
4912
4928
|
}
|
|
4913
4929
|
function noteUsage() {
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4930
|
+
printError('Usage: hippo note new "<customer>" --text "<note>"');
|
|
4931
|
+
printError(' hippo note list [--status active|superseded|closed|all] [--customer "<id>"] [--limit N]');
|
|
4932
|
+
printError(' hippo note get <id>');
|
|
4933
|
+
printError(' hippo note supersede <id> --text "<note>" [--change "<summary>"]');
|
|
4934
|
+
printError(' hippo note close <id>');
|
|
4919
4935
|
}
|
|
4920
4936
|
function cmdGraph(hippoRoot, args, flags) {
|
|
4921
4937
|
requireInit(hippoRoot);
|
|
@@ -4929,7 +4945,7 @@ function cmdGraph(hippoRoot, args, flags) {
|
|
|
4929
4945
|
const supersedes = result.relations - result.references;
|
|
4930
4946
|
console.log(`Graph extracted: ${result.entities} entities (${byType}) + ${result.relations} relations (${supersedes} supersedes, ${result.references} references).`);
|
|
4931
4947
|
if (result.truncated.length > 0) {
|
|
4932
|
-
|
|
4948
|
+
printError(`WARNING: under-extracted (hit the per-type cap): ${result.truncated.join(', ')}. The graph is incomplete for those types.`);
|
|
4933
4949
|
}
|
|
4934
4950
|
return;
|
|
4935
4951
|
}
|
|
@@ -4968,7 +4984,7 @@ function cmdGraph(hippoRoot, args, flags) {
|
|
|
4968
4984
|
if (subcommand === 'view') {
|
|
4969
4985
|
const format = typeof flags['format'] === 'string' ? flags['format'] : 'html';
|
|
4970
4986
|
if (format !== 'html' && format !== 'canvas') {
|
|
4971
|
-
|
|
4987
|
+
printError("graph view: --format must be 'html' or 'canvas'");
|
|
4972
4988
|
process.exit(1);
|
|
4973
4989
|
}
|
|
4974
4990
|
const model = buildGraphModel(hippoRoot, tenantId, { entity, limit: DEFAULT_VIEW_LIMIT });
|
|
@@ -4998,7 +5014,7 @@ function cmdGraph(hippoRoot, args, flags) {
|
|
|
4998
5014
|
}
|
|
4999
5015
|
return;
|
|
5000
5016
|
}
|
|
5001
|
-
|
|
5017
|
+
printError('Usage:\n' +
|
|
5002
5018
|
' hippo graph extract Rebuild the entity/relation graph from consolidated objects\n' +
|
|
5003
5019
|
' hippo graph show [--entity NAME] [--json] Inspect entities + their edges (text or JSON)\n' +
|
|
5004
5020
|
' hippo graph view [--out FILE] [--open] [--format html|canvas] [--entity NAME] Generate an interactive node-link diagram');
|
|
@@ -5016,13 +5032,13 @@ function cmdCustomerNote(hippoRoot, args, flags) {
|
|
|
5016
5032
|
const limitRaw = flags['limit'];
|
|
5017
5033
|
const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
|
|
5018
5034
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
5019
|
-
|
|
5035
|
+
printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
|
|
5020
5036
|
process.exit(1);
|
|
5021
5037
|
}
|
|
5022
5038
|
const opts = { limit, customer };
|
|
5023
5039
|
if (status !== 'all') {
|
|
5024
5040
|
if (!customerNotesModule.VALID_NOTE_STATES.has(status)) {
|
|
5025
|
-
|
|
5041
|
+
printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
|
|
5026
5042
|
process.exit(1);
|
|
5027
5043
|
}
|
|
5028
5044
|
opts.status = status;
|
|
@@ -5040,13 +5056,13 @@ function cmdCustomerNote(hippoRoot, args, flags) {
|
|
|
5040
5056
|
if (subcommand === 'get') {
|
|
5041
5057
|
const idRaw = args[1];
|
|
5042
5058
|
if (!idRaw) {
|
|
5043
|
-
|
|
5059
|
+
printError('Usage: hippo note get <id>');
|
|
5044
5060
|
process.exit(1);
|
|
5045
5061
|
}
|
|
5046
5062
|
const id = parsePositiveNoteId(idRaw);
|
|
5047
5063
|
const n = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
|
|
5048
5064
|
if (!n) {
|
|
5049
|
-
|
|
5065
|
+
printError(`Customer note ${id} not found.`);
|
|
5050
5066
|
process.exit(1);
|
|
5051
5067
|
}
|
|
5052
5068
|
console.log(`Customer note #${n.id}`);
|
|
@@ -5070,18 +5086,18 @@ function cmdCustomerNote(hippoRoot, args, flags) {
|
|
|
5070
5086
|
if (subcommand === 'supersede') {
|
|
5071
5087
|
const idRaw = args[1];
|
|
5072
5088
|
if (!idRaw) {
|
|
5073
|
-
|
|
5089
|
+
printError('Usage: hippo note supersede <id> --text "<note>" [--change "<summary>"]');
|
|
5074
5090
|
process.exit(1);
|
|
5075
5091
|
}
|
|
5076
5092
|
const id = parsePositiveNoteId(idRaw);
|
|
5077
5093
|
const textRaw = flags['text'];
|
|
5078
5094
|
if (typeof textRaw !== 'string' || !textRaw.trim()) {
|
|
5079
|
-
|
|
5095
|
+
printError('hippo note supersede requires --text "<note>" for the new version.');
|
|
5080
5096
|
process.exit(1);
|
|
5081
5097
|
}
|
|
5082
5098
|
const existing = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
|
|
5083
5099
|
if (!existing) {
|
|
5084
|
-
|
|
5100
|
+
printError(`Customer note ${id} not found.`);
|
|
5085
5101
|
process.exit(1);
|
|
5086
5102
|
}
|
|
5087
5103
|
const changeRaw = flags['change'];
|
|
@@ -5098,7 +5114,7 @@ function cmdCustomerNote(hippoRoot, args, flags) {
|
|
|
5098
5114
|
console.log(` memory: ${created.memoryId}`);
|
|
5099
5115
|
}
|
|
5100
5116
|
catch (e) {
|
|
5101
|
-
|
|
5117
|
+
printError(e.message);
|
|
5102
5118
|
process.exit(1);
|
|
5103
5119
|
}
|
|
5104
5120
|
return;
|
|
@@ -5106,7 +5122,7 @@ function cmdCustomerNote(hippoRoot, args, flags) {
|
|
|
5106
5122
|
if (subcommand === 'close') {
|
|
5107
5123
|
const idRaw = args[1];
|
|
5108
5124
|
if (!idRaw) {
|
|
5109
|
-
|
|
5125
|
+
printError('Usage: hippo note close <id>');
|
|
5110
5126
|
process.exit(1);
|
|
5111
5127
|
}
|
|
5112
5128
|
const id = parsePositiveNoteId(idRaw);
|
|
@@ -5133,7 +5149,7 @@ function cmdCustomerNote(hippoRoot, args, flags) {
|
|
|
5133
5149
|
console.log(` memory: ${created.memoryId}`);
|
|
5134
5150
|
}
|
|
5135
5151
|
catch (e) {
|
|
5136
|
-
|
|
5152
|
+
printError(e.message);
|
|
5137
5153
|
process.exit(1);
|
|
5138
5154
|
}
|
|
5139
5155
|
}
|
|
@@ -5188,9 +5204,21 @@ function cmdCurrent(hippoRoot, args, flags) {
|
|
|
5188
5204
|
}
|
|
5189
5205
|
return;
|
|
5190
5206
|
}
|
|
5191
|
-
|
|
5207
|
+
printError('Usage: hippo current <show>');
|
|
5192
5208
|
process.exit(1);
|
|
5193
5209
|
}
|
|
5210
|
+
/** Hook commands share one handle per store and wait at most HOOK_DB_WAIT_MS for a lock; a store still busy after that skips the hook's work with one warning, exit 0. */
|
|
5211
|
+
async function runHookWithStores(fn) {
|
|
5212
|
+
try {
|
|
5213
|
+
return await withSharedStoreHandles(fn, { busyWaitMs: HOOK_DB_WAIT_MS });
|
|
5214
|
+
}
|
|
5215
|
+
catch (error) {
|
|
5216
|
+
if (!isSqliteBusy(error))
|
|
5217
|
+
throw error;
|
|
5218
|
+
noteStoreBusy('hook skipped');
|
|
5219
|
+
return undefined;
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5194
5222
|
async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
5195
5223
|
const rec = startDeliveryRecorder(hippoRoot, flags, stdinText);
|
|
5196
5224
|
// No try/finally: a render throw keeps its own exit code and writes no event.
|
|
@@ -5216,7 +5244,8 @@ function startDeliveryRecorder(hippoRoot, flags, stdinText) {
|
|
|
5216
5244
|
});
|
|
5217
5245
|
}
|
|
5218
5246
|
catch (error) {
|
|
5219
|
-
|
|
5247
|
+
// The hook's one-line stderr contract pins this exact text, so it bypasses the leveled logger.
|
|
5248
|
+
printError(`[hippo] delivery ledger skipped:${error instanceof Error ? error.message : String(error)}`);
|
|
5220
5249
|
return null;
|
|
5221
5250
|
}
|
|
5222
5251
|
}
|
|
@@ -5228,9 +5257,26 @@ function flushDeliveryRecorder(rec, db) {
|
|
|
5228
5257
|
rec.flush((input) => (db ? writeDeliveryEventOnHandle(db, input) : writeDeliveryEventAtRoot(rec.root, input)));
|
|
5229
5258
|
}
|
|
5230
5259
|
catch (error) {
|
|
5231
|
-
|
|
5260
|
+
// Pinned stderr text, as in the recorder build above.
|
|
5261
|
+
printError(`[hippo] delivery ledger write failed:${error instanceof Error ? error.message : String(error)}`);
|
|
5232
5262
|
}
|
|
5233
5263
|
}
|
|
5264
|
+
/**
|
|
5265
|
+
* Whether this session sits in the pilot's holdout arm (src/pilot-arm.ts). Off at rate 0 and with no session id.
|
|
5266
|
+
* `write` books the arm row; a read-only caller (env-only id, sub-agent) follows the stored arm, else the hash.
|
|
5267
|
+
*/
|
|
5268
|
+
function inPilotHoldout(hippoRoot, tenantId, sessionId, write) {
|
|
5269
|
+
if (sessionId === undefined || sessionId.trim() === '')
|
|
5270
|
+
return false;
|
|
5271
|
+
const root = isInitialized(hippoRoot) ? hippoRoot : isInitialized(getGlobalRoot()) ? getGlobalRoot() : null;
|
|
5272
|
+
if (root === null)
|
|
5273
|
+
return false;
|
|
5274
|
+
const rate = loadConfig(root).pilot.holdoutRateBp;
|
|
5275
|
+
if (rate <= 0)
|
|
5276
|
+
return false;
|
|
5277
|
+
const arm = withLedgerDb(hippoRoot, (db) => write ? ensurePilotArm(db, tenantId, sessionId, rate) : readPilotArm(db, sessionId) ?? hashArm(sessionId, rate));
|
|
5278
|
+
return (arm ?? hashArm(sessionId, rate)) === 'holdout';
|
|
5279
|
+
}
|
|
5234
5280
|
async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
5235
5281
|
// --pinned-only fires on every UserPromptSubmit — including in directories
|
|
5236
5282
|
// that don't have a local .hippo. Skip requireInit for that path and fall
|
|
@@ -5240,30 +5286,6 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
|
5240
5286
|
if (!pinnedOnly) {
|
|
5241
5287
|
requireInit(hippoRoot);
|
|
5242
5288
|
}
|
|
5243
|
-
const budget = parseBudgetFlag(flags['budget'], 1500);
|
|
5244
|
-
if (budget <= 0) {
|
|
5245
|
-
rec?.disabled();
|
|
5246
|
-
return;
|
|
5247
|
-
}
|
|
5248
|
-
// Resolve query: explicit args, --auto (git diff via CLI-side helper), or
|
|
5249
|
-
// fall through to api.getContext's '*' fallback. api.getContext is host-
|
|
5250
|
-
// agnostic so the auto-detect (which shells out to git) stays CLI-side.
|
|
5251
|
-
let query = args.join(' ').trim();
|
|
5252
|
-
if (!query && flags['auto']) {
|
|
5253
|
-
query = autoDetectContext();
|
|
5254
|
-
}
|
|
5255
|
-
// Scope detection (CLI-side: uses cwd). api.getContext takes the resolved
|
|
5256
|
-
// scope via opts.scope to stay host-agnostic.
|
|
5257
|
-
const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
5258
|
-
const ctxActiveScope = ctxExplicitScope || detectScope();
|
|
5259
|
-
const ctx = {
|
|
5260
|
-
hippoRoot,
|
|
5261
|
-
tenantId: resolveTenantId({}),
|
|
5262
|
-
actor: api.adminActor('cli'),
|
|
5263
|
-
};
|
|
5264
|
-
// v39 memory scope isolation: --cross-project re-includes other-project
|
|
5265
|
-
// memories (rendered under a demarcated section below).
|
|
5266
|
-
const crossProject = flags['cross-project'] === true;
|
|
5267
5289
|
// DF1 T2: resolve the calling session's id for the bounded active-task-
|
|
5268
5290
|
// snapshot read (api.getContext -> loadFreshActiveTaskSnapshot). Stdin
|
|
5269
5291
|
// payload (the UserPromptSubmit hook JSON) wins; falls back to
|
|
@@ -5291,6 +5313,36 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
|
5291
5313
|
const ledgerSessionId = subagent ? undefined : currentSessionId;
|
|
5292
5314
|
if (subagent)
|
|
5293
5315
|
payloadSessionId = undefined;
|
|
5316
|
+
// The pilot arm is booked at the first hook call whatever the flags, so the holdout sees no budget or content branch.
|
|
5317
|
+
const resolvedTenant = resolveTenantId({});
|
|
5318
|
+
if (inPilotHoldout(hippoRoot, resolvedTenant, currentSessionId, payloadSessionId !== undefined)) {
|
|
5319
|
+
rec?.disabled();
|
|
5320
|
+
return;
|
|
5321
|
+
}
|
|
5322
|
+
const budget = parseBudgetFlag(flags['budget'], 1500);
|
|
5323
|
+
if (budget <= 0) {
|
|
5324
|
+
rec?.disabled();
|
|
5325
|
+
return;
|
|
5326
|
+
}
|
|
5327
|
+
// Resolve query: explicit args, --auto (git diff via CLI-side helper), or
|
|
5328
|
+
// fall through to api.getContext's '*' fallback. api.getContext is host-
|
|
5329
|
+
// agnostic so the auto-detect (which shells out to git) stays CLI-side.
|
|
5330
|
+
let query = args.join(' ').trim();
|
|
5331
|
+
if (!query && flags['auto']) {
|
|
5332
|
+
query = autoDetectContext();
|
|
5333
|
+
}
|
|
5334
|
+
// Scope detection (CLI-side: uses cwd). api.getContext takes the resolved
|
|
5335
|
+
// scope via opts.scope to stay host-agnostic.
|
|
5336
|
+
const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
5337
|
+
const ctxActiveScope = ctxExplicitScope || detectScope();
|
|
5338
|
+
const ctx = {
|
|
5339
|
+
hippoRoot,
|
|
5340
|
+
tenantId: resolvedTenant,
|
|
5341
|
+
actor: api.adminActor('cli'),
|
|
5342
|
+
};
|
|
5343
|
+
// v39 memory scope isolation: --cross-project re-includes other-project
|
|
5344
|
+
// memories (rendered under a demarcated section below).
|
|
5345
|
+
const crossProject = flags['cross-project'] === true;
|
|
5294
5346
|
const format = String(flags['format'] ?? 'markdown');
|
|
5295
5347
|
const framing = String(flags['framing'] ?? 'observe');
|
|
5296
5348
|
const opts = {
|
|
@@ -5447,8 +5499,12 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
|
5447
5499
|
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface, event: 'inject',
|
|
5448
5500
|
items: staticItems.length, tokens: estimateTokens(finalStatic), hash: blockHash(finalStatic),
|
|
5449
5501
|
});
|
|
5502
|
+
// Best-effort row: only a busy store is actionable, and a ledger failure must not break the hook.
|
|
5503
|
+
}
|
|
5504
|
+
catch (error) {
|
|
5505
|
+
if (isSqliteBusy(error))
|
|
5506
|
+
noteStoreBusy('token ledger row skipped');
|
|
5450
5507
|
}
|
|
5451
|
-
catch { /* best effort; see withLedgerDb doc comment */ }
|
|
5452
5508
|
}
|
|
5453
5509
|
if (recallBlock) {
|
|
5454
5510
|
try {
|
|
@@ -5456,8 +5512,12 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
|
5456
5512
|
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: 'hook_recall', event: 'inject',
|
|
5457
5513
|
items: recallItems.length, tokens: estimateTokens(recallBlock), hash: blockHash(recallBlock),
|
|
5458
5514
|
});
|
|
5515
|
+
// Same best-effort rule as the inject row above.
|
|
5516
|
+
}
|
|
5517
|
+
catch (error) {
|
|
5518
|
+
if (isSqliteBusy(error))
|
|
5519
|
+
noteStoreBusy('token ledger row skipped');
|
|
5459
5520
|
}
|
|
5460
|
-
catch { /* best effort; see withLedgerDb doc comment */ }
|
|
5461
5521
|
}
|
|
5462
5522
|
flushDeliveryRecorder(rec, db);
|
|
5463
5523
|
});
|
|
@@ -5560,7 +5620,7 @@ async function cmdEmbed(hippoRoot, flags) {
|
|
|
5560
5620
|
return resolveEmbeddingProvider(root);
|
|
5561
5621
|
}
|
|
5562
5622
|
catch (err) {
|
|
5563
|
-
|
|
5623
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
5564
5624
|
return null;
|
|
5565
5625
|
}
|
|
5566
5626
|
})();
|
|
@@ -5578,8 +5638,8 @@ async function cmdEmbed(hippoRoot, flags) {
|
|
|
5578
5638
|
console.log(' npm install @huggingface/transformers');
|
|
5579
5639
|
}
|
|
5580
5640
|
else {
|
|
5581
|
-
|
|
5582
|
-
|
|
5641
|
+
printError(`Embedding provider '${embedProvider.kind}' is configured but ${embedProvider.keyEnv} is not set.`);
|
|
5642
|
+
printError(`Export ${embedProvider.keyEnv}, or set config.embeddings.provider back to 'local'.`);
|
|
5583
5643
|
process.exitCode = 1;
|
|
5584
5644
|
}
|
|
5585
5645
|
return;
|
|
@@ -5590,9 +5650,9 @@ async function cmdEmbed(hippoRoot, flags) {
|
|
|
5590
5650
|
count = await embedAll(root, resolveEmbeddingModel(root));
|
|
5591
5651
|
}
|
|
5592
5652
|
catch (err) {
|
|
5593
|
-
|
|
5653
|
+
printError(`Embedding failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5594
5654
|
const partial = loadEmbeddingIndex(root);
|
|
5595
|
-
|
|
5655
|
+
printError(`Partial progress saved: ${Object.keys(partial).length} embeddings on disk. Re-run \`hippo embed\` to resume.`);
|
|
5596
5656
|
process.exitCode = 1;
|
|
5597
5657
|
return;
|
|
5598
5658
|
}
|
|
@@ -5605,7 +5665,7 @@ async function cmdEmbed(hippoRoot, flags) {
|
|
|
5605
5665
|
// ---------------------------------------------------------------------------
|
|
5606
5666
|
async function cmdWatch(command, hippoRoot) {
|
|
5607
5667
|
if (!command) {
|
|
5608
|
-
|
|
5668
|
+
printError('Usage: hippo watch "<command>"');
|
|
5609
5669
|
process.exit(1);
|
|
5610
5670
|
}
|
|
5611
5671
|
const { exitCode, stderr } = await runWatched(command);
|
|
@@ -5615,7 +5675,7 @@ async function cmdWatch(command, hippoRoot) {
|
|
|
5615
5675
|
}
|
|
5616
5676
|
// Only create memory if hippo is initialized
|
|
5617
5677
|
if (!isInitialized(hippoRoot)) {
|
|
5618
|
-
|
|
5678
|
+
printError('Command failed but .hippo not initialized. Run `hippo init` to enable auto-learn.');
|
|
5619
5679
|
process.exit(exitCode);
|
|
5620
5680
|
}
|
|
5621
5681
|
const entry = captureError(exitCode, stderr, command, resolveTenantId({}));
|
|
@@ -5634,11 +5694,11 @@ async function cmdWatch(command, hippoRoot) {
|
|
|
5634
5694
|
updateStats(hippoRoot, { remembered: 1 });
|
|
5635
5695
|
void embedMemory(hippoRoot, entry);
|
|
5636
5696
|
const preview = stderr.trim().slice(0, 80);
|
|
5637
|
-
|
|
5697
|
+
printError(`\nHippo learned from failure: "${preview}"`);
|
|
5638
5698
|
}
|
|
5639
5699
|
catch (err) {
|
|
5640
5700
|
if (err instanceof RejectedValueError) {
|
|
5641
|
-
|
|
5701
|
+
printError(`\nHippo: this failure matches a rejected value (${err.reason ?? 'no reason given'}); not stored.`);
|
|
5642
5702
|
}
|
|
5643
5703
|
else {
|
|
5644
5704
|
throw err;
|
|
@@ -5652,7 +5712,7 @@ async function cmdWatch(command, hippoRoot) {
|
|
|
5652
5712
|
function cmdLearn(hippoRoot, flags) {
|
|
5653
5713
|
requireInit(hippoRoot);
|
|
5654
5714
|
if (!flags['git']) {
|
|
5655
|
-
|
|
5715
|
+
printError('Usage: hippo learn --git [--days <n>] [--repos <paths>]');
|
|
5656
5716
|
process.exit(1);
|
|
5657
5717
|
}
|
|
5658
5718
|
const days = parseInt(String(flags['days'] ?? '7'), 10);
|
|
@@ -5678,6 +5738,10 @@ function cmdLearn(hippoRoot, flags) {
|
|
|
5678
5738
|
// ---------------------------------------------------------------------------
|
|
5679
5739
|
// Import command
|
|
5680
5740
|
// ---------------------------------------------------------------------------
|
|
5741
|
+
function warnRedacted(count) {
|
|
5742
|
+
if (count)
|
|
5743
|
+
printError(`Warning: secret-shaped text was redacted from ${count} imported ${count === 1 ? 'entry' : 'entries'} before storing`);
|
|
5744
|
+
}
|
|
5681
5745
|
function cmdImport(hippoRoot, args, flags) {
|
|
5682
5746
|
const useGlobal = Boolean(flags['global']);
|
|
5683
5747
|
const dryRun = Boolean(flags['dry-run']);
|
|
@@ -5696,7 +5760,7 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
5696
5760
|
for (const line of detailLines(report, dryRun))
|
|
5697
5761
|
console.log(line);
|
|
5698
5762
|
for (const warning of report.warnings)
|
|
5699
|
-
|
|
5763
|
+
printError(`hippo: agent memories: ${warning}`);
|
|
5700
5764
|
return;
|
|
5701
5765
|
}
|
|
5702
5766
|
if (useGlobal) {
|
|
@@ -5720,11 +5784,11 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
5720
5784
|
if (flags['vault']) {
|
|
5721
5785
|
const folderPath = String(flags['vault']);
|
|
5722
5786
|
if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
|
|
5723
|
-
|
|
5787
|
+
printError(`Vault folder not found (or not a directory): ${folderPath}`);
|
|
5724
5788
|
process.exit(1);
|
|
5725
5789
|
}
|
|
5726
5790
|
if (useGlobal) {
|
|
5727
|
-
|
|
5791
|
+
printError('hippo import --vault does not support --global (raw rows are tenant-local).');
|
|
5728
5792
|
process.exit(1);
|
|
5729
5793
|
}
|
|
5730
5794
|
if (typeof flags['name'] !== 'string' || !flags['name'].trim()) {
|
|
@@ -5733,14 +5797,14 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
5733
5797
|
// clobber each other (codex R10 P2). A valueless `--name` parses as boolean
|
|
5734
5798
|
// true, and String(true) === "true" would silently import under vault:true:*
|
|
5735
5799
|
// - reject a non-string so it fails fast instead (codex R11 P2).
|
|
5736
|
-
|
|
5800
|
+
printError('hippo import --vault requires --name <vault> (a non-empty identity key for source-deletion sync).');
|
|
5737
5801
|
process.exit(1);
|
|
5738
5802
|
}
|
|
5739
5803
|
if (flags['scope'] !== undefined && (typeof flags['scope'] !== 'string' || !flags['scope'].trim())) {
|
|
5740
5804
|
// Same valueless-flag trap: a bare `--scope` must not become scope "true".
|
|
5741
5805
|
// Example uses the source-prefixed private form, since a bare `private` scope
|
|
5742
5806
|
// is NOT treated as private by recall and importVault rejects it (R13 P2).
|
|
5743
|
-
|
|
5807
|
+
printError('hippo import --vault: --scope requires a value (e.g. --scope vault:private:notes).');
|
|
5744
5808
|
process.exit(1);
|
|
5745
5809
|
}
|
|
5746
5810
|
const tenantId = resolveTenantId({});
|
|
@@ -5758,6 +5822,7 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
5758
5822
|
if ((vaultResult.rejected ?? 0) > 0) {
|
|
5759
5823
|
console.log(` Rejected (tombstoned): ${vaultResult.rejected}`);
|
|
5760
5824
|
}
|
|
5825
|
+
warnRedacted(vaultResult.redacted);
|
|
5761
5826
|
console.log(` ${dryRun ? 'Would archive: ' : 'Archived (removed): '}${vaultResult.archived ?? 0}`);
|
|
5762
5827
|
console.log(` Store: ${hippoRoot}`);
|
|
5763
5828
|
// Batch producer, same contract as the single-file import below: vault rows
|
|
@@ -5804,11 +5869,11 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
5804
5869
|
importerName = 'File';
|
|
5805
5870
|
}
|
|
5806
5871
|
if (!filePath || !importer) {
|
|
5807
|
-
|
|
5872
|
+
printError('Usage: hippo import <--chatgpt|--claude|--cursor|--file|--markdown|--vault> <path>, or hippo import --agents [--dry-run]');
|
|
5808
5873
|
process.exit(1);
|
|
5809
5874
|
}
|
|
5810
5875
|
if (!fs.existsSync(filePath)) {
|
|
5811
|
-
|
|
5876
|
+
printError(`File not found: ${filePath}`);
|
|
5812
5877
|
process.exit(1);
|
|
5813
5878
|
}
|
|
5814
5879
|
const result = importer(filePath, importOptions);
|
|
@@ -5829,6 +5894,7 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
5829
5894
|
if ((result.rejected ?? 0) > 0) {
|
|
5830
5895
|
console.log(` Rejected (tombstoned): ${result.rejected}`);
|
|
5831
5896
|
}
|
|
5897
|
+
warnRedacted(result.redacted);
|
|
5832
5898
|
if (dryRun) {
|
|
5833
5899
|
console.log('\n (dry run - nothing written)');
|
|
5834
5900
|
if (result.entries.length > 0) {
|
|
@@ -5851,7 +5917,7 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
5851
5917
|
function cmdPromote(hippoRoot, id) {
|
|
5852
5918
|
requireInit(hippoRoot);
|
|
5853
5919
|
if (!id) {
|
|
5854
|
-
|
|
5920
|
+
printError('Usage: hippo promote <id>');
|
|
5855
5921
|
process.exit(1);
|
|
5856
5922
|
}
|
|
5857
5923
|
const ctx = {
|
|
@@ -5865,7 +5931,7 @@ function cmdPromote(hippoRoot, id) {
|
|
|
5865
5931
|
console.log(` Global store: ${getGlobalRoot()}`);
|
|
5866
5932
|
}
|
|
5867
5933
|
catch (err) {
|
|
5868
|
-
|
|
5934
|
+
printError(`Failed to promote: ${err.message}`);
|
|
5869
5935
|
process.exit(1);
|
|
5870
5936
|
}
|
|
5871
5937
|
}
|
|
@@ -5903,8 +5969,8 @@ function cmdHook(args, flags) {
|
|
|
5903
5969
|
}
|
|
5904
5970
|
if (subcommand === 'install') {
|
|
5905
5971
|
if (!target || !HOOKS[target]) {
|
|
5906
|
-
|
|
5907
|
-
|
|
5972
|
+
printError(`Unknown hook target: ${target ?? '(none)'}`);
|
|
5973
|
+
printError(` Available: ${Object.keys(HOOKS).join(', ')}`);
|
|
5908
5974
|
process.exit(1);
|
|
5909
5975
|
}
|
|
5910
5976
|
const hook = HOOKS[target];
|
|
@@ -6004,7 +6070,7 @@ function cmdHook(args, flags) {
|
|
|
6004
6070
|
}
|
|
6005
6071
|
if (subcommand === 'uninstall') {
|
|
6006
6072
|
if (!target || !HOOKS[target]) {
|
|
6007
|
-
|
|
6073
|
+
printError(`Unknown hook target: ${target ?? '(none)'}`);
|
|
6008
6074
|
process.exit(1);
|
|
6009
6075
|
}
|
|
6010
6076
|
const hook = HOOKS[target];
|
|
@@ -6064,7 +6130,7 @@ function cmdHook(args, flags) {
|
|
|
6064
6130
|
}
|
|
6065
6131
|
return;
|
|
6066
6132
|
}
|
|
6067
|
-
|
|
6133
|
+
printError('Usage: hippo hook <install|uninstall|list> [target]');
|
|
6068
6134
|
process.exit(1);
|
|
6069
6135
|
}
|
|
6070
6136
|
function withoutHookBlock(text) {
|
|
@@ -6211,7 +6277,7 @@ function cmdDailyRunner() {
|
|
|
6211
6277
|
const globalRoot = getGlobalRoot();
|
|
6212
6278
|
// No workspace sleep ever opens the global store, yet hooks in folders without a store compact into it.
|
|
6213
6279
|
if (isInitialized(globalRoot)) {
|
|
6214
|
-
const finished = replayCompactionsAt(globalRoot, (message) =>
|
|
6280
|
+
const finished = replayCompactionsAt(globalRoot, (message) => log.warn(`compaction replay: ${message}`));
|
|
6215
6281
|
if (finished > 0)
|
|
6216
6282
|
console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over in the global store.`);
|
|
6217
6283
|
}
|
|
@@ -6237,7 +6303,7 @@ function cmdDailyRunner() {
|
|
|
6237
6303
|
catch (err) {
|
|
6238
6304
|
failed++;
|
|
6239
6305
|
const action = args.join(' ');
|
|
6240
|
-
|
|
6306
|
+
log.error(`daily-runner failed in ${cwd} during \`${action}\`: ${err.message}`);
|
|
6241
6307
|
}
|
|
6242
6308
|
});
|
|
6243
6309
|
console.log(`Daily maintenance complete: ${processed} workspace${processed === 1 ? '' : 's'} processed, ${failed} command failure${failed === 1 ? '' : 's'}.`);
|
|
@@ -6255,7 +6321,7 @@ function cmdWm(hippoRoot, args, flags) {
|
|
|
6255
6321
|
const sessionId = flags['session'] ? String(flags['session']).trim() : undefined;
|
|
6256
6322
|
const taskId = flags['task'] ? String(flags['task']).trim() : undefined;
|
|
6257
6323
|
if (!content) {
|
|
6258
|
-
|
|
6324
|
+
printError('Usage: hippo wm push --scope <scope> --content "..." [--importance 0.8] [--session <id>] [--task <id>]');
|
|
6259
6325
|
process.exit(1);
|
|
6260
6326
|
}
|
|
6261
6327
|
const id = wmPush(hippoRoot, {
|
|
@@ -6306,7 +6372,7 @@ function cmdWm(hippoRoot, args, flags) {
|
|
|
6306
6372
|
console.log(`Flushed ${count} working memory entries.`);
|
|
6307
6373
|
return;
|
|
6308
6374
|
}
|
|
6309
|
-
|
|
6375
|
+
printError('Usage: hippo wm <push|read|clear|flush>');
|
|
6310
6376
|
process.exit(1);
|
|
6311
6377
|
}
|
|
6312
6378
|
function cmdDag(hippoRoot, flags) {
|
|
@@ -6423,7 +6489,7 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
|
6423
6489
|
let depth;
|
|
6424
6490
|
if (rawDepth !== undefined) {
|
|
6425
6491
|
if (!Number.isInteger(rawDepth) || rawDepth < 1 || rawDepth > 10) {
|
|
6426
|
-
|
|
6492
|
+
printError(`--depth must be an integer between 1 and 10 (got ${flags['depth']})`);
|
|
6427
6493
|
process.exit(2);
|
|
6428
6494
|
}
|
|
6429
6495
|
depth = rawDepth;
|
|
@@ -6444,10 +6510,10 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
|
6444
6510
|
// intentionally collapses cross-tenant + scope-blocked + missing
|
|
6445
6511
|
// (codex round 3 P1: distinguishing scope_blocked leaked existence).
|
|
6446
6512
|
if (r.failure === 'not_drillable') {
|
|
6447
|
-
|
|
6513
|
+
printError(`Id ${summaryId} is a leaf row, not a level-2+ summary; nothing to drill into.`);
|
|
6448
6514
|
}
|
|
6449
6515
|
else {
|
|
6450
|
-
|
|
6516
|
+
printError(`No drillable summary at id=${summaryId}.`);
|
|
6451
6517
|
}
|
|
6452
6518
|
process.exit(1);
|
|
6453
6519
|
}
|
|
@@ -6477,7 +6543,7 @@ function cmdAuthCreate(hippoRoot, flags) {
|
|
|
6477
6543
|
let role = 'admin';
|
|
6478
6544
|
if (roleFlag !== undefined) {
|
|
6479
6545
|
if (roleFlag !== 'admin' && roleFlag !== 'member') {
|
|
6480
|
-
|
|
6546
|
+
printError(`Invalid --role value: '${roleFlag}'. Use 'admin' or 'member'.`);
|
|
6481
6547
|
process.exit(1);
|
|
6482
6548
|
}
|
|
6483
6549
|
role = roleFlag;
|
|
@@ -6556,7 +6622,7 @@ function cmdAuthRevoke(hippoRoot, keyId, flags) {
|
|
|
6556
6622
|
closeHippoDb(db);
|
|
6557
6623
|
}
|
|
6558
6624
|
if (keyTenant === undefined) {
|
|
6559
|
-
|
|
6625
|
+
printError(`Unknown key_id: ${keyId}`);
|
|
6560
6626
|
process.exit(1);
|
|
6561
6627
|
}
|
|
6562
6628
|
const ctx = { hippoRoot: root, tenantId: keyTenant, actor: api.adminActor('cli') };
|
|
@@ -6565,7 +6631,7 @@ function cmdAuthRevoke(hippoRoot, keyId, flags) {
|
|
|
6565
6631
|
revokedAt = api.authRevoke(ctx, keyId).revokedAt;
|
|
6566
6632
|
}
|
|
6567
6633
|
catch (err) {
|
|
6568
|
-
|
|
6634
|
+
printError(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
6569
6635
|
process.exit(1);
|
|
6570
6636
|
}
|
|
6571
6637
|
if (flags['json']) {
|
|
@@ -6589,7 +6655,7 @@ function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
|
|
|
6589
6655
|
closeHippoDb(db);
|
|
6590
6656
|
}
|
|
6591
6657
|
if (keyTenant === undefined) {
|
|
6592
|
-
|
|
6658
|
+
printError(`Unknown key_id: ${keyId}`);
|
|
6593
6659
|
process.exit(1);
|
|
6594
6660
|
}
|
|
6595
6661
|
const ctx = { hippoRoot: root, tenantId: keyTenant, actor: api.adminActor('cli') };
|
|
@@ -6600,7 +6666,7 @@ function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
|
|
|
6600
6666
|
api.authUngrant(ctx, keyId, scope);
|
|
6601
6667
|
}
|
|
6602
6668
|
catch (err) {
|
|
6603
|
-
|
|
6669
|
+
printError(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
6604
6670
|
process.exit(1);
|
|
6605
6671
|
}
|
|
6606
6672
|
if (flags['json']) {
|
|
@@ -6627,13 +6693,13 @@ function cmdAuditList(hippoRoot, flags) {
|
|
|
6627
6693
|
// Regenerate from Set to prevent future drift (v1.11.5: pre-v1.11.5 message
|
|
6628
6694
|
// was hand-maintained and had drifted — missed 'auth_revoke' and 'outcome').
|
|
6629
6695
|
const expected = Array.from(VALID_AUDIT_OPS).join(' | ');
|
|
6630
|
-
|
|
6696
|
+
printError(`Unknown --op value: ${opFlag}. Expected one of: ${expected}.`);
|
|
6631
6697
|
process.exit(1);
|
|
6632
6698
|
}
|
|
6633
6699
|
const op = opFlag;
|
|
6634
6700
|
const since = typeof flags['since'] === 'string' ? flags['since'] : undefined;
|
|
6635
6701
|
if (since !== undefined && !Number.isFinite(new Date(since).getTime())) {
|
|
6636
|
-
|
|
6702
|
+
printError(`Invalid --since: ${since} (expected an ISO timestamp like 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
6637
6703
|
process.exit(1);
|
|
6638
6704
|
}
|
|
6639
6705
|
const limitRaw = flags['limit'];
|
|
@@ -6641,13 +6707,13 @@ function cmdAuditList(hippoRoot, flags) {
|
|
|
6641
6707
|
if (limitRaw !== undefined && typeof limitRaw !== 'boolean') {
|
|
6642
6708
|
const parsed = parseInt(String(limitRaw), 10);
|
|
6643
6709
|
if (!Number.isFinite(parsed)) {
|
|
6644
|
-
|
|
6710
|
+
printError(`Invalid --limit value: ${String(limitRaw)} (expected a positive integer).`);
|
|
6645
6711
|
process.exit(1);
|
|
6646
6712
|
}
|
|
6647
6713
|
limit = parsed;
|
|
6648
6714
|
}
|
|
6649
6715
|
if (limit < 1 || limit > 10000) {
|
|
6650
|
-
|
|
6716
|
+
printError(`--limit must be between 1 and 10000 (got ${limit}).`);
|
|
6651
6717
|
process.exit(1);
|
|
6652
6718
|
}
|
|
6653
6719
|
const ctx = { hippoRoot: root, tenantId, actor: { subject: 'cli', role: 'admin' } };
|
|
@@ -6675,7 +6741,7 @@ function printAuditPruneUsage() {
|
|
|
6675
6741
|
function cmdAuditPrune(hippoRoot, flags) {
|
|
6676
6742
|
const olderThanRaw = typeof flags['older-than'] === 'string' ? flags['older-than'] : '';
|
|
6677
6743
|
if (!olderThanRaw) {
|
|
6678
|
-
|
|
6744
|
+
printError('Usage: hippo audit prune --older-than <Nd> [--dry-run] [--tenant <t>]');
|
|
6679
6745
|
process.exit(1);
|
|
6680
6746
|
}
|
|
6681
6747
|
let olderThanDays;
|
|
@@ -6683,7 +6749,7 @@ function cmdAuditPrune(hippoRoot, flags) {
|
|
|
6683
6749
|
olderThanDays = parseOlderThanFlag(olderThanRaw);
|
|
6684
6750
|
}
|
|
6685
6751
|
catch (e) {
|
|
6686
|
-
|
|
6752
|
+
printError(e.message);
|
|
6687
6753
|
process.exit(1);
|
|
6688
6754
|
}
|
|
6689
6755
|
const tenantId = typeof flags['tenant'] === 'string'
|
|
@@ -6719,7 +6785,7 @@ function cmdAuditLog(hippoRoot, args, flags) {
|
|
|
6719
6785
|
cmdAuditPrune(hippoRoot, flags);
|
|
6720
6786
|
return;
|
|
6721
6787
|
}
|
|
6722
|
-
|
|
6788
|
+
printError(`Unknown audit subcommand: ${sub}. Expected: list | prune.`);
|
|
6723
6789
|
process.exit(1);
|
|
6724
6790
|
}
|
|
6725
6791
|
// ---------------------------------------------------------------------------
|
|
@@ -6740,7 +6806,7 @@ function resolveGoalSession(flags) {
|
|
|
6740
6806
|
? String(flags['session-id'])
|
|
6741
6807
|
: process.env.HIPPO_SESSION_ID ?? '').trim();
|
|
6742
6808
|
if (!sessionId) {
|
|
6743
|
-
|
|
6809
|
+
printError('session id required (set HIPPO_SESSION_ID or pass --session-id)');
|
|
6744
6810
|
process.exit(1);
|
|
6745
6811
|
}
|
|
6746
6812
|
const tenantId = (flags['tenant-id'] !== undefined
|
|
@@ -6751,51 +6817,51 @@ function resolveGoalSession(flags) {
|
|
|
6751
6817
|
function cmdGoalPush(hippoRoot, args, flags) {
|
|
6752
6818
|
const rawName = args.join(' ').trim();
|
|
6753
6819
|
if (!rawName) {
|
|
6754
|
-
|
|
6820
|
+
printError('Usage: hippo goal push <name> [--policy <type>] [--success "<condition>"] [--level N] [--parent <goalId>]');
|
|
6755
6821
|
process.exit(1);
|
|
6756
6822
|
}
|
|
6757
6823
|
// Sanitize at WRITE time so corrupt names never enter the DB.
|
|
6758
6824
|
const name = sanitizeGoalName(rawName);
|
|
6759
6825
|
if (name !== rawName) {
|
|
6760
|
-
|
|
6826
|
+
printError('note: stripped control characters from goal name');
|
|
6761
6827
|
}
|
|
6762
6828
|
const { sessionId, tenantId } = resolveGoalSession(flags);
|
|
6763
6829
|
let policy;
|
|
6764
6830
|
const policyRaw = flags['policy'];
|
|
6765
6831
|
if (policyRaw === true) {
|
|
6766
|
-
|
|
6832
|
+
printError('--policy requires a value (e.g., --policy error-prioritized)');
|
|
6767
6833
|
process.exit(1);
|
|
6768
6834
|
}
|
|
6769
6835
|
if (typeof policyRaw === 'string') {
|
|
6770
6836
|
if (!GOAL_POLICY_TYPES.includes(policyRaw)) {
|
|
6771
|
-
|
|
6837
|
+
printError(`Unknown --policy '${policyRaw}'. Expected one of: ${GOAL_POLICY_TYPES.join(' | ')}.`);
|
|
6772
6838
|
process.exit(1);
|
|
6773
6839
|
}
|
|
6774
6840
|
policy = { policyType: policyRaw };
|
|
6775
6841
|
}
|
|
6776
6842
|
const successRaw = flags['success'];
|
|
6777
6843
|
if (successRaw === true) {
|
|
6778
|
-
|
|
6844
|
+
printError('--success requires a value (e.g., --success "<condition>")');
|
|
6779
6845
|
process.exit(1);
|
|
6780
6846
|
}
|
|
6781
6847
|
const successCondition = typeof successRaw === 'string' ? successRaw : undefined;
|
|
6782
6848
|
const levelRaw = flags['level'];
|
|
6783
6849
|
let level;
|
|
6784
6850
|
if (levelRaw === true) {
|
|
6785
|
-
|
|
6851
|
+
printError('--level requires a value (e.g., --level 1)');
|
|
6786
6852
|
process.exit(1);
|
|
6787
6853
|
}
|
|
6788
6854
|
if (levelRaw !== undefined) {
|
|
6789
6855
|
const parsed = Number(levelRaw);
|
|
6790
6856
|
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 2 || !Number.isInteger(parsed)) {
|
|
6791
|
-
|
|
6857
|
+
printError('--level must be an integer in [0, 2]');
|
|
6792
6858
|
process.exit(1);
|
|
6793
6859
|
}
|
|
6794
6860
|
level = parsed;
|
|
6795
6861
|
}
|
|
6796
6862
|
const parentRaw = flags['parent'];
|
|
6797
6863
|
if (parentRaw === true) {
|
|
6798
|
-
|
|
6864
|
+
printError('--parent requires a value (e.g., --parent <goalId>)');
|
|
6799
6865
|
process.exit(1);
|
|
6800
6866
|
}
|
|
6801
6867
|
const parentGoalId = typeof parentRaw === 'string' ? parentRaw : undefined;
|
|
@@ -6861,19 +6927,19 @@ function cmdGoalList(hippoRoot, flags) {
|
|
|
6861
6927
|
function cmdGoalComplete(hippoRoot, args, flags) {
|
|
6862
6928
|
const id = args[0];
|
|
6863
6929
|
if (!id) {
|
|
6864
|
-
|
|
6930
|
+
printError('Usage: hippo goal complete <id> [--outcome <0..1>] [--no-propagate]');
|
|
6865
6931
|
process.exit(1);
|
|
6866
6932
|
}
|
|
6867
6933
|
let outcomeScore;
|
|
6868
6934
|
const outcomeRaw = flags['outcome'];
|
|
6869
6935
|
if (outcomeRaw === true) {
|
|
6870
|
-
|
|
6936
|
+
printError('--outcome requires a value (e.g., --outcome 0.9)');
|
|
6871
6937
|
process.exit(1);
|
|
6872
6938
|
}
|
|
6873
6939
|
if (outcomeRaw !== undefined) {
|
|
6874
6940
|
const parsed = Number(outcomeRaw);
|
|
6875
6941
|
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
6876
|
-
|
|
6942
|
+
printError('--outcome must be a number in [0, 1]');
|
|
6877
6943
|
process.exit(1);
|
|
6878
6944
|
}
|
|
6879
6945
|
outcomeScore = parsed;
|
|
@@ -6885,7 +6951,7 @@ function cmdGoalComplete(hippoRoot, args, flags) {
|
|
|
6885
6951
|
function cmdGoalSuspend(hippoRoot, args) {
|
|
6886
6952
|
const id = args[0];
|
|
6887
6953
|
if (!id) {
|
|
6888
|
-
|
|
6954
|
+
printError('Usage: hippo goal suspend <id>');
|
|
6889
6955
|
process.exit(1);
|
|
6890
6956
|
}
|
|
6891
6957
|
suspendGoal(hippoRoot, id);
|
|
@@ -6894,7 +6960,7 @@ function cmdGoalSuspend(hippoRoot, args) {
|
|
|
6894
6960
|
function cmdGoalResume(hippoRoot, args) {
|
|
6895
6961
|
const id = args[0];
|
|
6896
6962
|
if (!id) {
|
|
6897
|
-
|
|
6963
|
+
printError('Usage: hippo goal resume <id>');
|
|
6898
6964
|
process.exit(1);
|
|
6899
6965
|
}
|
|
6900
6966
|
resumeGoal(hippoRoot, id);
|
|
@@ -6903,7 +6969,7 @@ function cmdGoalResume(hippoRoot, args) {
|
|
|
6903
6969
|
function cmdGoal(hippoRoot, args, flags) {
|
|
6904
6970
|
const sub = args[0];
|
|
6905
6971
|
if (!sub) {
|
|
6906
|
-
|
|
6972
|
+
printError('Usage: hippo goal <push|list|complete|suspend|resume> [args]');
|
|
6907
6973
|
process.exit(1);
|
|
6908
6974
|
}
|
|
6909
6975
|
const subArgs = args.slice(1);
|
|
@@ -6924,14 +6990,14 @@ function cmdGoal(hippoRoot, args, flags) {
|
|
|
6924
6990
|
cmdGoalResume(hippoRoot, subArgs);
|
|
6925
6991
|
return;
|
|
6926
6992
|
default:
|
|
6927
|
-
|
|
6993
|
+
printError(`Unknown goal subcommand: ${sub}. Expected: push | list | complete | suspend | resume.`);
|
|
6928
6994
|
process.exit(1);
|
|
6929
6995
|
}
|
|
6930
6996
|
}
|
|
6931
6997
|
function cmdAuth(hippoRoot, args, flags) {
|
|
6932
6998
|
const sub = args[0];
|
|
6933
6999
|
if (!sub) {
|
|
6934
|
-
|
|
7000
|
+
printError('Usage: hippo auth <create|list|revoke|grant|ungrant> [options]');
|
|
6935
7001
|
process.exit(1);
|
|
6936
7002
|
}
|
|
6937
7003
|
const subArgs = args.slice(1);
|
|
@@ -6945,7 +7011,7 @@ function cmdAuth(hippoRoot, args, flags) {
|
|
|
6945
7011
|
case 'revoke': {
|
|
6946
7012
|
const keyId = subArgs[0];
|
|
6947
7013
|
if (!keyId) {
|
|
6948
|
-
|
|
7014
|
+
printError('Usage: hippo auth revoke <key_id>');
|
|
6949
7015
|
process.exit(1);
|
|
6950
7016
|
}
|
|
6951
7017
|
cmdAuthRevoke(hippoRoot, keyId, flags);
|
|
@@ -6955,14 +7021,14 @@ function cmdAuth(hippoRoot, args, flags) {
|
|
|
6955
7021
|
case 'ungrant': {
|
|
6956
7022
|
const [keyId, scope] = subArgs;
|
|
6957
7023
|
if (!keyId || !scope) {
|
|
6958
|
-
|
|
7024
|
+
printError(`Usage: hippo auth ${sub} <key_id> <scope>`);
|
|
6959
7025
|
process.exit(1);
|
|
6960
7026
|
}
|
|
6961
7027
|
cmdAuthScopeGrant(hippoRoot, keyId, scope, sub === 'grant', flags);
|
|
6962
7028
|
return;
|
|
6963
7029
|
}
|
|
6964
7030
|
default:
|
|
6965
|
-
|
|
7031
|
+
printError(`Unknown auth subcommand: ${sub}. Expected: create | list | revoke | grant | ungrant.`);
|
|
6966
7032
|
process.exit(1);
|
|
6967
7033
|
}
|
|
6968
7034
|
}
|
|
@@ -6983,7 +7049,7 @@ function cmdSlackBackfill(hippoRoot, flags) {
|
|
|
6983
7049
|
// Real fetcher requires SLACK_BOT_TOKEN with channels:history scope.
|
|
6984
7050
|
const token = process.env.SLACK_BOT_TOKEN;
|
|
6985
7051
|
if (!token) {
|
|
6986
|
-
|
|
7052
|
+
printError('SLACK_BOT_TOKEN is not set. Backfill requires a Slack bot token with channels:history scope.');
|
|
6987
7053
|
process.exit(2);
|
|
6988
7054
|
}
|
|
6989
7055
|
// --since is advisory in V1: the slack_cursors row drives resume, so the
|
|
@@ -7006,7 +7072,7 @@ function cmdSlackBackfill(hippoRoot, flags) {
|
|
|
7006
7072
|
console.log(`backfill ${channel}: ${r.ingested} new messages across ${r.pages} pages`);
|
|
7007
7073
|
})
|
|
7008
7074
|
.catch((e) => {
|
|
7009
|
-
|
|
7075
|
+
printError('backfill failed:', e.message);
|
|
7010
7076
|
process.exit(3);
|
|
7011
7077
|
});
|
|
7012
7078
|
}
|
|
@@ -7026,12 +7092,12 @@ function cmdSlackDlqList(hippoRoot, _flags) {
|
|
|
7026
7092
|
function cmdSlackDlqReplay(hippoRoot, args, flags) {
|
|
7027
7093
|
const idArg = args[2];
|
|
7028
7094
|
if (!idArg) {
|
|
7029
|
-
|
|
7095
|
+
printError('Usage: hippo slack dlq replay <id> [--force]');
|
|
7030
7096
|
process.exit(1);
|
|
7031
7097
|
}
|
|
7032
7098
|
const id = Number(idArg);
|
|
7033
7099
|
if (!Number.isFinite(id) || !Number.isInteger(id) || id < 1) {
|
|
7034
|
-
|
|
7100
|
+
printError(`replay: invalid id ${idArg}`);
|
|
7035
7101
|
process.exit(1);
|
|
7036
7102
|
}
|
|
7037
7103
|
const force = flags.force === true;
|
|
@@ -7040,7 +7106,7 @@ function cmdSlackDlqReplay(hippoRoot, args, flags) {
|
|
|
7040
7106
|
signingSecret: process.env.SLACK_SIGNING_SECRET,
|
|
7041
7107
|
});
|
|
7042
7108
|
if (!result.ok) {
|
|
7043
|
-
|
|
7109
|
+
printError(`replay failed: status=${result.status} retry_count=${result.retryCount}${result.reason ? ` reason=${result.reason}` : ''}`);
|
|
7044
7110
|
process.exit(1);
|
|
7045
7111
|
}
|
|
7046
7112
|
console.log(`replay ok: status=${result.status} memory_id=${result.memoryId ?? '(none)'} retry_count=${result.retryCount}`);
|
|
@@ -7055,7 +7121,7 @@ function cmdSlackWorkspacesAdd(hippoRoot, flags) {
|
|
|
7055
7121
|
const teamId = typeof flags['team'] === 'string' ? flags['team'].trim() : '';
|
|
7056
7122
|
const tenantId = typeof flags['tenant'] === 'string' ? flags['tenant'].trim() : '';
|
|
7057
7123
|
if (!teamId || !tenantId) {
|
|
7058
|
-
|
|
7124
|
+
printError('Usage: hippo slack workspaces add --team <T> --tenant <t>');
|
|
7059
7125
|
process.exit(1);
|
|
7060
7126
|
}
|
|
7061
7127
|
const db = openHippoDb(hippoRoot);
|
|
@@ -7086,14 +7152,14 @@ function cmdSlackWorkspacesList(hippoRoot) {
|
|
|
7086
7152
|
function cmdSlackWorkspacesRemove(hippoRoot, flags) {
|
|
7087
7153
|
const teamId = typeof flags['team'] === 'string' ? flags['team'].trim() : '';
|
|
7088
7154
|
if (!teamId) {
|
|
7089
|
-
|
|
7155
|
+
printError('Usage: hippo slack workspaces remove --team <T>');
|
|
7090
7156
|
process.exit(1);
|
|
7091
7157
|
}
|
|
7092
7158
|
const db = openHippoDb(hippoRoot);
|
|
7093
7159
|
try {
|
|
7094
7160
|
const removed = removeSlackWorkspace(db, teamId);
|
|
7095
7161
|
if (!removed) {
|
|
7096
|
-
|
|
7162
|
+
printError(`no workspace registered for team ${teamId}`);
|
|
7097
7163
|
process.exit(1);
|
|
7098
7164
|
}
|
|
7099
7165
|
console.log(`removed: ${teamId}`);
|
|
@@ -7133,7 +7199,7 @@ function cmdSlack(hippoRoot, args, flags) {
|
|
|
7133
7199
|
printSlackWorkspacesUsage();
|
|
7134
7200
|
process.exit(1);
|
|
7135
7201
|
}
|
|
7136
|
-
|
|
7202
|
+
printError('Usage: hippo slack <backfill|dlq list|dlq replay <id> [--force]|workspaces add|workspaces list|workspaces remove> [...]');
|
|
7137
7203
|
process.exit(1);
|
|
7138
7204
|
}
|
|
7139
7205
|
export function usageText() {
|
|
@@ -7207,17 +7273,21 @@ Commands:
|
|
|
7207
7273
|
where cost_factor = min(0.3, tokens / 10000). Re-sorts
|
|
7208
7274
|
results by utility. Default off. RESEARCH.md §PFC.OFC.
|
|
7209
7275
|
--reranker <name> Apply a reranker pass after retrieval
|
|
7210
|
-
(cross-encoder|jev|llm). Looks up
|
|
7211
|
-
reranker from src/rerankers/index.ts and
|
|
7212
|
-
the top-K candidates. Default unset (no reranker).
|
|
7276
|
+
(cross-encoder|jev|clef-flash|clef|llm). Looks up
|
|
7277
|
+
the named reranker from src/rerankers/index.ts and
|
|
7278
|
+
re-orders the top-K candidates. Default unset (no reranker).
|
|
7213
7279
|
jev calls the hosted TypeSafe Jev API: it needs
|
|
7214
7280
|
TYPESAFE_API_KEY, sends the query and candidate
|
|
7215
7281
|
text to that API, costs about 0.0004 USD a recall,
|
|
7216
7282
|
and falls back to cross-encoder on any failure.
|
|
7283
|
+
clef-flash and clef send the same request to
|
|
7284
|
+
Cloudflare Workers AI (CLOUDFLARE_ACCOUNT_ID and
|
|
7285
|
+
CLOUDFLARE_API_TOKEN) or to HIPPO_CLEF_ENDPOINT, and
|
|
7286
|
+
keep the native order on any failure.
|
|
7217
7287
|
See docs/evals/2026-09-19-jev-reranker.md and
|
|
7218
7288
|
docs/plans/2026-05-10-f6-reranker-hardening.md.
|
|
7219
7289
|
--reranker-top-k <n> Cap candidates passed to the reranker (default 50;
|
|
7220
|
-
40 for jev).
|
|
7290
|
+
40 for jev, clef-flash and clef).
|
|
7221
7291
|
--goal <tag> dlPFC goal-conditioned recall: memories tagged with
|
|
7222
7292
|
the goal tag get a 1.5x score boost and results are
|
|
7223
7293
|
re-sorted. Default off. RESEARCH.md §PFC.dlPFC.
|
|
@@ -7356,7 +7426,16 @@ Commands:
|
|
|
7356
7426
|
--global Operate on the global store
|
|
7357
7427
|
dormant restore <id> Bring a dormant memory back to active memory
|
|
7358
7428
|
dormant forget <id> Delete a dormant memory permanently
|
|
7359
|
-
|
|
7429
|
+
projects [list] List the project names in a store, with a hint for old worktree names
|
|
7430
|
+
--json Output as JSON
|
|
7431
|
+
--global Operate on the global store
|
|
7432
|
+
projects merge <from> <into> [--apply]
|
|
7433
|
+
Fold one project name into another (dry run unless --apply;
|
|
7434
|
+
writes a backup and one audit event first)
|
|
7435
|
+
projects repair [--apply]
|
|
7436
|
+
Re-tag merged rows older versions of sleep saved as user-global,
|
|
7437
|
+
by their parents' project (dry run unless --apply)
|
|
7438
|
+
quarantine [list] List memories a connector flagged as an instruction attempt, pending review
|
|
7360
7439
|
--all Include approved and rejected rows too (default: pending only)
|
|
7361
7440
|
--json Output as JSON
|
|
7362
7441
|
--global Operate on the global store
|
|
@@ -7828,7 +7907,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7828
7907
|
* --hops value-less guard, so every current and future command - including the
|
|
7829
7908
|
* thin-client dispatch relays - sees --scope only as a non-empty string. */
|
|
7830
7909
|
if ('scope' in flags && (typeof flags['scope'] !== 'string' || !flags['scope'].trim())) {
|
|
7831
|
-
|
|
7910
|
+
printError('--scope requires a non-empty value (e.g. --scope slack:private:C1).');
|
|
7832
7911
|
process.exit(1);
|
|
7833
7912
|
}
|
|
7834
7913
|
// parseArgs stores a value-less flag as boolean true, and NaN then survives every
|
|
@@ -7842,7 +7921,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7842
7921
|
if (raw === undefined)
|
|
7843
7922
|
continue;
|
|
7844
7923
|
if (typeof raw !== 'string' || !raw.trim() || !Number.isFinite(Number(raw))) {
|
|
7845
|
-
|
|
7924
|
+
printError(`--${key} requires a numeric value.`);
|
|
7846
7925
|
process.exit(1);
|
|
7847
7926
|
}
|
|
7848
7927
|
}
|
|
@@ -7850,7 +7929,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7850
7929
|
// so no single coercion of an inline value would be correct for every one of them.
|
|
7851
7930
|
for (const key of BOOLEAN_FLAGS) {
|
|
7852
7931
|
if (Object.hasOwn(flags, key) && typeof flags[key] !== 'boolean') {
|
|
7853
|
-
|
|
7932
|
+
printError(`--${key} takes no value`);
|
|
7854
7933
|
process.exit(1);
|
|
7855
7934
|
}
|
|
7856
7935
|
}
|
|
@@ -7859,14 +7938,14 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7859
7938
|
if (unknownFlags.length > 0) {
|
|
7860
7939
|
const names = unknownFlags.map((key) => `--${key}`).join(', ');
|
|
7861
7940
|
if (DESTRUCTIVE_COMMANDS.has(command)) {
|
|
7862
|
-
|
|
7941
|
+
printError(`Unknown flag ${names} for hippo ${command}. Nothing was changed.`);
|
|
7863
7942
|
process.exit(2);
|
|
7864
7943
|
}
|
|
7865
|
-
|
|
7944
|
+
printError(`hippo: ignoring unknown flag ${names}. A later release will reject it.`);
|
|
7866
7945
|
}
|
|
7867
7946
|
const refusal = Object.hasOwn(flags, 'dry-run') ? dryRunRefusal(command, args, flags) : null;
|
|
7868
7947
|
if (refusal) {
|
|
7869
|
-
|
|
7948
|
+
printError(refusal);
|
|
7870
7949
|
process.exit(2);
|
|
7871
7950
|
}
|
|
7872
7951
|
switch (command) {
|
|
@@ -7882,7 +7961,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7882
7961
|
text = args.join(' ').trim();
|
|
7883
7962
|
}
|
|
7884
7963
|
if (!text || text.length < 3) {
|
|
7885
|
-
|
|
7964
|
+
printError('Memory content too short (minimum 3 characters).');
|
|
7886
7965
|
process.exit(1);
|
|
7887
7966
|
}
|
|
7888
7967
|
// Thin-client routing. When a server is up, simple `remember` calls go
|
|
@@ -7906,11 +7985,11 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7906
7985
|
const thinOwnerRaw = typeof flags['owner'] === 'string' ? flags['owner'] : undefined;
|
|
7907
7986
|
const thinOwnerCheck = validateOwner(thinOwnerRaw, { strict: isStrictOwnerEnv() });
|
|
7908
7987
|
if (!thinOwnerCheck.ok) {
|
|
7909
|
-
|
|
7988
|
+
printError(thinOwnerCheck.message);
|
|
7910
7989
|
process.exit(1);
|
|
7911
7990
|
}
|
|
7912
7991
|
if (thinOwnerCheck.message)
|
|
7913
|
-
|
|
7992
|
+
printError(thinOwnerCheck.message);
|
|
7914
7993
|
const remembered = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
7915
7994
|
const result = await client.remember(info.url, apiKey, {
|
|
7916
7995
|
content: text,
|
|
@@ -7922,6 +8001,8 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7922
8001
|
});
|
|
7923
8002
|
console.log(`Remembered [${result.id}] (via ${info.url})`);
|
|
7924
8003
|
console.log(` Kind: ${result.kind} | Tenant: ${result.tenantId}`);
|
|
8004
|
+
for (const w of result.warnings ?? [])
|
|
8005
|
+
printError(`Warning: ${w}`);
|
|
7925
8006
|
});
|
|
7926
8007
|
if (remembered)
|
|
7927
8008
|
break;
|
|
@@ -7933,7 +8014,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7933
8014
|
case 'recall': {
|
|
7934
8015
|
const query = args.join(' ').trim();
|
|
7935
8016
|
if (!query) {
|
|
7936
|
-
|
|
8017
|
+
printError('Please provide a search query.');
|
|
7937
8018
|
process.exit(1);
|
|
7938
8019
|
}
|
|
7939
8020
|
await cmdRecall(hookStoreRoot(hippoRoot), query, flags);
|
|
@@ -7942,7 +8023,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7942
8023
|
case 'drill': {
|
|
7943
8024
|
const summaryId = args[0];
|
|
7944
8025
|
if (!summaryId) {
|
|
7945
|
-
|
|
8026
|
+
printError('Usage: hippo drill <summary-id> [--limit N] [--budget N]');
|
|
7946
8027
|
process.exit(1);
|
|
7947
8028
|
}
|
|
7948
8029
|
cmdDrillDown(hippoRoot, summaryId, flags);
|
|
@@ -7951,7 +8032,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7951
8032
|
case 'assemble': {
|
|
7952
8033
|
const sessionId = typeof flags['session'] === 'string' ? flags['session'] : args[0];
|
|
7953
8034
|
if (!sessionId) {
|
|
7954
|
-
|
|
8035
|
+
printError('Usage: hippo assemble --session <id> [--budget N] [--fresh-tail N] [--no-summarize-older] [--json]');
|
|
7955
8036
|
process.exit(1);
|
|
7956
8037
|
}
|
|
7957
8038
|
cmdAssemble(hippoRoot, sessionId, flags);
|
|
@@ -7961,7 +8042,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7961
8042
|
const oldId = args[0];
|
|
7962
8043
|
const newContent = args.slice(1).join(' ').trim();
|
|
7963
8044
|
if (!oldId || !newContent) {
|
|
7964
|
-
|
|
8045
|
+
printError('Usage: hippo supersede <old-id> "<new content>" [--layer L] [--tag T] [--pin]');
|
|
7965
8046
|
process.exit(1);
|
|
7966
8047
|
}
|
|
7967
8048
|
cmdSupersede(hippoRoot, oldId, newContent, flags);
|
|
@@ -7970,7 +8051,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7970
8051
|
case 'explain': {
|
|
7971
8052
|
const query = args.join(' ').trim();
|
|
7972
8053
|
if (!query) {
|
|
7973
|
-
|
|
8054
|
+
printError('Please provide a search query.');
|
|
7974
8055
|
process.exit(1);
|
|
7975
8056
|
}
|
|
7976
8057
|
await cmdExplain(hippoRoot, query, flags);
|
|
@@ -7988,7 +8069,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7988
8069
|
break;
|
|
7989
8070
|
}
|
|
7990
8071
|
if (!sub) {
|
|
7991
|
-
|
|
8072
|
+
printError('Usage: hippo trace <memory-id> | hippo trace record --task <t> --steps <json> --outcome <o>');
|
|
7992
8073
|
process.exit(1);
|
|
7993
8074
|
}
|
|
7994
8075
|
cmdTrace(hippoRoot, sub, flags);
|
|
@@ -8012,11 +8093,13 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8012
8093
|
case 'pre-compact': {
|
|
8013
8094
|
// Bounded wait, not a TTY guard: an idle non-TTY pipe must not hang.
|
|
8014
8095
|
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8096
|
+
await runHookWithStores(async () => {
|
|
8097
|
+
resetHookInjection(hippoRoot, stdinText, null);
|
|
8098
|
+
await cmdPreCompact(hookStoreRoot(hippoRoot), {
|
|
8099
|
+
stdinText,
|
|
8100
|
+
stdinTimedOut,
|
|
8101
|
+
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
8102
|
+
});
|
|
8020
8103
|
});
|
|
8021
8104
|
break;
|
|
8022
8105
|
}
|
|
@@ -8025,7 +8108,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8025
8108
|
const { text } = await readStdinBounded();
|
|
8026
8109
|
const logFlag = flags['log-file'];
|
|
8027
8110
|
const store = hookStoreRoot(hippoRoot);
|
|
8028
|
-
const line = cmdPostCompact(store, {
|
|
8111
|
+
const line = await runHookWithStores(() => cmdPostCompact(store, {
|
|
8029
8112
|
stdinText: text,
|
|
8030
8113
|
logFile: logFlag === true || logFlag === false || Array.isArray(logFlag) ? undefined : logFlag,
|
|
8031
8114
|
// Passed in, since capture.ts importing the sync would close an import cycle.
|
|
@@ -8037,8 +8120,8 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8037
8120
|
for (const warning of report.warnings)
|
|
8038
8121
|
log(`agent memories: ${warning}`);
|
|
8039
8122
|
},
|
|
8040
|
-
});
|
|
8041
|
-
if (line !== null)
|
|
8123
|
+
}));
|
|
8124
|
+
if (line !== null && line !== undefined)
|
|
8042
8125
|
console.log(line);
|
|
8043
8126
|
break;
|
|
8044
8127
|
}
|
|
@@ -8051,7 +8134,8 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8051
8134
|
const payload = (text ?? '').trim();
|
|
8052
8135
|
if (isInitialized(root) && payload) {
|
|
8053
8136
|
// SAFETY: JSON.parse returns a JSON value by definition.
|
|
8054
|
-
|
|
8137
|
+
const failure = JSON.parse(payload);
|
|
8138
|
+
await runHookWithStores(() => captureToolFailure(root, resolveTenantId({}), failure));
|
|
8055
8139
|
}
|
|
8056
8140
|
}
|
|
8057
8141
|
catch {
|
|
@@ -8061,8 +8145,10 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8061
8145
|
}
|
|
8062
8146
|
case 'compact-resume': {
|
|
8063
8147
|
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8064
|
-
|
|
8065
|
-
|
|
8148
|
+
await runHookWithStores(() => {
|
|
8149
|
+
resetHookInjection(hippoRoot, stdinText, 'compact');
|
|
8150
|
+
cmdCompactResume(hookStoreRoot(hippoRoot), stdinText, stdinTimedOut);
|
|
8151
|
+
});
|
|
8066
8152
|
break;
|
|
8067
8153
|
}
|
|
8068
8154
|
case 'codex-run':
|
|
@@ -8212,6 +8298,9 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8212
8298
|
case 'dormant':
|
|
8213
8299
|
cmdDormant(hippoRoot, args, flags);
|
|
8214
8300
|
break;
|
|
8301
|
+
case 'projects':
|
|
8302
|
+
(await import('./cli/projects.js')).cmdProjects(hippoRoot, args, flags);
|
|
8303
|
+
break;
|
|
8215
8304
|
case 'quarantine':
|
|
8216
8305
|
cmdQuarantine(hippoRoot, args, flags);
|
|
8217
8306
|
break;
|
|
@@ -8233,7 +8322,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8233
8322
|
case 'support-bundle': {
|
|
8234
8323
|
const outFlag = cardStringFlag(flags, 'out');
|
|
8235
8324
|
if (outFlag === '') {
|
|
8236
|
-
|
|
8325
|
+
printError('--out requires a file path.');
|
|
8237
8326
|
process.exit(1);
|
|
8238
8327
|
}
|
|
8239
8328
|
const includeLogs = flags['include-logs'] === true;
|
|
@@ -8248,10 +8337,10 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8248
8337
|
}
|
|
8249
8338
|
catch (err) {
|
|
8250
8339
|
if (err instanceof Error && 'code' in err && err.code === 'EEXIST') {
|
|
8251
|
-
|
|
8340
|
+
printError(`${file} already exists; pass --out to choose another file. Nothing was written.`);
|
|
8252
8341
|
}
|
|
8253
8342
|
else {
|
|
8254
|
-
|
|
8343
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
8255
8344
|
}
|
|
8256
8345
|
process.exit(1);
|
|
8257
8346
|
}
|
|
@@ -8283,7 +8372,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8283
8372
|
case 'forget': {
|
|
8284
8373
|
const id = args[0];
|
|
8285
8374
|
if (!id) {
|
|
8286
|
-
|
|
8375
|
+
printError('Please provide a memory ID.');
|
|
8287
8376
|
process.exit(1);
|
|
8288
8377
|
}
|
|
8289
8378
|
// Archive has its own HTTP route (POST /v1/memories/:id/archive); route
|
|
@@ -8291,7 +8380,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8291
8380
|
const archive = flags['archive'] === true;
|
|
8292
8381
|
const reason = typeof flags['reason'] === 'string' ? flags['reason'] : null;
|
|
8293
8382
|
if (archive && !reason) {
|
|
8294
|
-
|
|
8383
|
+
printError(ARCHIVE_REASON_REQUIRED);
|
|
8295
8384
|
process.exit(1);
|
|
8296
8385
|
}
|
|
8297
8386
|
if (flags['dry-run'] === true) {
|
|
@@ -8315,7 +8404,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8315
8404
|
if (client.classifyTransportFailure(err) !== 'none')
|
|
8316
8405
|
throw err;
|
|
8317
8406
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8318
|
-
|
|
8407
|
+
printError(archive ? `Could not archive ${id}: ${msg}` : msg);
|
|
8319
8408
|
process.exit(1);
|
|
8320
8409
|
}
|
|
8321
8410
|
});
|
|
@@ -8327,7 +8416,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8327
8416
|
case 'inspect': {
|
|
8328
8417
|
const id = args[0];
|
|
8329
8418
|
if (!id) {
|
|
8330
|
-
|
|
8419
|
+
printError('Please provide a memory ID.');
|
|
8331
8420
|
process.exit(1);
|
|
8332
8421
|
}
|
|
8333
8422
|
cmdInspect(hippoRoot, id);
|
|
@@ -8337,7 +8426,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8337
8426
|
// Bounded, not a TTY guard (DF1 T2, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
8338
8427
|
// the hot stdin path and a manual run share this one command.
|
|
8339
8428
|
const { text: stdinText } = await readStdinBounded();
|
|
8340
|
-
await cmdContext(hookStoreRoot(hippoRoot), args, flags, stdinText);
|
|
8429
|
+
await runHookWithStores(() => cmdContext(hookStoreRoot(hippoRoot), args, flags, stdinText));
|
|
8341
8430
|
break;
|
|
8342
8431
|
}
|
|
8343
8432
|
case 'hook':
|
|
@@ -8363,7 +8452,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8363
8452
|
case 'promote': {
|
|
8364
8453
|
const id = args[0];
|
|
8365
8454
|
if (!id) {
|
|
8366
|
-
|
|
8455
|
+
printError('Please provide a memory ID.');
|
|
8367
8456
|
process.exit(1);
|
|
8368
8457
|
}
|
|
8369
8458
|
const promoted = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
@@ -8372,7 +8461,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8372
8461
|
console.log(`Promoted ${id} to global store as ${result.globalId}`);
|
|
8373
8462
|
}
|
|
8374
8463
|
catch (err) {
|
|
8375
|
-
|
|
8464
|
+
printError(`Failed to promote: ${err.message}`);
|
|
8376
8465
|
process.exit(1);
|
|
8377
8466
|
}
|
|
8378
8467
|
});
|
|
@@ -8426,13 +8515,13 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8426
8515
|
console.log('Use --force to share anyway.');
|
|
8427
8516
|
}
|
|
8428
8517
|
else {
|
|
8429
|
-
|
|
8518
|
+
printError(`Memory not found: ${shareId}`);
|
|
8430
8519
|
process.exit(1);
|
|
8431
8520
|
}
|
|
8432
8521
|
}
|
|
8433
8522
|
}
|
|
8434
8523
|
else {
|
|
8435
|
-
|
|
8524
|
+
printError('Usage: hippo share <memory_id> [--force] or hippo share --auto [--dry-run]');
|
|
8436
8525
|
process.exit(1);
|
|
8437
8526
|
}
|
|
8438
8527
|
break;
|
|
@@ -8510,7 +8599,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8510
8599
|
captureSource = 'last-session';
|
|
8511
8600
|
}
|
|
8512
8601
|
if (!captureSource) {
|
|
8513
|
-
|
|
8602
|
+
printError('Usage: hippo capture --stdin|--file <path>|--last-session [--transcript <path>] [--log-file <path>] [--dry-run] [--global]');
|
|
8514
8603
|
process.exit(1);
|
|
8515
8604
|
}
|
|
8516
8605
|
// Bounded, and only when last-session has no explicit path: the
|
|
@@ -8558,7 +8647,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8558
8647
|
const portRaw = flags['port'] ?? process.env['HIPPO_PORT'] ?? '6789';
|
|
8559
8648
|
const port = Number(portRaw);
|
|
8560
8649
|
if (!Number.isFinite(port) || port < 0) {
|
|
8561
|
-
|
|
8650
|
+
printError(`Invalid --port: ${String(portRaw)}`);
|
|
8562
8651
|
process.exit(1);
|
|
8563
8652
|
}
|
|
8564
8653
|
const host = typeof flags['host'] === 'string' ? flags['host'] : '127.0.0.1';
|
|
@@ -8575,19 +8664,19 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8575
8664
|
requireInit(hippoRoot);
|
|
8576
8665
|
if (flags['churn'] === true) {
|
|
8577
8666
|
if (args[0] || flags['id'] !== undefined) {
|
|
8578
|
-
|
|
8579
|
-
|
|
8667
|
+
printError('Usage: hippo invalidate --churn [--dry-run]');
|
|
8668
|
+
printError('--churn takes no pattern or --id.');
|
|
8580
8669
|
process.exit(1);
|
|
8581
8670
|
}
|
|
8582
8671
|
if (!isGitRepo(process.cwd())) {
|
|
8583
|
-
|
|
8672
|
+
printError('hippo invalidate --churn must run inside a git repository.');
|
|
8584
8673
|
process.exit(1);
|
|
8585
8674
|
}
|
|
8586
8675
|
const churnDryRun = flags['dry-run'] === true;
|
|
8587
8676
|
let churnFailed = false;
|
|
8588
8677
|
for (const { root, result } of runChurnStaleForRepo(hippoRoot, churnDryRun)) {
|
|
8589
8678
|
if (result.error) {
|
|
8590
|
-
|
|
8679
|
+
printError(`Churn-staleness check failed for ${root}: ${result.error}`);
|
|
8591
8680
|
churnFailed = true;
|
|
8592
8681
|
continue;
|
|
8593
8682
|
}
|
|
@@ -8613,21 +8702,21 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8613
8702
|
if (flags['id'] === true) {
|
|
8614
8703
|
// Value-less --id must never silently fall through to pattern mode
|
|
8615
8704
|
// (pattern mode writes broadly; an ignored --id reverses user intent).
|
|
8616
|
-
|
|
8705
|
+
printError('--id requires a memory id');
|
|
8617
8706
|
process.exit(1);
|
|
8618
8707
|
}
|
|
8619
8708
|
const onlyId = typeof flags['id'] === 'string' ? flags['id'] : undefined;
|
|
8620
8709
|
if (typeof flags['dry-run'] === 'string') {
|
|
8621
8710
|
// Dead: the earlier global BOOLEAN_FLAGS guard now exits first on any --dry-run=<v>.
|
|
8622
8711
|
// Kept as defence in depth on a destructive command.
|
|
8623
|
-
|
|
8712
|
+
printError('--dry-run takes no value');
|
|
8624
8713
|
process.exit(1);
|
|
8625
8714
|
}
|
|
8626
8715
|
const dryRun = flags['dry-run'] === true;
|
|
8627
8716
|
if ((target && onlyId) || (!target && !onlyId)) {
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8717
|
+
printError('Usage: hippo invalidate "<old pattern>" [--dry-run] [--reason "<why>"]');
|
|
8718
|
+
printError(' hippo invalidate --id <memory-id> [--dry-run] [--reason "<why>"]');
|
|
8719
|
+
printError('Pass a pattern OR --id, not both. Tag matching is EXACT: the full pattern must equal a tag.');
|
|
8631
8720
|
process.exit(1);
|
|
8632
8721
|
}
|
|
8633
8722
|
const reason = flags['reason'] || null;
|
|
@@ -8686,7 +8775,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8686
8775
|
cmdGraph(hippoRoot, args, flags);
|
|
8687
8776
|
break;
|
|
8688
8777
|
default:
|
|
8689
|
-
|
|
8778
|
+
printError(`Unknown command: ${command}`);
|
|
8690
8779
|
printUsage();
|
|
8691
8780
|
process.exit(1);
|
|
8692
8781
|
}
|
|
@@ -8697,7 +8786,7 @@ export async function runCli(argv = process.argv) {
|
|
|
8697
8786
|
await main(command, args, flags, getHippoRoot(process.cwd()));
|
|
8698
8787
|
}
|
|
8699
8788
|
catch (err) {
|
|
8700
|
-
|
|
8789
|
+
printError('Error:', err instanceof Error ? err.message : err);
|
|
8701
8790
|
process.exit(1);
|
|
8702
8791
|
}
|
|
8703
8792
|
}
|