hippo-memory 1.38.10 → 1.40.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 +8 -0
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +36 -28
- package/dist/api.js.map +1 -1
- package/dist/card.d.ts +45 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/card.js +16 -0
- package/dist/card.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +451 -36
- package/dist/cli.js.map +1 -1
- package/dist/connectors/slack/backfill.js +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +168 -1
- package/dist/db.js.map +1 -1
- package/dist/handoff.d.ts +22 -0
- package/dist/handoff.d.ts.map +1 -1
- package/dist/handoff.js +34 -0
- package/dist/handoff.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +19 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/src/api.js +36 -28
- package/dist/src/api.js.map +1 -1
- package/dist/src/card.js +16 -0
- package/dist/src/card.js.map +1 -0
- package/dist/src/cli.js +451 -36
- package/dist/src/cli.js.map +1 -1
- package/dist/src/connectors/slack/backfill.js +1 -1
- package/dist/src/db.js +168 -1
- package/dist/src/db.js.map +1 -1
- package/dist/src/handoff.js +34 -0
- package/dist/src/handoff.js.map +1 -1
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/server.js +19 -17
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/store.js +533 -35
- package/dist/src/store.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/store.d.ts +59 -5
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +533 -35
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist-ui/index.html +12 -12
- 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 +1 -1
- package/dist/benchmarks/e1.3/scenarios.json +0 -2587
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* hippo snapshot <save|show|clear>
|
|
14
14
|
* hippo session <log|show|latest|resume|complete>
|
|
15
15
|
* hippo handoff <create|latest|show>
|
|
16
|
+
* hippo card <create|show|list|claim|block|review|complete|comment>
|
|
16
17
|
* hippo current <show>
|
|
17
18
|
* hippo forget <id> [--archive --reason "<why>"]
|
|
18
19
|
* hippo reject <id>|--value "<text>" --reason "<why>"
|
|
@@ -37,9 +38,12 @@ import { execFileSync, execSync, spawn } from 'child_process';
|
|
|
37
38
|
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, isCodexWrapperInstalled, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
38
39
|
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
|
|
39
40
|
import { detectSecret } from './secret-detect.js';
|
|
40
|
-
import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById,
|
|
41
|
+
import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, createCard, loadCard, listCards, loadCardDeps, loadCardRuns, loadCardComments, claimCard, blockCard, reviewCard, completeCard, addCardComment, loadLatestHandoffForCard, } from './store.js';
|
|
41
42
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
42
43
|
import { RejectedValueError } from './rejection.js';
|
|
44
|
+
import { isHandoffOutcome, formatHandoffEvidenceLine } from './handoff.js';
|
|
45
|
+
import { isCardStatus } from './card.js';
|
|
46
|
+
import { passesScopeFilterForRecall } from './recall-scope.js';
|
|
43
47
|
import { markRetrieved, hybridSearch, physicsSearch, explainMatch, textOverlap, tokenize as tokenizeQuery } from './search.js';
|
|
44
48
|
import { compareEntryIdentity } from './compare.js';
|
|
45
49
|
import { renderTraceContent, parseSteps } from './trace.js';
|
|
@@ -251,8 +255,8 @@ export function parseArgs(argv) {
|
|
|
251
255
|
i++;
|
|
252
256
|
}
|
|
253
257
|
else {
|
|
254
|
-
// Check if it's a repeatable flag (tag, artifact, link, step)
|
|
255
|
-
if (key === 'tag' || key === 'artifact' || key === 'link' || key === 'step') {
|
|
258
|
+
// Check if it's a repeatable flag (tag, artifact, link, step, constraint, depends-on)
|
|
259
|
+
if (key === 'tag' || key === 'artifact' || key === 'link' || key === 'step' || key === 'constraint' || key === 'depends-on') {
|
|
256
260
|
if (Array.isArray(flags[key])) {
|
|
257
261
|
flags[key].push(next);
|
|
258
262
|
}
|
|
@@ -1544,37 +1548,16 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1544
1548
|
const rawEvents = sessionId
|
|
1545
1549
|
? listSessionEvents(hippoRoot, tenantId, { session_id: sessionId, limit: 5 })
|
|
1546
1550
|
: [];
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
|
|
1551
|
-
// (matches existing memory recall behavior).
|
|
1552
|
-
// Scope filter mirrors api.recall: exact match when scope is set, default-deny
|
|
1553
|
-
// on ANY `<source>:private:*` and 'unknown:legacy' otherwise. recallActiveScope
|
|
1554
|
-
// merges --scope flag and detectScope() so auto-detected source contexts get
|
|
1555
|
-
// matched. v1.2.1: generalized from slack-only to source-agnostic via
|
|
1556
|
-
// api.isPrivateScope so v1.3 GitHub (and future sources) cannot leak.
|
|
1557
|
-
const effectiveScope = recallActiveScope ?? '';
|
|
1558
|
-
const passesScopeFilter = (s) => {
|
|
1559
|
-
if (effectiveScope) {
|
|
1560
|
-
return s === effectiveScope;
|
|
1561
|
-
}
|
|
1562
|
-
if (s === null)
|
|
1563
|
-
return true;
|
|
1564
|
-
if (api.isPrivateScope(s))
|
|
1565
|
-
return false;
|
|
1566
|
-
// v1.7.2: read from RECALL_DEFAULT_DENY_SCOPES (single source of truth
|
|
1567
|
-
// shared with SQL + api.passesScopeFilterForRecall).
|
|
1568
|
-
if (RECALL_DEFAULT_DENY_SCOPES.includes(s))
|
|
1569
|
-
return false;
|
|
1570
|
-
return true;
|
|
1571
|
-
};
|
|
1551
|
+
// W1: was its own copy of passesScopeFilterForRecall (cloned 3x);
|
|
1552
|
+
// calls the shared helper directly now (recallActiveScope merges
|
|
1553
|
+
// --scope and detectScope()).
|
|
1554
|
+
const effectiveScope = recallActiveScope || undefined;
|
|
1572
1555
|
const rowScope = (r) => r?.scope ?? null;
|
|
1573
1556
|
activeSnapshot =
|
|
1574
|
-
rawSnapshot &&
|
|
1557
|
+
rawSnapshot && passesScopeFilterForRecall(rowScope(rawSnapshot), effectiveScope) ? rawSnapshot : null;
|
|
1575
1558
|
sessionHandoff =
|
|
1576
|
-
rawHandoff &&
|
|
1577
|
-
recentSessionEvents = rawEvents.filter((e) =>
|
|
1559
|
+
rawHandoff && passesScopeFilterForRecall(rowScope(rawHandoff), effectiveScope) ? rawHandoff : null;
|
|
1560
|
+
recentSessionEvents = rawEvents.filter((e) => passesScopeFilterForRecall(rowScope(e), effectiveScope));
|
|
1578
1561
|
const tokenize = (s) => s ? Math.ceil(s.length / 4) : 0;
|
|
1579
1562
|
continuityTokens =
|
|
1580
1563
|
tokenize(activeSnapshot?.task) +
|
|
@@ -1583,6 +1566,11 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1583
1566
|
tokenize(sessionHandoff?.summary) +
|
|
1584
1567
|
tokenize(sessionHandoff?.nextAction) +
|
|
1585
1568
|
(sessionHandoff?.artifacts ?? []).reduce((acc, a) => acc + tokenize(a), 0) +
|
|
1569
|
+
(sessionHandoff?.constraints ?? []).reduce((acc, c) => acc + tokenize(c), 0) +
|
|
1570
|
+
tokenize(sessionHandoff?.evidence ? formatHandoffEvidenceLine(sessionHandoff.evidence) : null) +
|
|
1571
|
+
tokenize(sessionHandoff?.outcome) +
|
|
1572
|
+
tokenize(sessionHandoff?.targetRuntime) +
|
|
1573
|
+
tokenize(sessionHandoff?.cardId) +
|
|
1586
1574
|
recentSessionEvents.reduce((acc, e) => acc + tokenize(e.content), 0);
|
|
1587
1575
|
}
|
|
1588
1576
|
const hasContinuity = activeSnapshot !== null
|
|
@@ -2936,6 +2924,30 @@ async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
2936
2924
|
* `__session-end-worker` subcommand (not user-facing). Failures in one stage
|
|
2937
2925
|
* do not block the other.
|
|
2938
2926
|
*/
|
|
2927
|
+
// Best-effort git state; a missing git, non-repo cwd, or the timeout all
|
|
2928
|
+
// yield null fields rather than throw (autolearn.ts execFileSync shape).
|
|
2929
|
+
function collectHandoffEvidence(cwd, testStatus) {
|
|
2930
|
+
let gitRef = null;
|
|
2931
|
+
try {
|
|
2932
|
+
gitRef = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
2933
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
2934
|
+
}).trim() || null;
|
|
2935
|
+
}
|
|
2936
|
+
catch {
|
|
2937
|
+
gitRef = null;
|
|
2938
|
+
}
|
|
2939
|
+
let dirtyTree = null;
|
|
2940
|
+
try {
|
|
2941
|
+
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
2942
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
2943
|
+
});
|
|
2944
|
+
dirtyTree = status.trim().length > 0;
|
|
2945
|
+
}
|
|
2946
|
+
catch {
|
|
2947
|
+
dirtyTree = null;
|
|
2948
|
+
}
|
|
2949
|
+
return { gitRef, dirtyTree, testStatus };
|
|
2950
|
+
}
|
|
2939
2951
|
async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
2940
2952
|
try {
|
|
2941
2953
|
await cmdSleep(hippoRoot, flags);
|
|
@@ -2972,6 +2984,25 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
2972
2984
|
// in loadFreshActiveTaskSnapshot is the backstop layer, not this close.
|
|
2973
2985
|
const closeLogFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
|
|
2974
2986
|
const closeSessionId = typeof flags['session-id'] === 'string' ? flags['session-id'] : null;
|
|
2987
|
+
// Handoff write happens BEFORE the snapshot close below, while the
|
|
2988
|
+
// snapshot writeSessionEndHandoff reads is still active.
|
|
2989
|
+
if (closeSessionId) {
|
|
2990
|
+
try {
|
|
2991
|
+
const activeForHandoff = loadActiveTaskSnapshot(hippoRoot, resolveTenantId({}));
|
|
2992
|
+
if (!activeForHandoff || activeForHandoff.session_id !== closeSessionId) {
|
|
2993
|
+
appendSessionEndCloseLog(closeLogFile, 'skip: no active snapshot for session');
|
|
2994
|
+
}
|
|
2995
|
+
else {
|
|
2996
|
+
const evidence = collectHandoffEvidence(process.cwd(), 'unknown');
|
|
2997
|
+
const handoff = writeSessionEndHandoff(hippoRoot, resolveTenantId({}), closeSessionId, evidence);
|
|
2998
|
+
appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` : 'skip: handoff newer than snapshot');
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
catch (err) {
|
|
3002
|
+
// SAFETY: catch clauses bind unknown, but Node/V8 always throws an Error here.
|
|
3003
|
+
appendSessionEndCloseLog(closeLogFile, `handoff write failed: ${err.message}`);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
2975
3006
|
try {
|
|
2976
3007
|
if (closeSessionId) {
|
|
2977
3008
|
const closed = closeTaskSnapshotsForSession(hippoRoot, resolveTenantId({}), closeSessionId);
|
|
@@ -3753,17 +3784,17 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
3753
3784
|
return;
|
|
3754
3785
|
}
|
|
3755
3786
|
if (subcommand === 'complete') {
|
|
3756
|
-
const
|
|
3787
|
+
const outcomeRaw = String(flags['outcome'] ?? '').trim();
|
|
3757
3788
|
const summary = String(flags['summary'] ?? '').trim();
|
|
3758
|
-
const validOutcomes = ['success', 'failure', 'partial'];
|
|
3759
3789
|
if (!sessionId) {
|
|
3760
3790
|
console.error('Usage: hippo session complete --session <session-id> --outcome <success|failure|partial> [--summary "..."]');
|
|
3761
3791
|
process.exit(1);
|
|
3762
3792
|
}
|
|
3763
|
-
if (!
|
|
3764
|
-
console.error(`Invalid outcome: "${
|
|
3793
|
+
if (!isHandoffOutcome(outcomeRaw)) {
|
|
3794
|
+
console.error(`Invalid outcome: "${outcomeRaw}". Must be one of: success, failure, partial.`);
|
|
3765
3795
|
process.exit(1);
|
|
3766
3796
|
}
|
|
3797
|
+
const outcome = outcomeRaw;
|
|
3767
3798
|
const metadata = { ended_at: new Date().toISOString() };
|
|
3768
3799
|
if (summary)
|
|
3769
3800
|
metadata.summary = summary;
|
|
@@ -3776,6 +3807,10 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
3776
3807
|
metadata,
|
|
3777
3808
|
});
|
|
3778
3809
|
console.log(`Completed session ${event.session_id} with outcome=${outcome} (event #${event.id})`);
|
|
3810
|
+
const stamped = stampHandoffOutcome(hippoRoot, resolveTenantId({}), sessionId, outcome);
|
|
3811
|
+
if (stamped > 0) {
|
|
3812
|
+
console.log(`Stamped outcome on handoff for session ${sessionId}`);
|
|
3813
|
+
}
|
|
3779
3814
|
return;
|
|
3780
3815
|
}
|
|
3781
3816
|
if (subcommand === 'resume') {
|
|
@@ -3794,6 +3829,12 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
3794
3829
|
lines.push(`- Task: ${handoff.taskId}`);
|
|
3795
3830
|
if (handoff.repoRoot)
|
|
3796
3831
|
lines.push(`- Repo: ${handoff.repoRoot}`);
|
|
3832
|
+
if (handoff.outcome)
|
|
3833
|
+
lines.push(`- Outcome: ${handoff.outcome}`);
|
|
3834
|
+
if (handoff.targetRuntime)
|
|
3835
|
+
lines.push(`- Target runtime: ${handoff.targetRuntime}`);
|
|
3836
|
+
if (handoff.cardId)
|
|
3837
|
+
lines.push(`- Card: ${handoff.cardId}`);
|
|
3797
3838
|
lines.push('', '### Summary', handoff.summary);
|
|
3798
3839
|
if (handoff.nextAction) {
|
|
3799
3840
|
lines.push('', '### Next action', handoff.nextAction);
|
|
@@ -3804,6 +3845,15 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
3804
3845
|
lines.push(`- ${artifact}`);
|
|
3805
3846
|
}
|
|
3806
3847
|
}
|
|
3848
|
+
if (handoff.constraints && handoff.constraints.length > 0) {
|
|
3849
|
+
lines.push('', '### Constraints');
|
|
3850
|
+
for (const constraint of handoff.constraints) {
|
|
3851
|
+
lines.push(`- ${constraint}`);
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
if (handoff.evidence) {
|
|
3855
|
+
lines.push('', '### Evidence', formatHandoffEvidenceLine(handoff.evidence));
|
|
3856
|
+
}
|
|
3807
3857
|
lines.push('');
|
|
3808
3858
|
console.log(lines.join('\n'));
|
|
3809
3859
|
return;
|
|
@@ -3819,6 +3869,12 @@ function printHandoff(handoff) {
|
|
|
3819
3869
|
console.log(`- Task: ${handoff.taskId}`);
|
|
3820
3870
|
if (handoff.repoRoot)
|
|
3821
3871
|
console.log(`- Repo: ${handoff.repoRoot}`);
|
|
3872
|
+
if (handoff.outcome)
|
|
3873
|
+
console.log(`- Outcome: ${handoff.outcome}`);
|
|
3874
|
+
if (handoff.targetRuntime)
|
|
3875
|
+
console.log(`- Target runtime: ${handoff.targetRuntime}`);
|
|
3876
|
+
if (handoff.cardId)
|
|
3877
|
+
console.log(`- Card: ${handoff.cardId}`);
|
|
3822
3878
|
console.log('');
|
|
3823
3879
|
console.log('### Summary');
|
|
3824
3880
|
console.log(handoff.summary);
|
|
@@ -3834,6 +3890,18 @@ function printHandoff(handoff) {
|
|
|
3834
3890
|
console.log(`- ${artifact}`);
|
|
3835
3891
|
}
|
|
3836
3892
|
}
|
|
3893
|
+
if (handoff.constraints && handoff.constraints.length > 0) {
|
|
3894
|
+
console.log('');
|
|
3895
|
+
console.log('### Constraints');
|
|
3896
|
+
for (const constraint of handoff.constraints) {
|
|
3897
|
+
console.log(`- ${constraint}`);
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
if (handoff.evidence) {
|
|
3901
|
+
console.log('');
|
|
3902
|
+
console.log('### Evidence');
|
|
3903
|
+
console.log(formatHandoffEvidenceLine(handoff.evidence));
|
|
3904
|
+
}
|
|
3837
3905
|
console.log('');
|
|
3838
3906
|
}
|
|
3839
3907
|
function cmdHandoff(hippoRoot, args, flags) {
|
|
@@ -3842,7 +3910,12 @@ function cmdHandoff(hippoRoot, args, flags) {
|
|
|
3842
3910
|
if (subcommand === 'create') {
|
|
3843
3911
|
const summary = String(flags['summary'] ?? '').trim();
|
|
3844
3912
|
if (!summary) {
|
|
3845
|
-
console.error('Usage: hippo handoff create --summary "..." [--next "..."] [--session <id>] [--task <id>] [--artifact <path>...]');
|
|
3913
|
+
console.error('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>]');
|
|
3914
|
+
process.exit(1);
|
|
3915
|
+
}
|
|
3916
|
+
const outcomeRaw = flags['outcome'];
|
|
3917
|
+
if (outcomeRaw !== undefined && !isHandoffOutcome(outcomeRaw)) {
|
|
3918
|
+
console.error(`Invalid outcome: "${String(outcomeRaw)}". Must be one of: success, failure, partial.`);
|
|
3846
3919
|
process.exit(1);
|
|
3847
3920
|
}
|
|
3848
3921
|
const sessionId = String(flags['session'] ?? flags['id'] ?? '').trim() || `fallback-${Date.now()}-${process.pid}`;
|
|
@@ -3852,6 +3925,22 @@ function cmdHandoff(hippoRoot, args, flags) {
|
|
|
3852
3925
|
const artifacts = Array.isArray(artifactFlag)
|
|
3853
3926
|
? artifactFlag
|
|
3854
3927
|
: (typeof artifactFlag === 'string' ? [artifactFlag] : []);
|
|
3928
|
+
const constraintFlag = flags['constraint'];
|
|
3929
|
+
const isFlagString = (v) => typeof v === 'string';
|
|
3930
|
+
const constraints = Array.isArray(constraintFlag)
|
|
3931
|
+
? constraintFlag
|
|
3932
|
+
: (isFlagString(constraintFlag) ? [constraintFlag] : []);
|
|
3933
|
+
for (const name of ['target-runtime', 'card-id']) {
|
|
3934
|
+
// parseArgs turns a value-less flag into `true`; refuse rather than store "true".
|
|
3935
|
+
if (flags[name] === true) {
|
|
3936
|
+
console.error(`--${name} needs a value`);
|
|
3937
|
+
process.exit(1);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
const targetRuntime = String(flags['target-runtime'] ?? '').trim() || undefined;
|
|
3941
|
+
const cardId = String(flags['card-id'] ?? '').trim() || undefined;
|
|
3942
|
+
const testStatus = String(flags['tests'] ?? '').trim();
|
|
3943
|
+
const evidence = collectHandoffEvidence(process.cwd(), testStatus === 'pass' || testStatus === 'fail' ? testStatus : 'unknown');
|
|
3855
3944
|
const handoff = saveSessionHandoff(hippoRoot, resolveTenantId({}), {
|
|
3856
3945
|
version: 1,
|
|
3857
3946
|
sessionId,
|
|
@@ -3860,6 +3949,12 @@ function cmdHandoff(hippoRoot, args, flags) {
|
|
|
3860
3949
|
summary,
|
|
3861
3950
|
nextAction,
|
|
3862
3951
|
artifacts,
|
|
3952
|
+
constraints,
|
|
3953
|
+
evidence,
|
|
3954
|
+
// SAFETY: isHandoffOutcome above already refused any non-matching value.
|
|
3955
|
+
outcome: outcomeRaw,
|
|
3956
|
+
targetRuntime,
|
|
3957
|
+
cardId,
|
|
3863
3958
|
});
|
|
3864
3959
|
console.log(`Created session handoff for session ${handoff.sessionId}`);
|
|
3865
3960
|
console.log(` Summary: ${handoff.summary}`);
|
|
@@ -3868,6 +3963,17 @@ function cmdHandoff(hippoRoot, args, flags) {
|
|
|
3868
3963
|
if (handoff.artifacts && handoff.artifacts.length > 0) {
|
|
3869
3964
|
console.log(` Artifacts: ${handoff.artifacts.join(', ')}`);
|
|
3870
3965
|
}
|
|
3966
|
+
if (handoff.constraints && handoff.constraints.length > 0) {
|
|
3967
|
+
console.log(` Constraints: ${handoff.constraints.join(', ')}`);
|
|
3968
|
+
}
|
|
3969
|
+
if (handoff.outcome)
|
|
3970
|
+
console.log(` Outcome: ${handoff.outcome}`);
|
|
3971
|
+
if (handoff.targetRuntime)
|
|
3972
|
+
console.log(` Target runtime: ${handoff.targetRuntime}`);
|
|
3973
|
+
if (handoff.cardId)
|
|
3974
|
+
console.log(` Card: ${handoff.cardId}`);
|
|
3975
|
+
if (handoff.evidence)
|
|
3976
|
+
console.log(` Evidence: ${formatHandoffEvidenceLine(handoff.evidence)}`);
|
|
3871
3977
|
return;
|
|
3872
3978
|
}
|
|
3873
3979
|
if (subcommand === 'latest') {
|
|
@@ -3920,6 +4026,281 @@ function cmdHandoff(hippoRoot, args, flags) {
|
|
|
3920
4026
|
console.error('Usage: hippo handoff <create|latest|show>');
|
|
3921
4027
|
process.exit(1);
|
|
3922
4028
|
}
|
|
4029
|
+
// Mirrors ARCHIVE_REASON_REQUIRED so the block message can't drift from its usage line.
|
|
4030
|
+
const CARD_BLOCK_REASON_REQUIRED = 'hippo card block <id> requires --reason "<why>" (recorded as a comment).';
|
|
4031
|
+
function printCard(hippoRoot, tenantId, card) {
|
|
4032
|
+
console.log(`## Card ${card.id}\n`);
|
|
4033
|
+
console.log(`- Title: ${card.title}`);
|
|
4034
|
+
console.log(`- Status: ${card.status}`);
|
|
4035
|
+
if (card.assigneeRuntime)
|
|
4036
|
+
console.log(`- Assignee: ${card.assigneeRuntime}`);
|
|
4037
|
+
if (card.repo)
|
|
4038
|
+
console.log(`- Repo: ${card.repo}`);
|
|
4039
|
+
if (card.contract)
|
|
4040
|
+
console.log(`- Contract: ${card.contract}`);
|
|
4041
|
+
if (card.budget !== null)
|
|
4042
|
+
console.log(`- Budget: ${card.budget}`);
|
|
4043
|
+
console.log(`- Updated: ${card.updatedAt}`);
|
|
4044
|
+
const deps = loadCardDeps(hippoRoot, tenantId, card.id);
|
|
4045
|
+
if (deps.parents.length > 0)
|
|
4046
|
+
console.log(`- Parents: ${deps.parents.join(', ')}`);
|
|
4047
|
+
if (deps.children.length > 0)
|
|
4048
|
+
console.log(`- Children: ${deps.children.join(', ')}`);
|
|
4049
|
+
const runs = loadCardRuns(hippoRoot, tenantId, card.id);
|
|
4050
|
+
if (runs.length > 0) {
|
|
4051
|
+
console.log('\n### Runs');
|
|
4052
|
+
for (const run of runs) {
|
|
4053
|
+
console.log(`- ${run.runtime} started ${run.started}${run.ended ? ` ended ${run.ended} (${run.outcome})` : ' (open)'}`);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
const comments = loadCardComments(hippoRoot, tenantId, card.id);
|
|
4057
|
+
if (comments.length > 0) {
|
|
4058
|
+
console.log('\n### Comments');
|
|
4059
|
+
for (const comment of comments) {
|
|
4060
|
+
console.log(`- [${comment.createdAt}] ${comment.author}: ${comment.body}`);
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
const handoff = loadLatestHandoffForCard(hippoRoot, tenantId, card.id);
|
|
4064
|
+
if (handoff) {
|
|
4065
|
+
console.log('\n### Latest handoff');
|
|
4066
|
+
console.log(`- Session: ${handoff.sessionId}, updated ${handoff.updatedAt}`);
|
|
4067
|
+
console.log(handoff.summary);
|
|
4068
|
+
}
|
|
4069
|
+
console.log('');
|
|
4070
|
+
}
|
|
4071
|
+
// parseArgs turns a value-less flag into `true`; refuse rather than silently
|
|
4072
|
+
// stringifying it (String(true) === 'true'), mirroring cmdHandoff's guard.
|
|
4073
|
+
function cardStringFlag(flags, key) {
|
|
4074
|
+
const v = flags[key];
|
|
4075
|
+
if (v === undefined)
|
|
4076
|
+
return undefined;
|
|
4077
|
+
if (v === true || v === false || Array.isArray(v)) {
|
|
4078
|
+
console.error(`--${key} requires a value`);
|
|
4079
|
+
process.exit(1);
|
|
4080
|
+
}
|
|
4081
|
+
return v.trim();
|
|
4082
|
+
}
|
|
4083
|
+
const CARD_SUBCOMMAND_FLAGS = {
|
|
4084
|
+
create: ['title', 'repo', 'contract', 'budget', 'depends-on'],
|
|
4085
|
+
show: ['json'],
|
|
4086
|
+
list: ['status', 'json'],
|
|
4087
|
+
claim: ['runtime', 'session'],
|
|
4088
|
+
block: ['reason'],
|
|
4089
|
+
review: new Array(),
|
|
4090
|
+
complete: ['outcome'],
|
|
4091
|
+
comment: ['body', 'author'],
|
|
4092
|
+
};
|
|
4093
|
+
function cmdCard(hippoRoot, args, flags) {
|
|
4094
|
+
requireInit(hippoRoot);
|
|
4095
|
+
const tenantId = resolveTenantId({});
|
|
4096
|
+
const subcommand = args[0] ?? '';
|
|
4097
|
+
if (Object.hasOwn(CARD_SUBCOMMAND_FLAGS, subcommand)) {
|
|
4098
|
+
// SAFETY: hasOwn, unlike `in`, skips inherited keys such as constructor, so subcommand is a real key.
|
|
4099
|
+
const allowedFlags = CARD_SUBCOMMAND_FLAGS[subcommand];
|
|
4100
|
+
for (const key of Object.keys(flags)) {
|
|
4101
|
+
if (!allowedFlags.includes(key)) {
|
|
4102
|
+
// --flag=value never splits on '=' (see parseArgs), so it lands here as one long key.
|
|
4103
|
+
const hint = key.includes('=') ? ` Use --${key.slice(0, key.indexOf('='))} <value>, not --${key}.` : '';
|
|
4104
|
+
const valid = allowedFlags.length > 0 ? allowedFlags.map((f) => `--${f}`).join(', ') : '(none)';
|
|
4105
|
+
console.error(`Unknown flag --${key} for hippo card ${subcommand}.${hint} Valid flags: ${valid}`);
|
|
4106
|
+
process.exit(1);
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
if (subcommand === 'create') {
|
|
4111
|
+
const title = cardStringFlag(flags, 'title') ?? '';
|
|
4112
|
+
if (!title) {
|
|
4113
|
+
console.error('Usage: hippo card create --title "..." [--repo <name>] [--contract <text>] [--budget <n>] [--depends-on <id>...]');
|
|
4114
|
+
process.exit(1);
|
|
4115
|
+
}
|
|
4116
|
+
const repo = cardStringFlag(flags, 'repo') || undefined;
|
|
4117
|
+
const contract = cardStringFlag(flags, 'contract') || undefined;
|
|
4118
|
+
const budgetRaw = cardStringFlag(flags, 'budget');
|
|
4119
|
+
let budget;
|
|
4120
|
+
if (budgetRaw !== undefined) {
|
|
4121
|
+
if (!/^\d+$/.test(budgetRaw)) {
|
|
4122
|
+
console.error(`Invalid budget: "${budgetRaw}" (expected a positive integer)`);
|
|
4123
|
+
process.exit(1);
|
|
4124
|
+
}
|
|
4125
|
+
budget = Number(budgetRaw);
|
|
4126
|
+
}
|
|
4127
|
+
const dependsOnFlag = flags['depends-on'];
|
|
4128
|
+
if (dependsOnFlag === true) {
|
|
4129
|
+
console.error('--depends-on requires a value');
|
|
4130
|
+
process.exit(1);
|
|
4131
|
+
}
|
|
4132
|
+
const dependsOn = Array.isArray(dependsOnFlag) ? dependsOnFlag : [];
|
|
4133
|
+
let card;
|
|
4134
|
+
try {
|
|
4135
|
+
card = createCard(hippoRoot, tenantId, { title, repo, contract, budget, dependsOn });
|
|
4136
|
+
}
|
|
4137
|
+
catch (error) {
|
|
4138
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
4139
|
+
process.exit(1);
|
|
4140
|
+
}
|
|
4141
|
+
console.log(`Created card ${card.id} (status: ${card.status})`);
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
if (subcommand === 'show') {
|
|
4145
|
+
const id = args[1];
|
|
4146
|
+
if (!id) {
|
|
4147
|
+
console.error('Usage: hippo card show <id> [--json]');
|
|
4148
|
+
process.exit(1);
|
|
4149
|
+
}
|
|
4150
|
+
const card = loadCard(hippoRoot, tenantId, id);
|
|
4151
|
+
if (!card) {
|
|
4152
|
+
console.error(`No card found with id ${id}.`);
|
|
4153
|
+
process.exit(1);
|
|
4154
|
+
}
|
|
4155
|
+
if (flags['json']) {
|
|
4156
|
+
console.log(JSON.stringify({
|
|
4157
|
+
card,
|
|
4158
|
+
deps: loadCardDeps(hippoRoot, tenantId, id),
|
|
4159
|
+
runs: loadCardRuns(hippoRoot, tenantId, id),
|
|
4160
|
+
comments: loadCardComments(hippoRoot, tenantId, id),
|
|
4161
|
+
handoff: loadLatestHandoffForCard(hippoRoot, tenantId, id),
|
|
4162
|
+
}, null, 2));
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
4165
|
+
printCard(hippoRoot, tenantId, card);
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4168
|
+
if (subcommand === 'list') {
|
|
4169
|
+
const status = cardStringFlag(flags, 'status');
|
|
4170
|
+
if (status !== undefined && !isCardStatus(status)) {
|
|
4171
|
+
console.error(`Invalid status: "${status}".`);
|
|
4172
|
+
process.exit(1);
|
|
4173
|
+
}
|
|
4174
|
+
const cards = listCards(hippoRoot, tenantId, { status });
|
|
4175
|
+
if (flags['json']) {
|
|
4176
|
+
console.log(JSON.stringify({ cards }, null, 2));
|
|
4177
|
+
return;
|
|
4178
|
+
}
|
|
4179
|
+
if (cards.length === 0) {
|
|
4180
|
+
console.log('No cards found.');
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
for (const card of cards) {
|
|
4184
|
+
console.log(`${card.id}\t${card.status}\t${card.title}${card.assigneeRuntime ? `\t(${card.assigneeRuntime})` : ''}`);
|
|
4185
|
+
}
|
|
4186
|
+
return;
|
|
4187
|
+
}
|
|
4188
|
+
if (subcommand === 'claim') {
|
|
4189
|
+
const id = args[1];
|
|
4190
|
+
const runtime = cardStringFlag(flags, 'runtime') ?? '';
|
|
4191
|
+
if (!id || !runtime) {
|
|
4192
|
+
console.error('Usage: hippo card claim <id> --runtime <name> [--session <id>]');
|
|
4193
|
+
process.exit(1);
|
|
4194
|
+
}
|
|
4195
|
+
const sessionId = cardStringFlag(flags, 'session') || undefined;
|
|
4196
|
+
let card;
|
|
4197
|
+
try {
|
|
4198
|
+
card = claimCard(hippoRoot, tenantId, id, runtime, sessionId);
|
|
4199
|
+
}
|
|
4200
|
+
catch (error) {
|
|
4201
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
4202
|
+
process.exit(1);
|
|
4203
|
+
}
|
|
4204
|
+
if (!card) {
|
|
4205
|
+
console.error(`Could not claim card ${id} (not ready/blocked, or already claimed).`);
|
|
4206
|
+
process.exit(1);
|
|
4207
|
+
}
|
|
4208
|
+
console.log(`Claimed card ${card.id} for ${runtime}`);
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
if (subcommand === 'block') {
|
|
4212
|
+
const id = args[1];
|
|
4213
|
+
const reason = cardStringFlag(flags, 'reason') ?? '';
|
|
4214
|
+
if (!id || !reason) {
|
|
4215
|
+
console.error(CARD_BLOCK_REASON_REQUIRED);
|
|
4216
|
+
process.exit(1);
|
|
4217
|
+
}
|
|
4218
|
+
let card;
|
|
4219
|
+
try {
|
|
4220
|
+
card = blockCard(hippoRoot, tenantId, id, reason);
|
|
4221
|
+
}
|
|
4222
|
+
catch (error) {
|
|
4223
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
4224
|
+
process.exit(1);
|
|
4225
|
+
}
|
|
4226
|
+
if (!card) {
|
|
4227
|
+
console.error(`Could not block card ${id} (not running).`);
|
|
4228
|
+
process.exit(1);
|
|
4229
|
+
}
|
|
4230
|
+
console.log(`Blocked card ${card.id}`);
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
if (subcommand === 'review') {
|
|
4234
|
+
const id = args[1];
|
|
4235
|
+
if (!id) {
|
|
4236
|
+
console.error('Usage: hippo card review <id>');
|
|
4237
|
+
process.exit(1);
|
|
4238
|
+
}
|
|
4239
|
+
let card;
|
|
4240
|
+
try {
|
|
4241
|
+
card = reviewCard(hippoRoot, tenantId, id);
|
|
4242
|
+
}
|
|
4243
|
+
catch (error) {
|
|
4244
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
4245
|
+
process.exit(1);
|
|
4246
|
+
}
|
|
4247
|
+
if (!card) {
|
|
4248
|
+
console.error(`Could not move card ${id} to review (not running).`);
|
|
4249
|
+
process.exit(1);
|
|
4250
|
+
}
|
|
4251
|
+
console.log(`Card ${card.id} moved to review`);
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
if (subcommand === 'complete') {
|
|
4255
|
+
const id = args[1];
|
|
4256
|
+
const outcomeRaw = flags['outcome'];
|
|
4257
|
+
if (!id || !isHandoffOutcome(outcomeRaw)) {
|
|
4258
|
+
console.error('Usage: hippo card complete <id> --outcome <success|failure|partial>');
|
|
4259
|
+
process.exit(1);
|
|
4260
|
+
}
|
|
4261
|
+
let result;
|
|
4262
|
+
try {
|
|
4263
|
+
result = completeCard(hippoRoot, tenantId, id, outcomeRaw);
|
|
4264
|
+
}
|
|
4265
|
+
catch (error) {
|
|
4266
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
4267
|
+
process.exit(1);
|
|
4268
|
+
}
|
|
4269
|
+
if (!result) {
|
|
4270
|
+
console.error(`Could not complete card ${id} (not in review).`);
|
|
4271
|
+
process.exit(1);
|
|
4272
|
+
}
|
|
4273
|
+
console.log(`Completed card ${result.card.id} (status: ${result.card.status})`);
|
|
4274
|
+
if (result.promotedChildren.length > 0) {
|
|
4275
|
+
console.log(`Promoted to ready: ${result.promotedChildren.join(', ')}`);
|
|
4276
|
+
}
|
|
4277
|
+
return;
|
|
4278
|
+
}
|
|
4279
|
+
if (subcommand === 'comment') {
|
|
4280
|
+
const id = args[1];
|
|
4281
|
+
if (!id) {
|
|
4282
|
+
console.error('Usage: hippo card comment <id> --body "..." [--author <name>]');
|
|
4283
|
+
process.exit(1);
|
|
4284
|
+
}
|
|
4285
|
+
// Only show and comment look the card up directly; claim/block/review/complete throw from the store instead.
|
|
4286
|
+
const card = loadCard(hippoRoot, tenantId, id);
|
|
4287
|
+
if (!card) {
|
|
4288
|
+
console.error(`No card found with id ${id}.`);
|
|
4289
|
+
process.exit(1);
|
|
4290
|
+
}
|
|
4291
|
+
const body = cardStringFlag(flags, 'body') ?? '';
|
|
4292
|
+
if (!body) {
|
|
4293
|
+
console.error('Usage: hippo card comment <id> --body "..." [--author <name>]');
|
|
4294
|
+
process.exit(1);
|
|
4295
|
+
}
|
|
4296
|
+
const author = cardStringFlag(flags, 'author') || 'cli';
|
|
4297
|
+
const comment = addCardComment(hippoRoot, tenantId, id, author, body);
|
|
4298
|
+
console.log(`Added comment ${comment.id} to card ${id}`);
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
console.error('Usage: hippo card <create|show|list|claim|block|review|complete|comment>');
|
|
4302
|
+
process.exit(1);
|
|
4303
|
+
}
|
|
3923
4304
|
// ---------------------------------------------------------------------------
|
|
3924
4305
|
// E2 prediction first-class object (v0.31)
|
|
3925
4306
|
// docs/plans/2026-05-26-e2-prediction-object.md
|
|
@@ -7823,10 +8204,37 @@ Commands:
|
|
|
7823
8204
|
--session <id> Session ID (auto-generated if omitted)
|
|
7824
8205
|
--task <id> Associated task ID
|
|
7825
8206
|
--artifact <path> Related file path (repeatable)
|
|
8207
|
+
--constraint <text> Constraint for the successor to respect (repeatable)
|
|
8208
|
+
--outcome <o> success | failure | partial
|
|
8209
|
+
--target-runtime <n> Name of the runtime the successor will run in
|
|
8210
|
+
--card-id <id> Associated card/ticket ID
|
|
8211
|
+
--tests <status> pass | fail | unknown (default: unknown)
|
|
7826
8212
|
handoff latest Show the most recent handoff
|
|
7827
8213
|
--session <id> Filter by session
|
|
7828
8214
|
--json Output as JSON
|
|
7829
8215
|
handoff show <id> Show a specific handoff by ID
|
|
8216
|
+
card <sub> Manage claimable work-queue cards
|
|
8217
|
+
card create Create a new card
|
|
8218
|
+
--title <text> Card title (required)
|
|
8219
|
+
--repo <name> Associated repo
|
|
8220
|
+
--contract <text> Associated contract
|
|
8221
|
+
--budget <n> Token/step budget
|
|
8222
|
+
--depends-on <id> Parent card id (repeatable)
|
|
8223
|
+
card show <id> Show a card, its deps, runs, comments and latest handoff
|
|
8224
|
+
--json Output as JSON
|
|
8225
|
+
card list List cards, newest-updated first
|
|
8226
|
+
--status <status> Filter by status
|
|
8227
|
+
card claim <id> Claim a ready or blocked card
|
|
8228
|
+
--runtime <name> Claiming runtime (required)
|
|
8229
|
+
--session <id> Session ID
|
|
8230
|
+
card block <id> Block a running card
|
|
8231
|
+
--reason "<why>" Reason recorded as a card comment (required)
|
|
8232
|
+
card review <id> Move a running card to review
|
|
8233
|
+
card complete <id> Complete a card in review
|
|
8234
|
+
--outcome <o> success | failure | partial (required)
|
|
8235
|
+
card comment <id> Add a comment to a card
|
|
8236
|
+
--body <text> Comment body (required)
|
|
8237
|
+
--author <name> Comment author (default: cli)
|
|
7830
8238
|
current <sub> Show compact current state for agent injection
|
|
7831
8239
|
current show Active task + recent session events (default)
|
|
7832
8240
|
--json Output as JSON
|
|
@@ -8051,6 +8459,10 @@ Examples:
|
|
|
8051
8459
|
hippo session resume
|
|
8052
8460
|
hippo snapshot save --task "Ship feature" --summary "Tests are green" --next-step "Open the PR" --session sess_123
|
|
8053
8461
|
hippo handoff create --summary "PR is open, tests green" --next "Merge after review" --session sess_123 --artifact src/foo.ts
|
|
8462
|
+
hippo handoff create --summary s --constraint a --constraint b --outcome partial --target-runtime codex --card-id c1 --tests pass
|
|
8463
|
+
hippo card create --title "Add cards table" --repo hippo --depends-on card_abc
|
|
8464
|
+
hippo card claim card_abc --runtime codex
|
|
8465
|
+
hippo card complete card_abc --outcome success
|
|
8054
8466
|
hippo embed --status
|
|
8055
8467
|
hippo watch "npm run build"
|
|
8056
8468
|
hippo learn --git --days 30
|
|
@@ -8438,6 +8850,9 @@ async function main() {
|
|
|
8438
8850
|
case 'handoff':
|
|
8439
8851
|
cmdHandoff(hippoRoot, args, flags);
|
|
8440
8852
|
break;
|
|
8853
|
+
case 'card':
|
|
8854
|
+
cmdCard(hippoRoot, args, flags);
|
|
8855
|
+
break;
|
|
8441
8856
|
case 'predict':
|
|
8442
8857
|
cmdPredict(hippoRoot, args, flags);
|
|
8443
8858
|
break;
|