hippo-memory 1.40.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.
package/README.md CHANGED
@@ -622,10 +622,12 @@ hippo watch "npm run build"
622
622
  | `hippo card create --title "..."` | Create a work-queue card (`--repo`, `--contract`, `--budget`, repeatable `--depends-on <id>`) |
623
623
  | `hippo card show <id>` | Show a card, its deps, runs, comments and latest handoff |
624
624
  | `hippo card list [--status <status>]` | List cards, newest-updated first |
625
- | `hippo card claim <id> --runtime <name>` | Claim a ready or blocked card |
626
- | `hippo card block <id> --reason "<why>"` | Block a running card; the reason is recorded as a comment |
627
- | `hippo card review <id>` | Move a running card to review |
628
- | `hippo card complete <id> --outcome <success\|failure\|partial>` | Complete a card in review: `success` marks it done and promotes children whose parents are all done; `failure` or `partial` shelves it |
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 |
629
631
  | `hippo card comment <id> --body "..."` | Add a comment to a card |
630
632
  | `hippo wm push --scope <s> --content "..."` | Push to working memory |
631
633
  | `hippo wm read --scope <s>` | Read working memory entries |
@@ -635,6 +637,8 @@ hippo watch "npm run build"
635
637
  | `hippo dashboard --port <n>` | Use custom port |
636
638
  | `hippo mcp` | Start MCP server (stdio transport) |
637
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
+
638
642
  ---
639
643
 
640
644
  ## Framework Integrations
package/dist/card.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export type CardStatus = 'backlog' | 'ready' | 'running' | 'blocked' | 'review' | 'done' | 'shelved';
3
3
  /** Narrows a raw string, such as a CLI flag value, to a CardStatus. */
4
4
  export declare function isCardStatus(v: string): v is CardStatus;
5
- /** A card row; leaseUntil and heartbeatAt are reserved for W2b's lease and heartbeat, W2a never writes them and they stay null. */
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
6
  export interface Card {
7
7
  id: string;
8
8
  title: string;
@@ -42,4 +42,6 @@ export type CardTransitions = {
42
42
  };
43
43
  /** transitionCard checks every status write against this map. */
44
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;
45
47
  //# sourceMappingURL=card.d.ts.map
@@ -1 +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,mIAAmI;AACnI,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"}
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 CHANGED
@@ -7,10 +7,13 @@ export function isCardStatus(v) {
7
7
  export const CARD_TRANSITIONS = {
8
8
  backlog: ['ready'],
9
9
  ready: ['running'],
10
- running: ['blocked', 'review'],
10
+ running: ['blocked', 'review', 'ready'],
11
11
  blocked: ['running'],
12
12
  review: ['done', 'shelved'],
13
13
  done: [],
14
14
  shelved: [],
15
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;
16
19
  //# sourceMappingURL=card.js.map
package/dist/card.js.map CHANGED
@@ -1 +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,CAAC;IAC9B,OAAO,EAAE,CAAC,SAAS,CAAC;IACpB,MAAM,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,IAAI,EAAE,EAAE;IACR,OAAO,EAAE,EAAE;CACZ,CAAC"}
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,7 +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
+ * hippo card <create|show|list|claim|heartbeat|block|review|complete|reclaim|comment>
17
17
  * hippo current <show>
18
18
  * hippo forget <id> [--archive --reason "<why>"]
19
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AA2BH,OAAO,EAWL,WAAW,EAEZ,MAAM,aAAa,CAAC;AA2HrB,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;AA87GD,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,7 +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
+ * hippo card <create|show|list|claim|heartbeat|block|review|complete|reclaim|comment>
17
17
  * hippo current <show>
18
18
  * hippo forget <id> [--archive --reason "<why>"]
19
19
  * hippo reject <id>|--value "<text>" --reason "<why>"
@@ -38,7 +38,7 @@ import { execFileSync, execSync, spawn } from 'child_process';
38
38
  import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, isCodexWrapperInstalled, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
39
39
  import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
40
40
  import { detectSecret } from './secret-detect.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, blockCard, reviewCard, completeCard, addCardComment, loadLatestHandoffForCard, } 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';
42
42
  import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
43
43
  import { RejectedValueError } from './rejection.js';
44
44
  import { isHandoffOutcome, formatHandoffEvidenceLine } from './handoff.js';
@@ -4034,6 +4034,10 @@ function printCard(hippoRoot, tenantId, card) {
4034
4034
  console.log(`- Status: ${card.status}`);
4035
4035
  if (card.assigneeRuntime)
4036
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}`);
4037
4041
  if (card.repo)
4038
4042
  console.log(`- Repo: ${card.repo}`);
4039
4043
  if (card.contract)
@@ -4050,7 +4054,7 @@ function printCard(hippoRoot, tenantId, card) {
4050
4054
  if (runs.length > 0) {
4051
4055
  console.log('\n### Runs');
4052
4056
  for (const run of runs) {
4053
- console.log(`- ${run.runtime} started ${run.started}${run.ended ? ` ended ${run.ended} (${run.outcome})` : ' (open)'}`);
4057
+ console.log(`- run ${run.id}: ${run.runtime} started ${run.started}${run.ended ? ` ended ${run.ended} (${run.outcome})` : ' (open)'}`);
4054
4058
  }
4055
4059
  }
4056
4060
  const comments = loadCardComments(hippoRoot, tenantId, card.id);
@@ -4080,14 +4084,34 @@ function cardStringFlag(flags, key) {
4080
4084
  }
4081
4085
  return v.trim();
4082
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
+ }
4083
4105
  const CARD_SUBCOMMAND_FLAGS = {
4084
4106
  create: ['title', 'repo', 'contract', 'budget', 'depends-on'],
4085
4107
  show: ['json'],
4086
4108
  list: ['status', 'json'],
4087
4109
  claim: ['runtime', 'session'],
4088
- block: ['reason'],
4089
- review: new Array(),
4090
- complete: ['outcome'],
4110
+ heartbeat: ['run'],
4111
+ block: ['reason', 'run'],
4112
+ review: ['run'],
4113
+ complete: ['outcome', 'run'],
4114
+ reclaim: new Array(),
4091
4115
  comment: ['body', 'author'],
4092
4116
  };
4093
4117
  function cmdCard(hippoRoot, args, flags) {
@@ -4205,7 +4229,29 @@ function cmdCard(hippoRoot, args, flags) {
4205
4229
  console.error(`Could not claim card ${id} (not ready/blocked, or already claimed).`);
4206
4230
  process.exit(1);
4207
4231
  }
4208
- console.log(`Claimed card ${card.id} for ${runtime}`);
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}`);
4209
4255
  return;
4210
4256
  }
4211
4257
  if (subcommand === 'block') {
@@ -4215,16 +4261,18 @@ function cmdCard(hippoRoot, args, flags) {
4215
4261
  console.error(CARD_BLOCK_REASON_REQUIRED);
4216
4262
  process.exit(1);
4217
4263
  }
4264
+ const runId = cardRunFlag(flags);
4218
4265
  let card;
4219
4266
  try {
4220
- card = blockCard(hippoRoot, tenantId, id, reason);
4267
+ card = blockCard(hippoRoot, tenantId, id, reason, runId);
4221
4268
  }
4222
4269
  catch (error) {
4223
4270
  console.error(error instanceof Error ? error.message : String(error));
4224
4271
  process.exit(1);
4225
4272
  }
4226
4273
  if (!card) {
4227
- console.error(`Could not block card ${id} (not running).`);
4274
+ const why = runId === undefined ? 'not running' : cardRefusal(hippoRoot, tenantId, id);
4275
+ console.error(`Could not block card ${id} (${why}).`);
4228
4276
  process.exit(1);
4229
4277
  }
4230
4278
  console.log(`Blocked card ${card.id}`);
@@ -4233,19 +4281,21 @@ function cmdCard(hippoRoot, args, flags) {
4233
4281
  if (subcommand === 'review') {
4234
4282
  const id = args[1];
4235
4283
  if (!id) {
4236
- console.error('Usage: hippo card review <id>');
4284
+ console.error('Usage: hippo card review <id> [--run <n>]');
4237
4285
  process.exit(1);
4238
4286
  }
4287
+ const runId = cardRunFlag(flags);
4239
4288
  let card;
4240
4289
  try {
4241
- card = reviewCard(hippoRoot, tenantId, id);
4290
+ card = reviewCard(hippoRoot, tenantId, id, runId);
4242
4291
  }
4243
4292
  catch (error) {
4244
4293
  console.error(error instanceof Error ? error.message : String(error));
4245
4294
  process.exit(1);
4246
4295
  }
4247
4296
  if (!card) {
4248
- console.error(`Could not move card ${id} to review (not running).`);
4297
+ const why = runId === undefined ? 'not running' : cardRefusal(hippoRoot, tenantId, id);
4298
+ console.error(`Could not move card ${id} to review (${why}).`);
4249
4299
  process.exit(1);
4250
4300
  }
4251
4301
  console.log(`Card ${card.id} moved to review`);
@@ -4255,19 +4305,21 @@ function cmdCard(hippoRoot, args, flags) {
4255
4305
  const id = args[1];
4256
4306
  const outcomeRaw = flags['outcome'];
4257
4307
  if (!id || !isHandoffOutcome(outcomeRaw)) {
4258
- console.error('Usage: hippo card complete <id> --outcome <success|failure|partial>');
4308
+ console.error('Usage: hippo card complete <id> --outcome <success|failure|partial> [--run <n>]');
4259
4309
  process.exit(1);
4260
4310
  }
4311
+ const runId = cardRunFlag(flags);
4261
4312
  let result;
4262
4313
  try {
4263
- result = completeCard(hippoRoot, tenantId, id, outcomeRaw);
4314
+ result = completeCard(hippoRoot, tenantId, id, outcomeRaw, runId);
4264
4315
  }
4265
4316
  catch (error) {
4266
4317
  console.error(error instanceof Error ? error.message : String(error));
4267
4318
  process.exit(1);
4268
4319
  }
4269
4320
  if (!result) {
4270
- console.error(`Could not complete card ${id} (not in review).`);
4321
+ const why = runId === undefined ? 'not in review' : cardRefusal(hippoRoot, tenantId, id);
4322
+ console.error(`Could not complete card ${id} (${why}).`);
4271
4323
  process.exit(1);
4272
4324
  }
4273
4325
  console.log(`Completed card ${result.card.id} (status: ${result.card.status})`);
@@ -4276,13 +4328,28 @@ function cmdCard(hippoRoot, args, flags) {
4276
4328
  }
4277
4329
  return;
4278
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
+ }
4279
4346
  if (subcommand === 'comment') {
4280
4347
  const id = args[1];
4281
4348
  if (!id) {
4282
4349
  console.error('Usage: hippo card comment <id> --body "..." [--author <name>]');
4283
4350
  process.exit(1);
4284
4351
  }
4285
- // Only show and comment look the card up directly; claim/block/review/complete throw from the store instead.
4352
+ // Only show and comment look the card up directly; claim/heartbeat/block/review/complete throw from the store instead.
4286
4353
  const card = loadCard(hippoRoot, tenantId, id);
4287
4354
  if (!card) {
4288
4355
  console.error(`No card found with id ${id}.`);
@@ -4298,7 +4365,7 @@ function cmdCard(hippoRoot, args, flags) {
4298
4365
  console.log(`Added comment ${comment.id} to card ${id}`);
4299
4366
  return;
4300
4367
  }
4301
- console.error('Usage: hippo card <create|show|list|claim|block|review|complete|comment>');
4368
+ console.error('Usage: hippo card <create|show|list|claim|heartbeat|block|review|complete|reclaim|comment>');
4302
4369
  process.exit(1);
4303
4370
  }
4304
4371
  // ---------------------------------------------------------------------------
@@ -8224,14 +8291,20 @@ Commands:
8224
8291
  --json Output as JSON
8225
8292
  card list List cards, newest-updated first
8226
8293
  --status <status> Filter by status
8227
- card claim <id> Claim a ready or blocked card
8294
+ card claim <id> Claim a ready or blocked card; prints its run id and lease
8228
8295
  --runtime <name> Claiming runtime (required)
8229
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)
8230
8299
  card block <id> Block a running card
8231
8300
  --reason "<why>" Reason recorded as a card comment (required)
8301
+ --run <n> Refuse unless <n> is the card's live run
8232
8302
  card review <id> Move a running card to review
8303
+ --run <n> Refuse unless <n> is the card's live run
8233
8304
  card complete <id> Complete a card in review
8234
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
8235
8308
  card comment <id> Add a comment to a card
8236
8309
  --body <text> Comment body (required)
8237
8310
  --author <name> Comment author (default: cli)
@@ -8463,6 +8536,8 @@ Examples:
8463
8536
  hippo card create --title "Add cards table" --repo hippo --depends-on card_abc
8464
8537
  hippo card claim card_abc --runtime codex
8465
8538
  hippo card complete card_abc --outcome success
8539
+ hippo card heartbeat card_abc --run 7
8540
+ hippo card reclaim
8466
8541
  hippo embed --status
8467
8542
  hippo watch "npm run build"
8468
8543
  hippo learn --git --days 30