hippo-memory 1.39.0 → 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 CHANGED
@@ -619,6 +619,14 @@ 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 |
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 |
629
+ | `hippo card comment <id> --body "..."` | Add a comment to a card |
622
630
  | `hippo wm push --scope <s> --content "..."` | Push to working memory |
623
631
  | `hippo wm read --scope <s>` | Read working memory entries |
624
632
  | `hippo wm clear --scope <s>` | Clear working memory |
package/dist/card.d.ts ADDED
@@ -0,0 +1,45 @@
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 reserved for W2b's lease and heartbeat, W2a never writes them and they stay null. */
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
+ //# 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,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"}
package/dist/card.js ADDED
@@ -0,0 +1,16 @@
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'],
11
+ blocked: ['running'],
12
+ review: ['done', 'shelved'],
13
+ done: [],
14
+ shelved: [],
15
+ };
16
+ //# 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,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"}
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|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>"
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;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"}
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,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, 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';
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,281 @@ 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.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
+ }
4027
4304
  // ---------------------------------------------------------------------------
4028
4305
  // E2 prediction first-class object (v0.31)
4029
4306
  // docs/plans/2026-05-26-e2-prediction-object.md
@@ -7936,6 +8213,28 @@ Commands:
7936
8213
  --session <id> Filter by session
7937
8214
  --json Output as JSON
7938
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)
7939
8238
  current <sub> Show compact current state for agent injection
7940
8239
  current show Active task + recent session events (default)
7941
8240
  --json Output as JSON
@@ -8161,6 +8460,9 @@ Examples:
8161
8460
  hippo snapshot save --task "Ship feature" --summary "Tests are green" --next-step "Open the PR" --session sess_123
8162
8461
  hippo handoff create --summary "PR is open, tests green" --next "Merge after review" --session sess_123 --artifact src/foo.ts
8163
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
8164
8466
  hippo embed --status
8165
8467
  hippo watch "npm run build"
8166
8468
  hippo learn --git --days 30
@@ -8548,6 +8850,9 @@ async function main() {
8548
8850
  case 'handoff':
8549
8851
  cmdHandoff(hippoRoot, args, flags);
8550
8852
  break;
8853
+ case 'card':
8854
+ cmdCard(hippoRoot, args, flags);
8855
+ break;
8551
8856
  case 'predict':
8552
8857
  cmdPredict(hippoRoot, args, flags);
8553
8858
  break;