hippo-memory 1.39.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +12 -0
  2. package/dist/card.d.ts +47 -0
  3. package/dist/card.d.ts.map +1 -0
  4. package/dist/card.js +19 -0
  5. package/dist/card.js.map +1 -0
  6. package/dist/cli.d.ts +1 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +383 -3
  9. package/dist/cli.js.map +1 -1
  10. package/dist/connectors/slack/backfill.js +1 -1
  11. package/dist/db.d.ts.map +1 -1
  12. package/dist/db.js +123 -1
  13. package/dist/db.js.map +1 -1
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/src/card.js +19 -0
  19. package/dist/src/card.js.map +1 -0
  20. package/dist/src/cli.js +383 -3
  21. package/dist/src/cli.js.map +1 -1
  22. package/dist/src/connectors/slack/backfill.js +1 -1
  23. package/dist/src/db.js +123 -1
  24. package/dist/src/db.js.map +1 -1
  25. package/dist/src/index.js +3 -1
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/store.js +521 -1
  28. package/dist/src/store.js.map +1 -1
  29. package/dist/src/version.js +2 -15
  30. package/dist/src/version.js.map +1 -1
  31. package/dist/store.d.ts +50 -0
  32. package/dist/store.d.ts.map +1 -1
  33. package/dist/store.js +521 -1
  34. package/dist/store.js.map +1 -1
  35. package/dist/version.d.ts +2 -15
  36. package/dist/version.d.ts.map +1 -1
  37. package/dist/version.js +2 -15
  38. package/dist/version.js.map +1 -1
  39. package/dist-ui/index.html +12 -12
  40. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  41. package/extensions/openclaw-plugin/package.json +1 -1
  42. package/openclaw.plugin.json +1 -1
  43. package/package.json +1 -1
  44. package/dist/benchmarks/e1.3/scenarios.json +0 -2587
package/README.md CHANGED
@@ -619,6 +619,16 @@ hippo watch "npm run build"
619
619
  | `hippo session latest` | Show latest task snapshot + events |
620
620
  | `hippo session resume` | Re-inject latest handoff as context |
621
621
  | `hippo current show` | Compact current state (task + session events) |
622
+ | `hippo card create --title "..."` | Create a work-queue card (`--repo`, `--contract`, `--budget`, repeatable `--depends-on <id>`) |
623
+ | `hippo card show <id>` | Show a card, its deps, runs, comments and latest handoff |
624
+ | `hippo card list [--status <status>]` | List cards, newest-updated first |
625
+ | `hippo card claim <id> --runtime <name>` | Claim a ready or blocked card; prints its run id and the time its 4-hour lease expires |
626
+ | `hippo card heartbeat <id> --run <n>` | Extend a claimed card's lease |
627
+ | `hippo card block <id> --reason "<why>" [--run <n>]` | Block a running card; the reason is recorded as a comment |
628
+ | `hippo card review <id> [--run <n>]` | Move a running card to review |
629
+ | `hippo card complete <id> --outcome <success\|failure\|partial> [--run <n>]` | Complete a card in review: `success` marks it done and promotes children whose parents are all done; `failure` or `partial` shelves it |
630
+ | `hippo card reclaim` | Sweep every card whose lease has passed and return it to ready |
631
+ | `hippo card comment <id> --body "..."` | Add a comment to a card |
622
632
  | `hippo wm push --scope <s> --content "..."` | Push to working memory |
623
633
  | `hippo wm read --scope <s>` | Read working memory entries |
624
634
  | `hippo wm clear --scope <s>` | Clear working memory |
@@ -627,6 +637,8 @@ hippo watch "npm run build"
627
637
  | `hippo dashboard --port <n>` | Use custom port |
628
638
  | `hippo mcp` | Start MCP server (stdio transport) |
629
639
 
640
+ On `heartbeat`, `block`, `review` and `complete`, a given `--run` is checked against the card's live run and the command is refused, unchanged, if the two do not match.
641
+
630
642
  ---
631
643
 
632
644
  ## Framework Integrations
package/dist/card.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ /** A work-queue card's lifecycle state; CARD_TRANSITIONS lists the legal moves between them. */
2
+ export type CardStatus = 'backlog' | 'ready' | 'running' | 'blocked' | 'review' | 'done' | 'shelved';
3
+ /** Narrows a raw string, such as a CLI flag value, to a CardStatus. */
4
+ export declare function isCardStatus(v: string): v is CardStatus;
5
+ /** A card row; leaseUntil and heartbeatAt are null unless the card is running, and a card claimed under 1.40.0 runs with both null until a heartbeat sets them. */
6
+ export interface Card {
7
+ id: string;
8
+ title: string;
9
+ status: CardStatus;
10
+ assigneeRuntime: string | null;
11
+ repo: string | null;
12
+ contract: string | null;
13
+ budget: number | null;
14
+ leaseUntil: string | null;
15
+ heartbeatAt: string | null;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ tenantId: string;
19
+ scope: string | null;
20
+ }
21
+ /** One claim-to-close attempt at a card by a runtime. */
22
+ export interface CardRun {
23
+ id: number;
24
+ card: string;
25
+ runtime: string;
26
+ sessionId: string | null;
27
+ started: string;
28
+ ended: string | null;
29
+ outcome: string | null;
30
+ }
31
+ /** A comment left on a card. */
32
+ export interface CardComment {
33
+ id: number;
34
+ cardId: string;
35
+ author: string;
36
+ body: string;
37
+ createdAt: string;
38
+ }
39
+ /** The legal next-statuses for each CardStatus. */
40
+ export type CardTransitions = {
41
+ readonly [S in CardStatus]: readonly CardStatus[];
42
+ };
43
+ /** transitionCard checks every status write against this map. */
44
+ export declare const CARD_TRANSITIONS: CardTransitions;
45
+ /** How long a claim or a heartbeat keeps a running card's lease before reclaimExpiredCards may return it to ready. */
46
+ export declare const CARD_LEASE_MS: number;
47
+ //# sourceMappingURL=card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../src/card.ts"],"names":[],"mappings":"AACA,gGAAgG;AAChG,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAIrG,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,UAAU,CAEvD;AAED,mKAAmK;AACnK,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,gCAAgC;AAChC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,mDAAmD;AACnD,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,UAAU,GAAG,SAAS,UAAU,EAAE;CAAE,CAAC;AAEpF,iEAAiE;AACjE,eAAO,MAAM,gBAAgB,EAAE,eAQ9B,CAAC;AAGF,sHAAsH;AACtH,eAAO,MAAM,aAAa,QAAqB,CAAC"}
package/dist/card.js ADDED
@@ -0,0 +1,19 @@
1
+ const CARD_STATUSES = ['backlog', 'ready', 'running', 'blocked', 'review', 'done', 'shelved'];
2
+ /** Narrows a raw string, such as a CLI flag value, to a CardStatus. */
3
+ export function isCardStatus(v) {
4
+ return CARD_STATUSES.some((s) => s === v);
5
+ }
6
+ /** transitionCard checks every status write against this map. */
7
+ export const CARD_TRANSITIONS = {
8
+ backlog: ['ready'],
9
+ ready: ['running'],
10
+ running: ['blocked', 'review', 'ready'],
11
+ blocked: ['running'],
12
+ review: ['done', 'shelved'],
13
+ done: [],
14
+ shelved: [],
15
+ };
16
+ // SHORTCUT: one fixed lease for every claim; add a per-claim lease when the W3 dispatcher heartbeats on a timer.
17
+ /** How long a claim or a heartbeat keeps a running card's lease before reclaimExpiredCards may return it to ready. */
18
+ export const CARD_LEASE_MS = 4 * 60 * 60 * 1000;
19
+ //# sourceMappingURL=card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card.js","sourceRoot":"","sources":["../src/card.ts"],"names":[],"mappings":"AAIA,MAAM,aAAa,GAA0B,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;AAErH,uEAAuE;AACvE,MAAM,UAAU,YAAY,CAAC,CAAS;IACpC,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5C,CAAC;AA0CD,iEAAiE;AACjE,MAAM,CAAC,MAAM,gBAAgB,GAAoB;IAC/C,OAAO,EAAE,CAAC,OAAO,CAAC;IAClB,KAAK,EAAE,CAAC,SAAS,CAAC;IAClB,OAAO,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC;IACvC,OAAO,EAAE,CAAC,SAAS,CAAC;IACpB,MAAM,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,IAAI,EAAE,EAAE;IACR,OAAO,EAAE,EAAE;CACZ,CAAC;AAEF,iHAAiH;AACjH,sHAAsH;AACtH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC"}
package/dist/cli.d.ts 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|heartbeat|block|review|complete|reclaim|comment>
16
17
  * hippo current <show>
17
18
  * hippo forget <id> [--archive --reason "<why>"]
18
19
  * hippo reject <id>|--value "<text>" --reason "<why>"
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA2BH,OAAO,EAWL,WAAW,EAEZ,MAAM,aAAa,CAAC;AA8GrB,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AA4ChC,kFAAkF;AAClF,wBAAgB,8BAA8B,IAAI,IAAI,CAErD;AAsJD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC,CAAA;CAAE,CAwCjI;AAqxED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,MAAqB,GAAG,MAAM,CAyF3F;AAyGD;;;GAGG;AACH,oHAAoH;AACpH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAuD/D;AA+pGD,oHAAoH;AACpH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,EACtF,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,MAAkB,GAC1B,IAAI,CA8BN"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AA2BH,OAAO,EAWL,WAAW,EAEZ,MAAM,aAAa,CAAC;AA6HrB,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AA4ChC,kFAAkF;AAClF,wBAAgB,8BAA8B,IAAI,IAAI,CAErD;AAsJD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC,CAAA;CAAE,CAwCjI;AAqxED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,MAAqB,GAAG,MAAM,CAyF3F;AAyGD;;;GAGG;AACH,oHAAoH;AACpH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAuD/D;AAigHD,oHAAoH;AACpH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,EACtF,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,MAAkB,GAC1B,IAAI,CA8BN"}
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|heartbeat|block|review|complete|reclaim|comment>
16
17
  * hippo current <show>
17
18
  * hippo forget <id> [--archive --reason "<why>"]
18
19
  * hippo reject <id>|--value "<text>" --reason "<why>"
@@ -37,10 +38,11 @@ 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, stampHandoffOutcome, writeSessionEndHandoff, } from './store.js';
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, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, loadLatestHandoffForCard, } from './store.js';
41
42
  import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
42
43
  import { RejectedValueError } from './rejection.js';
43
44
  import { isHandoffOutcome, formatHandoffEvidenceLine } from './handoff.js';
45
+ import { isCardStatus } from './card.js';
44
46
  import { passesScopeFilterForRecall } from './recall-scope.js';
45
47
  import { markRetrieved, hybridSearch, physicsSearch, explainMatch, textOverlap, tokenize as tokenizeQuery } from './search.js';
46
48
  import { compareEntryIdentity } from './compare.js';
@@ -253,8 +255,8 @@ export function parseArgs(argv) {
253
255
  i++;
254
256
  }
255
257
  else {
256
- // Check if it's a repeatable flag (tag, artifact, link, step, constraint)
257
- if (key === 'tag' || key === 'artifact' || key === 'link' || key === 'step' || key === 'constraint') {
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') {
258
260
  if (Array.isArray(flags[key])) {
259
261
  flags[key].push(next);
260
262
  }
@@ -4024,6 +4026,348 @@ function cmdHandoff(hippoRoot, args, flags) {
4024
4026
  console.error('Usage: hippo handoff <create|latest|show>');
4025
4027
  process.exit(1);
4026
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.leaseUntil)
4038
+ console.log(`- Lease until: ${card.leaseUntil}`);
4039
+ if (card.heartbeatAt)
4040
+ console.log(`- Heartbeat: ${card.heartbeatAt}`);
4041
+ if (card.repo)
4042
+ console.log(`- Repo: ${card.repo}`);
4043
+ if (card.contract)
4044
+ console.log(`- Contract: ${card.contract}`);
4045
+ if (card.budget !== null)
4046
+ console.log(`- Budget: ${card.budget}`);
4047
+ console.log(`- Updated: ${card.updatedAt}`);
4048
+ const deps = loadCardDeps(hippoRoot, tenantId, card.id);
4049
+ if (deps.parents.length > 0)
4050
+ console.log(`- Parents: ${deps.parents.join(', ')}`);
4051
+ if (deps.children.length > 0)
4052
+ console.log(`- Children: ${deps.children.join(', ')}`);
4053
+ const runs = loadCardRuns(hippoRoot, tenantId, card.id);
4054
+ if (runs.length > 0) {
4055
+ console.log('\n### Runs');
4056
+ for (const run of runs) {
4057
+ console.log(`- run ${run.id}: ${run.runtime} started ${run.started}${run.ended ? ` ended ${run.ended} (${run.outcome})` : ' (open)'}`);
4058
+ }
4059
+ }
4060
+ const comments = loadCardComments(hippoRoot, tenantId, card.id);
4061
+ if (comments.length > 0) {
4062
+ console.log('\n### Comments');
4063
+ for (const comment of comments) {
4064
+ console.log(`- [${comment.createdAt}] ${comment.author}: ${comment.body}`);
4065
+ }
4066
+ }
4067
+ const handoff = loadLatestHandoffForCard(hippoRoot, tenantId, card.id);
4068
+ if (handoff) {
4069
+ console.log('\n### Latest handoff');
4070
+ console.log(`- Session: ${handoff.sessionId}, updated ${handoff.updatedAt}`);
4071
+ console.log(handoff.summary);
4072
+ }
4073
+ console.log('');
4074
+ }
4075
+ // parseArgs turns a value-less flag into `true`; refuse rather than silently
4076
+ // stringifying it (String(true) === 'true'), mirroring cmdHandoff's guard.
4077
+ function cardStringFlag(flags, key) {
4078
+ const v = flags[key];
4079
+ if (v === undefined)
4080
+ return undefined;
4081
+ if (v === true || v === false || Array.isArray(v)) {
4082
+ console.error(`--${key} requires a value`);
4083
+ process.exit(1);
4084
+ }
4085
+ return v.trim();
4086
+ }
4087
+ // A too-large --run would silently round to a different id (mirrors parsePositiveIncidentId).
4088
+ function cardRunFlag(flags) {
4089
+ const raw = cardStringFlag(flags, 'run');
4090
+ if (raw === undefined)
4091
+ return undefined;
4092
+ const n = Number(raw);
4093
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n <= 0) {
4094
+ console.error(`Invalid --run: "${raw}" (expected a positive integer).`);
4095
+ process.exit(1);
4096
+ }
4097
+ return n;
4098
+ }
4099
+ // Reads after the store already refused, so this only explains the refusal, never changes it.
4100
+ function cardRefusal(hippoRoot, tenantId, id) {
4101
+ const card = loadCard(hippoRoot, tenantId, id);
4102
+ const liveRun = loadCardRuns(hippoRoot, tenantId, id).find((r) => !r.ended);
4103
+ return `status ${card?.status ?? 'unknown'}, live run ${liveRun?.id ?? 'none'}`;
4104
+ }
4105
+ const CARD_SUBCOMMAND_FLAGS = {
4106
+ create: ['title', 'repo', 'contract', 'budget', 'depends-on'],
4107
+ show: ['json'],
4108
+ list: ['status', 'json'],
4109
+ claim: ['runtime', 'session'],
4110
+ heartbeat: ['run'],
4111
+ block: ['reason', 'run'],
4112
+ review: ['run'],
4113
+ complete: ['outcome', 'run'],
4114
+ reclaim: new Array(),
4115
+ comment: ['body', 'author'],
4116
+ };
4117
+ function cmdCard(hippoRoot, args, flags) {
4118
+ requireInit(hippoRoot);
4119
+ const tenantId = resolveTenantId({});
4120
+ const subcommand = args[0] ?? '';
4121
+ if (Object.hasOwn(CARD_SUBCOMMAND_FLAGS, subcommand)) {
4122
+ // SAFETY: hasOwn, unlike `in`, skips inherited keys such as constructor, so subcommand is a real key.
4123
+ const allowedFlags = CARD_SUBCOMMAND_FLAGS[subcommand];
4124
+ for (const key of Object.keys(flags)) {
4125
+ if (!allowedFlags.includes(key)) {
4126
+ // --flag=value never splits on '=' (see parseArgs), so it lands here as one long key.
4127
+ const hint = key.includes('=') ? ` Use --${key.slice(0, key.indexOf('='))} <value>, not --${key}.` : '';
4128
+ const valid = allowedFlags.length > 0 ? allowedFlags.map((f) => `--${f}`).join(', ') : '(none)';
4129
+ console.error(`Unknown flag --${key} for hippo card ${subcommand}.${hint} Valid flags: ${valid}`);
4130
+ process.exit(1);
4131
+ }
4132
+ }
4133
+ }
4134
+ if (subcommand === 'create') {
4135
+ const title = cardStringFlag(flags, 'title') ?? '';
4136
+ if (!title) {
4137
+ console.error('Usage: hippo card create --title "..." [--repo <name>] [--contract <text>] [--budget <n>] [--depends-on <id>...]');
4138
+ process.exit(1);
4139
+ }
4140
+ const repo = cardStringFlag(flags, 'repo') || undefined;
4141
+ const contract = cardStringFlag(flags, 'contract') || undefined;
4142
+ const budgetRaw = cardStringFlag(flags, 'budget');
4143
+ let budget;
4144
+ if (budgetRaw !== undefined) {
4145
+ if (!/^\d+$/.test(budgetRaw)) {
4146
+ console.error(`Invalid budget: "${budgetRaw}" (expected a positive integer)`);
4147
+ process.exit(1);
4148
+ }
4149
+ budget = Number(budgetRaw);
4150
+ }
4151
+ const dependsOnFlag = flags['depends-on'];
4152
+ if (dependsOnFlag === true) {
4153
+ console.error('--depends-on requires a value');
4154
+ process.exit(1);
4155
+ }
4156
+ const dependsOn = Array.isArray(dependsOnFlag) ? dependsOnFlag : [];
4157
+ let card;
4158
+ try {
4159
+ card = createCard(hippoRoot, tenantId, { title, repo, contract, budget, dependsOn });
4160
+ }
4161
+ catch (error) {
4162
+ console.error(error instanceof Error ? error.message : String(error));
4163
+ process.exit(1);
4164
+ }
4165
+ console.log(`Created card ${card.id} (status: ${card.status})`);
4166
+ return;
4167
+ }
4168
+ if (subcommand === 'show') {
4169
+ const id = args[1];
4170
+ if (!id) {
4171
+ console.error('Usage: hippo card show <id> [--json]');
4172
+ process.exit(1);
4173
+ }
4174
+ const card = loadCard(hippoRoot, tenantId, id);
4175
+ if (!card) {
4176
+ console.error(`No card found with id ${id}.`);
4177
+ process.exit(1);
4178
+ }
4179
+ if (flags['json']) {
4180
+ console.log(JSON.stringify({
4181
+ card,
4182
+ deps: loadCardDeps(hippoRoot, tenantId, id),
4183
+ runs: loadCardRuns(hippoRoot, tenantId, id),
4184
+ comments: loadCardComments(hippoRoot, tenantId, id),
4185
+ handoff: loadLatestHandoffForCard(hippoRoot, tenantId, id),
4186
+ }, null, 2));
4187
+ return;
4188
+ }
4189
+ printCard(hippoRoot, tenantId, card);
4190
+ return;
4191
+ }
4192
+ if (subcommand === 'list') {
4193
+ const status = cardStringFlag(flags, 'status');
4194
+ if (status !== undefined && !isCardStatus(status)) {
4195
+ console.error(`Invalid status: "${status}".`);
4196
+ process.exit(1);
4197
+ }
4198
+ const cards = listCards(hippoRoot, tenantId, { status });
4199
+ if (flags['json']) {
4200
+ console.log(JSON.stringify({ cards }, null, 2));
4201
+ return;
4202
+ }
4203
+ if (cards.length === 0) {
4204
+ console.log('No cards found.');
4205
+ return;
4206
+ }
4207
+ for (const card of cards) {
4208
+ console.log(`${card.id}\t${card.status}\t${card.title}${card.assigneeRuntime ? `\t(${card.assigneeRuntime})` : ''}`);
4209
+ }
4210
+ return;
4211
+ }
4212
+ if (subcommand === 'claim') {
4213
+ const id = args[1];
4214
+ const runtime = cardStringFlag(flags, 'runtime') ?? '';
4215
+ if (!id || !runtime) {
4216
+ console.error('Usage: hippo card claim <id> --runtime <name> [--session <id>]');
4217
+ process.exit(1);
4218
+ }
4219
+ const sessionId = cardStringFlag(flags, 'session') || undefined;
4220
+ let card;
4221
+ try {
4222
+ card = claimCard(hippoRoot, tenantId, id, runtime, sessionId);
4223
+ }
4224
+ catch (error) {
4225
+ console.error(error instanceof Error ? error.message : String(error));
4226
+ process.exit(1);
4227
+ }
4228
+ if (!card) {
4229
+ console.error(`Could not claim card ${id} (not ready/blocked, or already claimed).`);
4230
+ process.exit(1);
4231
+ }
4232
+ console.log(`Claimed card ${card.id} for ${runtime} (run ${card.runId}, lease until ${card.leaseUntil})`);
4233
+ return;
4234
+ }
4235
+ if (subcommand === 'heartbeat') {
4236
+ const id = args[1];
4237
+ const runId = cardRunFlag(flags);
4238
+ if (!id || runId === undefined) {
4239
+ console.error('Usage: hippo card heartbeat <id> --run <n>');
4240
+ process.exit(1);
4241
+ }
4242
+ let card;
4243
+ try {
4244
+ card = heartbeatCard(hippoRoot, tenantId, id, runId);
4245
+ }
4246
+ catch (error) {
4247
+ console.error(error instanceof Error ? error.message : String(error));
4248
+ process.exit(1);
4249
+ }
4250
+ if (!card) {
4251
+ console.error(`Could not heartbeat card ${id} (${cardRefusal(hippoRoot, tenantId, id)}).`);
4252
+ process.exit(1);
4253
+ }
4254
+ console.log(`Heartbeat card ${card.id}: lease until ${card.leaseUntil}`);
4255
+ return;
4256
+ }
4257
+ if (subcommand === 'block') {
4258
+ const id = args[1];
4259
+ const reason = cardStringFlag(flags, 'reason') ?? '';
4260
+ if (!id || !reason) {
4261
+ console.error(CARD_BLOCK_REASON_REQUIRED);
4262
+ process.exit(1);
4263
+ }
4264
+ const runId = cardRunFlag(flags);
4265
+ let card;
4266
+ try {
4267
+ card = blockCard(hippoRoot, tenantId, id, reason, runId);
4268
+ }
4269
+ catch (error) {
4270
+ console.error(error instanceof Error ? error.message : String(error));
4271
+ process.exit(1);
4272
+ }
4273
+ if (!card) {
4274
+ const why = runId === undefined ? 'not running' : cardRefusal(hippoRoot, tenantId, id);
4275
+ console.error(`Could not block card ${id} (${why}).`);
4276
+ process.exit(1);
4277
+ }
4278
+ console.log(`Blocked card ${card.id}`);
4279
+ return;
4280
+ }
4281
+ if (subcommand === 'review') {
4282
+ const id = args[1];
4283
+ if (!id) {
4284
+ console.error('Usage: hippo card review <id> [--run <n>]');
4285
+ process.exit(1);
4286
+ }
4287
+ const runId = cardRunFlag(flags);
4288
+ let card;
4289
+ try {
4290
+ card = reviewCard(hippoRoot, tenantId, id, runId);
4291
+ }
4292
+ catch (error) {
4293
+ console.error(error instanceof Error ? error.message : String(error));
4294
+ process.exit(1);
4295
+ }
4296
+ if (!card) {
4297
+ const why = runId === undefined ? 'not running' : cardRefusal(hippoRoot, tenantId, id);
4298
+ console.error(`Could not move card ${id} to review (${why}).`);
4299
+ process.exit(1);
4300
+ }
4301
+ console.log(`Card ${card.id} moved to review`);
4302
+ return;
4303
+ }
4304
+ if (subcommand === 'complete') {
4305
+ const id = args[1];
4306
+ const outcomeRaw = flags['outcome'];
4307
+ if (!id || !isHandoffOutcome(outcomeRaw)) {
4308
+ console.error('Usage: hippo card complete <id> --outcome <success|failure|partial> [--run <n>]');
4309
+ process.exit(1);
4310
+ }
4311
+ const runId = cardRunFlag(flags);
4312
+ let result;
4313
+ try {
4314
+ result = completeCard(hippoRoot, tenantId, id, outcomeRaw, runId);
4315
+ }
4316
+ catch (error) {
4317
+ console.error(error instanceof Error ? error.message : String(error));
4318
+ process.exit(1);
4319
+ }
4320
+ if (!result) {
4321
+ const why = runId === undefined ? 'not in review' : cardRefusal(hippoRoot, tenantId, id);
4322
+ console.error(`Could not complete card ${id} (${why}).`);
4323
+ process.exit(1);
4324
+ }
4325
+ console.log(`Completed card ${result.card.id} (status: ${result.card.status})`);
4326
+ if (result.promotedChildren.length > 0) {
4327
+ console.log(`Promoted to ready: ${result.promotedChildren.join(', ')}`);
4328
+ }
4329
+ return;
4330
+ }
4331
+ if (subcommand === 'reclaim') {
4332
+ if (args.length > 1) {
4333
+ console.error('Usage: hippo card reclaim (sweeps every expired lease; use hippo card block <id> for one card)');
4334
+ process.exit(1);
4335
+ }
4336
+ const ids = reclaimExpiredCards(hippoRoot, tenantId);
4337
+ if (ids.length === 0) {
4338
+ console.log('No expired leases.');
4339
+ return;
4340
+ }
4341
+ for (const id of ids) {
4342
+ console.log(`Reclaimed card ${id} (now ready)`);
4343
+ }
4344
+ return;
4345
+ }
4346
+ if (subcommand === 'comment') {
4347
+ const id = args[1];
4348
+ if (!id) {
4349
+ console.error('Usage: hippo card comment <id> --body "..." [--author <name>]');
4350
+ process.exit(1);
4351
+ }
4352
+ // Only show and comment look the card up directly; claim/heartbeat/block/review/complete throw from the store instead.
4353
+ const card = loadCard(hippoRoot, tenantId, id);
4354
+ if (!card) {
4355
+ console.error(`No card found with id ${id}.`);
4356
+ process.exit(1);
4357
+ }
4358
+ const body = cardStringFlag(flags, 'body') ?? '';
4359
+ if (!body) {
4360
+ console.error('Usage: hippo card comment <id> --body "..." [--author <name>]');
4361
+ process.exit(1);
4362
+ }
4363
+ const author = cardStringFlag(flags, 'author') || 'cli';
4364
+ const comment = addCardComment(hippoRoot, tenantId, id, author, body);
4365
+ console.log(`Added comment ${comment.id} to card ${id}`);
4366
+ return;
4367
+ }
4368
+ console.error('Usage: hippo card <create|show|list|claim|heartbeat|block|review|complete|reclaim|comment>');
4369
+ process.exit(1);
4370
+ }
4027
4371
  // ---------------------------------------------------------------------------
4028
4372
  // E2 prediction first-class object (v0.31)
4029
4373
  // docs/plans/2026-05-26-e2-prediction-object.md
@@ -7936,6 +8280,34 @@ Commands:
7936
8280
  --session <id> Filter by session
7937
8281
  --json Output as JSON
7938
8282
  handoff show <id> Show a specific handoff by ID
8283
+ card <sub> Manage claimable work-queue cards
8284
+ card create Create a new card
8285
+ --title <text> Card title (required)
8286
+ --repo <name> Associated repo
8287
+ --contract <text> Associated contract
8288
+ --budget <n> Token/step budget
8289
+ --depends-on <id> Parent card id (repeatable)
8290
+ card show <id> Show a card, its deps, runs, comments and latest handoff
8291
+ --json Output as JSON
8292
+ card list List cards, newest-updated first
8293
+ --status <status> Filter by status
8294
+ card claim <id> Claim a ready or blocked card; prints its run id and lease
8295
+ --runtime <name> Claiming runtime (required)
8296
+ --session <id> Session ID
8297
+ card heartbeat <id> Extend a running card's lease
8298
+ --run <n> Your run id, as card claim printed it (required)
8299
+ card block <id> Block a running card
8300
+ --reason "<why>" Reason recorded as a card comment (required)
8301
+ --run <n> Refuse unless <n> is the card's live run
8302
+ card review <id> Move a running card to review
8303
+ --run <n> Refuse unless <n> is the card's live run
8304
+ card complete <id> Complete a card in review
8305
+ --outcome <o> success | failure | partial (required)
8306
+ --run <n> Refuse unless <n> is the card's live run
8307
+ card reclaim Return every running card whose lease has expired to ready
8308
+ card comment <id> Add a comment to a card
8309
+ --body <text> Comment body (required)
8310
+ --author <name> Comment author (default: cli)
7939
8311
  current <sub> Show compact current state for agent injection
7940
8312
  current show Active task + recent session events (default)
7941
8313
  --json Output as JSON
@@ -8161,6 +8533,11 @@ Examples:
8161
8533
  hippo snapshot save --task "Ship feature" --summary "Tests are green" --next-step "Open the PR" --session sess_123
8162
8534
  hippo handoff create --summary "PR is open, tests green" --next "Merge after review" --session sess_123 --artifact src/foo.ts
8163
8535
  hippo handoff create --summary s --constraint a --constraint b --outcome partial --target-runtime codex --card-id c1 --tests pass
8536
+ hippo card create --title "Add cards table" --repo hippo --depends-on card_abc
8537
+ hippo card claim card_abc --runtime codex
8538
+ hippo card complete card_abc --outcome success
8539
+ hippo card heartbeat card_abc --run 7
8540
+ hippo card reclaim
8164
8541
  hippo embed --status
8165
8542
  hippo watch "npm run build"
8166
8543
  hippo learn --git --days 30
@@ -8548,6 +8925,9 @@ async function main() {
8548
8925
  case 'handoff':
8549
8926
  cmdHandoff(hippoRoot, args, flags);
8550
8927
  break;
8928
+ case 'card':
8929
+ cmdCard(hippoRoot, args, flags);
8930
+ break;
8551
8931
  case 'predict':
8552
8932
  cmdPredict(hippoRoot, args, flags);
8553
8933
  break;