@merittdev/horus 0.21.6 → 0.21.7

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.
Binary file
package/dist/index.cjs CHANGED
@@ -3057,7 +3057,7 @@ var init_version = __esm({
3057
3057
  "../../packages/core/src/version.ts"() {
3058
3058
  "use strict";
3059
3059
  init_cjs_shims();
3060
- HORUS_VERSION = true ? "0.21.6" : "dev";
3060
+ HORUS_VERSION = true ? "0.21.7" : "dev";
3061
3061
  PINNED_SOURCE_VERSION = HORUS_VERSION;
3062
3062
  SOURCE_PIN_ENFORCED = HORUS_VERSION !== "dev";
3063
3063
  EXPECTED_INDEX_CAPABILITY = 2;
@@ -77378,6 +77378,35 @@ var CloudClient = class {
77378
77378
  `/v1/projects/${projectId}/memory-items${qs ? `?${qs}` : ""}`
77379
77379
  );
77380
77380
  }
77381
+ // ── Team memory (HOR-464 / M4) ────────────────────────────────────────────
77382
+ /**
77383
+ * One-way PROMOTE local items into the workspace's shared team-memory store. Server-wins &
77384
+ * idempotent (`ON CONFLICT (workspace_id, origin_client_id) DO NOTHING`); it clamps/skips
77385
+ * confirmed-outcome (privacy). The body is a positive allowlist — vectors NEVER cross the wire.
77386
+ */
77387
+ promoteTeamMemory(projectId, body2) {
77388
+ return this.request(
77389
+ "POST",
77390
+ `/v1/projects/${projectId}/team-memory/promote`,
77391
+ body2
77392
+ );
77393
+ }
77394
+ /**
77395
+ * Read the workspace's team-memory items with `seq > since`, ORDER BY seq ASC. The v1 client pulls
77396
+ * `since:'0'` and paginates on `nextCursor` until `hasMore` is false, then reconciles the local
77397
+ * cache. `includeDeleted` surfaces tombstones (deletedAt) so the cache can drop removed rows.
77398
+ */
77399
+ listTeamMemorySince(projectId, q2) {
77400
+ const params = new URLSearchParams();
77401
+ params.set("since", q2.since);
77402
+ if (q2.limit !== void 0) params.set("limit", String(q2.limit));
77403
+ if (q2.includeDeleted) params.set("includeDeleted", "true");
77404
+ if (q2.search) params.set("search", q2.search);
77405
+ return this.request(
77406
+ "GET",
77407
+ `/v1/projects/${projectId}/team-memory?${params.toString()}`
77408
+ );
77409
+ }
77381
77410
  };
77382
77411
 
77383
77412
  // ../../packages/cli/src/lib/cloud/auth-store.ts
@@ -84445,6 +84474,21 @@ var memoryItem = pgTable(
84445
84474
  userId: text("user_id"),
84446
84475
  visibility: text("visibility").notNull().default("private"),
84447
84476
  // private|team
84477
+ // HOR-464 (M4): team-memory cache provenance. `origin` distinguishes a locally-authored row
84478
+ // (`local`) from a disposable read-cache row pulled from the cloud team store (`cloud`). The
84479
+ // cloud-owned copy is server-authoritative — `origin='cloud'` rows are readonly locally (any
84480
+ // local mutation throws `MemoryCacheReadonlyError`; re-promote to change) and are NEVER re-pushed
84481
+ // by `memory sync` (which pushes `origin='local'` only). `cloudId` is the server team-memory id,
84482
+ // `authorName` the promoting teammate (DISPLAY-ONLY — never feeds rank), `pulledAt` the last
84483
+ // refresh time (staleness).
84484
+ origin: text("origin").notNull().default("local"),
84485
+ // local|cloud
84486
+ cloudId: text("cloud_id"),
84487
+ // server team_memory id (provenance/join)
84488
+ authorName: text("author_name"),
84489
+ // promoting teammate's display name (attribution, display-only)
84490
+ pulledAt: timestamp("pulled_at", { withTimezone: true }),
84491
+ // last cache-refresh time
84448
84492
  payload: jsonb("payload"),
84449
84493
  // forward-compat, zero-migration extension
84450
84494
  // Incident-family recall keys, populated at WRITE for incident-derived kinds
@@ -84461,7 +84505,9 @@ var memoryItem = pgTable(
84461
84505
  index("memory_item_scope_idx").on(t2.scope),
84462
84506
  index("memory_item_status_idx").on(t2.status),
84463
84507
  index("memory_item_tenancy_idx").on(t2.orgId, t2.workspaceId, t2.repo, t2.userId),
84464
- index("memory_item_signature_idx").on(t2.signature)
84508
+ index("memory_item_signature_idx").on(t2.signature),
84509
+ // HOR-464: fast repo-scoped split of local-authored vs cloud-cached rows (pull reconcile + sync filter).
84510
+ index("memory_item_origin_idx").on(t2.repo, t2.origin)
84465
84511
  ]
84466
84512
  );
84467
84513
  var memoryLink = pgTable(
@@ -92771,6 +92817,16 @@ WHERE im.investigation_id IS NOT NULL AND im.project IS NULL;`,
92771
92817
  WHERE "project" IS NULL AND incident_input->>'repo' IS NOT NULL;`,
92772
92818
  'CREATE INDEX "investigations_project_created_idx" ON "investigations" ("project","created_at");'
92773
92819
  ]
92820
+ },
92821
+ {
92822
+ "tag": "0012_memory_item_origin",
92823
+ "statements": [
92824
+ `ALTER TABLE "memory_item" ADD COLUMN "origin" text DEFAULT 'local' NOT NULL;`,
92825
+ 'ALTER TABLE "memory_item" ADD COLUMN "cloud_id" text;',
92826
+ 'ALTER TABLE "memory_item" ADD COLUMN "author_name" text;',
92827
+ 'ALTER TABLE "memory_item" ADD COLUMN "pulled_at" timestamp with time zone;',
92828
+ 'CREATE INDEX IF NOT EXISTS "memory_item_origin_idx" ON "memory_item" ("repo","origin");'
92829
+ ]
92774
92830
  }
92775
92831
  ];
92776
92832
 
@@ -96403,6 +96459,16 @@ var MemorySecretError = class extends Error {
96403
96459
  this.kind = kind;
96404
96460
  }
96405
96461
  };
96462
+ var MemoryCacheReadonlyError = class extends Error {
96463
+ id;
96464
+ constructor(id) {
96465
+ super(
96466
+ `refusing to mutate a cloud-owned cached memory item (${id}); the team store owns it \u2014 re-promote to change it`
96467
+ );
96468
+ this.name = "MemoryCacheReadonlyError";
96469
+ this.id = id;
96470
+ }
96471
+ };
96406
96472
  var CLAIM_SECRET_PATTERNS = [
96407
96473
  { label: "private-key", re: /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----/ },
96408
96474
  { label: "jwt", re: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/ },
@@ -96562,6 +96628,7 @@ function createLocalMemoryStore(db) {
96562
96628
  async update(id, patch, opts) {
96563
96629
  const current = await getById(id);
96564
96630
  if (current === null) throw new Error(`memory item not found: ${id}`);
96631
+ if (current.origin === "cloud") throw new MemoryCacheReadonlyError(id);
96565
96632
  const set = {};
96566
96633
  if (patch.claim !== void 0) {
96567
96634
  assertClaimClean(patch.claim);
@@ -96594,6 +96661,7 @@ function createLocalMemoryStore(db) {
96594
96661
  if (repo === "") return [];
96595
96662
  const conds = [eq(memoryItem.repo, repo)];
96596
96663
  if (q2.scope !== void 0) conds.push(eq(memoryItem.scope, q2.scope));
96664
+ if (q2.origin !== void 0) conds.push(eq(memoryItem.origin, q2.origin));
96597
96665
  if (q2.visibility !== void 0) conds.push(eq(memoryItem.visibility, q2.visibility));
96598
96666
  if (q2.orgId !== void 0) conds.push(eq(memoryItem.orgId, q2.orgId));
96599
96667
  if (q2.workspaceId !== void 0) conds.push(eq(memoryItem.workspaceId, q2.workspaceId));
@@ -96614,6 +96682,7 @@ function createLocalMemoryStore(db) {
96614
96682
  async setStatus(id, status, audit) {
96615
96683
  const current = await getById(id);
96616
96684
  if (current === null) throw new Error(`memory item not found: ${id}`);
96685
+ if (current.origin === "cloud") throw new MemoryCacheReadonlyError(id);
96617
96686
  await db.update(memoryItem).set({ status }).where(eq(memoryItem.id, id));
96618
96687
  await appendAudit({
96619
96688
  memoryId: id,
@@ -96627,6 +96696,7 @@ function createLocalMemoryStore(db) {
96627
96696
  async setVisibility(id, v4, audit) {
96628
96697
  const current = await getById(id);
96629
96698
  if (current === null) throw new Error(`memory item not found: ${id}`);
96699
+ if (current.origin === "cloud") throw new MemoryCacheReadonlyError(id);
96630
96700
  await db.update(memoryItem).set({ visibility: v4 }).where(eq(memoryItem.id, id));
96631
96701
  await appendAudit({
96632
96702
  memoryId: id,
@@ -96771,6 +96841,96 @@ function createLocalMemoryStore(db) {
96771
96841
  },
96772
96842
  async history(id) {
96773
96843
  return db.select().from(memoryAudit).where(eq(memoryAudit.memoryId, id)).orderBy(desc(memoryAudit.at), desc(memoryAudit.id));
96844
+ },
96845
+ // ---- HOR-464 / M4: disposable team-memory read-cache + one-way promote ----
96846
+ async upsertCached(item, audit) {
96847
+ assertClaimClean(item.claim);
96848
+ const repo = (item.repo ?? "").trim();
96849
+ if (repo === "") throw new Error("memory_item requires a repo (HOR-46 fail-closed)");
96850
+ const id = (item.id ?? "").trim();
96851
+ if (id === "") throw new Error("upsertCached requires a stable item id (the origin client id)");
96852
+ const status = item.status ?? "fresh";
96853
+ const now = /* @__PURE__ */ new Date();
96854
+ const values2 = {
96855
+ ...item,
96856
+ id,
96857
+ repo,
96858
+ status,
96859
+ origin: "cloud",
96860
+ pulledAt: now,
96861
+ signature: null,
96862
+ tags: null
96863
+ };
96864
+ await db.insert(memoryItem).values(values2).onConflictDoUpdate({
96865
+ target: memoryItem.id,
96866
+ set: {
96867
+ kind: values2.kind,
96868
+ claim: values2.claim,
96869
+ scope: values2.scope,
96870
+ source: values2.source,
96871
+ status,
96872
+ confidence: values2.confidence,
96873
+ visibility: values2.visibility ?? "team",
96874
+ evidence: values2.evidence,
96875
+ cloudId: values2.cloudId ?? null,
96876
+ authorName: values2.authorName ?? null,
96877
+ lastVerifiedAt: values2.lastVerifiedAt ?? null,
96878
+ lastVerifiedHash: values2.lastVerifiedHash ?? null,
96879
+ origin: "cloud",
96880
+ // Re-scope to the repo we're pulling into. The cache is disposable and
96881
+ // project-isolated (HOR-46): a team item's id is the promoter's local id and is
96882
+ // stable across a workspace, so pulling it from a different repo on the same store
96883
+ // must move the cache row to the current repo — otherwise "pull here" silently
96884
+ // leaves it invisible here, stuck in whichever repo first pulled it.
96885
+ repo: values2.repo,
96886
+ pulledAt: now
96887
+ }
96888
+ });
96889
+ const row = await getById(id);
96890
+ if (row === null) throw new Error("memory_item upsertCached returned no row");
96891
+ await appendAudit({
96892
+ memoryId: id,
96893
+ action: "pulled",
96894
+ actor: audit.actor,
96895
+ fromStatus: null,
96896
+ toStatus: status,
96897
+ note: audit.note ?? null,
96898
+ detail: { origin: "cloud", ...values2.cloudId ? { cloudId: values2.cloudId } : {} }
96899
+ });
96900
+ return row;
96901
+ },
96902
+ async reconcileCache(repo, keepIds) {
96903
+ const r = repo.trim();
96904
+ if (r === "") return 0;
96905
+ const conds = keepIds.length > 0 ? and(
96906
+ eq(memoryItem.repo, r),
96907
+ eq(memoryItem.origin, "cloud"),
96908
+ notInArray(memoryItem.id, keepIds)
96909
+ ) : and(eq(memoryItem.repo, r), eq(memoryItem.origin, "cloud"));
96910
+ const deleted = await db.delete(memoryItem).where(conds).returning();
96911
+ return deleted.length;
96912
+ },
96913
+ async markPromoted(id, provenance, audit) {
96914
+ const current = await getById(id);
96915
+ if (current === null) throw new Error(`memory item not found: ${id}`);
96916
+ await db.update(memoryItem).set({
96917
+ visibility: "team",
96918
+ origin: "cloud",
96919
+ cloudId: provenance.cloudId,
96920
+ authorName: provenance.authorName ?? current.authorName ?? null
96921
+ }).where(eq(memoryItem.id, id));
96922
+ await appendAudit({
96923
+ memoryId: id,
96924
+ action: "promote",
96925
+ actor: audit.actor,
96926
+ fromStatus: null,
96927
+ toStatus: null,
96928
+ note: audit.note ?? `promoted to team (cloud id ${provenance.cloudId})`,
96929
+ detail: { origin: "cloud", visibility: "team", cloudId: provenance.cloudId }
96930
+ });
96931
+ const updated = await getById(id);
96932
+ if (updated === null) throw new Error(`memory item vanished during promote: ${id}`);
96933
+ return updated;
96774
96934
  }
96775
96935
  };
96776
96936
  }
@@ -122314,6 +122474,12 @@ function fromWire(r, repo) {
122314
122474
  repo,
122315
122475
  userId: r.createdByUserId ?? null,
122316
122476
  visibility: r.visibility,
122477
+ // This is the write-only private mirror path (dualWrite reads local): a round-tripped record is
122478
+ // the device's OWN authored item, so it carries no team-cache provenance (HOR-464).
122479
+ origin: "local",
122480
+ cloudId: null,
122481
+ authorName: null,
122482
+ pulledAt: null,
122317
122483
  payload: null,
122318
122484
  // Incident-family recall keys are LOCAL-ONLY context (like vectors): they never enter the
122319
122485
  // cloud-sync path, so a cloud-read row carries neither.
@@ -122375,6 +122541,10 @@ function createCloudMemoryStore(client, cfg) {
122375
122541
  repo: item.repo ?? repoFallback,
122376
122542
  userId: item.userId ?? null,
122377
122543
  visibility: input.visibility ?? "private",
122544
+ origin: "local",
122545
+ cloudId: null,
122546
+ authorName: null,
122547
+ pulledAt: null,
122378
122548
  payload: null,
122379
122549
  // Incident-family recall keys stay LOCAL-ONLY — never synced, so the cloud echo carries neither.
122380
122550
  signature: null,
@@ -122409,6 +122579,10 @@ function createCloudMemoryStore(client, cfg) {
122409
122579
  repo: repoFallback,
122410
122580
  userId: null,
122411
122581
  visibility: "private",
122582
+ origin: "local",
122583
+ cloudId: null,
122584
+ authorName: null,
122585
+ pulledAt: null,
122412
122586
  payload: null,
122413
122587
  signature: null,
122414
122588
  tags: null
@@ -122667,6 +122841,10 @@ function recalledToJSON(r) {
122667
122841
  effectiveStatus: r.freshness.status,
122668
122842
  confidence: r.item.confidence,
122669
122843
  visibility: r.item.visibility,
122844
+ // HOR-464: provenance + teammate attribution. `origin='cloud'` marks a pulled team item; the
122845
+ // `authorName` is DISPLAY-ONLY (it is never read into rank/order — the honesty invariant).
122846
+ origin: r.item.origin,
122847
+ authorName: r.item.authorName ?? null,
122670
122848
  evidence: r.item.evidence,
122671
122849
  freshness: {
122672
122850
  label: r.freshness.label,
@@ -122693,6 +122871,10 @@ function renderStoredItems(items) {
122693
122871
  lines.push(
122694
122872
  ` - ${r.item.kind} \xB7 scope: ${r.item.scope} \xB7 ${r.freshness.label}${drift} \xB7 confidence: ${conf}`
122695
122873
  );
122874
+ if (r.item.origin === "cloud") {
122875
+ const who = r.item.authorName ? `teammate ${r.item.authorName}` : "a teammate";
122876
+ lines.push(` - ${import_picocolors36.default.dim(`shared by ${who} (team)`)}`);
122877
+ }
122696
122878
  }
122697
122879
  return lines.join("\n");
122698
122880
  }
@@ -123171,6 +123353,7 @@ async function runMemorySync(opts) {
123171
123353
  const store = createLocalMemoryStore(db);
123172
123354
  items = await store.query({
123173
123355
  repo: project,
123356
+ origin: "local",
123174
123357
  status: SYNCABLE_STATUSES,
123175
123358
  limit: opts.limit ?? 5e3
123176
123359
  });
@@ -123305,6 +123488,215 @@ async function runMemorySync(opts) {
123305
123488
  }
123306
123489
  return failed > 0 ? 1 : 0;
123307
123490
  }
123491
+ function toPromoteInput(item) {
123492
+ return {
123493
+ originClientId: item.id,
123494
+ kind: item.kind,
123495
+ claim: item.claim,
123496
+ scope: item.scope,
123497
+ source: item.source,
123498
+ status: item.status,
123499
+ confidence: item.confidence,
123500
+ contentHash: item.lastVerifiedHash ?? null,
123501
+ provenance: {}
123502
+ };
123503
+ }
123504
+ async function runMemoryPromote(id, opts) {
123505
+ const json2 = opts.json === true;
123506
+ const fail = (msg) => {
123507
+ if (json2) console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
123508
+ else console.error(import_picocolors36.default.red(msg));
123509
+ return 1;
123510
+ };
123511
+ const memId = (id ?? "").trim();
123512
+ if (memId === "") return fail("A memory item id is required.");
123513
+ const root = repoRootOrCwd(opts.cwd);
123514
+ const cfg = readCloudConfig(root);
123515
+ if (!isCloudActive(cfg) || !cfg.project) {
123516
+ return fail("This repo isn't linked to a cloud project. Run `horus cloud link` first.");
123517
+ }
123518
+ const session = authedClient();
123519
+ if (!session) return fail("Not logged in. Run `horus login` first.");
123520
+ let config2;
123521
+ try {
123522
+ config2 = await loadConfig(opts.config);
123523
+ } catch (err2) {
123524
+ return fail(err2.message);
123525
+ }
123526
+ const project = resolveProject(config2, opts.repo);
123527
+ if (!project) return 1;
123528
+ const { db, sql: sql2 } = await openDb(config2.database.url);
123529
+ try {
123530
+ const store = createLocalMemoryStore(db);
123531
+ const row = await store.get(memId);
123532
+ if (row === null) return fail(`Memory item not found: ${memId}`);
123533
+ if (row.kind === "confirmed-outcome" || row.source === "confirmed-outcome") {
123534
+ return fail("Refusing to promote a confirmed-outcome item \u2014 it stays private (spec \xA75.2).");
123535
+ }
123536
+ if (row.origin === "cloud") {
123537
+ if (json2) console.log(JSON.stringify({ ok: true, id: memId, alreadyTeam: true }, null, 2));
123538
+ else console.log(import_picocolors36.default.dim(`${memId} is already a team item \u2014 nothing to promote.`));
123539
+ return 0;
123540
+ }
123541
+ const res = await session.client.promoteTeamMemory(cfg.project.id, {
123542
+ items: [toPromoteInput(row)]
123543
+ });
123544
+ const promoted = res.items.find((it2) => it2.originClientId === memId);
123545
+ if (!promoted) {
123546
+ return fail(
123547
+ `The server did not promote ${memId} (skipped ${res.skipped}/${res.total}). Local state unchanged.`
123548
+ );
123549
+ }
123550
+ const updated = await store.markPromoted(
123551
+ memId,
123552
+ { cloudId: promoted.id, authorName: promoted.authorName ?? null },
123553
+ { actor: { kind: "user" }, note: `promoted to team ${cfg.project.slug}` }
123554
+ );
123555
+ if (json2) {
123556
+ console.log(
123557
+ JSON.stringify(
123558
+ {
123559
+ ok: true,
123560
+ id: memId,
123561
+ cloudId: promoted.id,
123562
+ visibility: updated.visibility,
123563
+ origin: updated.origin
123564
+ },
123565
+ null,
123566
+ 2
123567
+ )
123568
+ );
123569
+ } else {
123570
+ console.log(
123571
+ import_picocolors36.default.green(`Promoted ${memId} to team memory (${cfg.project.slug}) \u2014 now visible to teammates.`)
123572
+ );
123573
+ }
123574
+ return 0;
123575
+ } catch (err2) {
123576
+ return fail(err2.message);
123577
+ } finally {
123578
+ await sql2.end();
123579
+ }
123580
+ }
123581
+ var TEAM_PULL_PAGE_LIMIT = 200;
123582
+ var TEAM_PULL_MAX_PAGES = 1e4;
123583
+ function formatAge3(ms) {
123584
+ const mins = Math.floor(ms / 6e4);
123585
+ if (mins < 1) return "just now";
123586
+ if (mins < 60) return `${mins}m ago`;
123587
+ const hrs = Math.floor(mins / 60);
123588
+ if (hrs < 24) return `${hrs}h ago`;
123589
+ return `${Math.floor(hrs / 24)}d ago`;
123590
+ }
123591
+ async function runMemoryPull(opts) {
123592
+ const json2 = opts.json === true;
123593
+ const fail = (msg) => {
123594
+ if (json2) console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
123595
+ else console.error(import_picocolors36.default.red(msg));
123596
+ return 1;
123597
+ };
123598
+ const root = repoRootOrCwd(opts.cwd);
123599
+ const cfg = readCloudConfig(root);
123600
+ if (!isCloudActive(cfg) || !cfg.project) {
123601
+ return fail("This repo isn't linked to a cloud project. Run `horus cloud link` first.");
123602
+ }
123603
+ const session = authedClient();
123604
+ if (!session) return fail("Not logged in. Run `horus login` first.");
123605
+ let config2;
123606
+ try {
123607
+ config2 = await loadConfig(opts.config);
123608
+ } catch (err2) {
123609
+ return fail(err2.message);
123610
+ }
123611
+ const project = resolveProject(config2, opts.repo);
123612
+ if (!project) return 1;
123613
+ const { db, sql: sql2 } = await openDb(config2.database.url);
123614
+ try {
123615
+ const store = createLocalMemoryStore(db);
123616
+ const vectorIndex = buildVectorIndex(config2, project);
123617
+ const items = [];
123618
+ let since = "0";
123619
+ try {
123620
+ for (let page = 0; page < TEAM_PULL_MAX_PAGES; page++) {
123621
+ const res = await session.client.listTeamMemorySince(cfg.project.id, {
123622
+ since,
123623
+ limit: TEAM_PULL_PAGE_LIMIT,
123624
+ includeDeleted: true
123625
+ });
123626
+ items.push(...res.items);
123627
+ if (!res.hasMore || !res.nextCursor) break;
123628
+ since = res.nextCursor;
123629
+ }
123630
+ } catch (err2) {
123631
+ if (err2 instanceof CloudOfflineError) {
123632
+ const cached2 = await store.query({ repo: project, origin: "cloud" });
123633
+ const newest = cached2.map((c4) => c4.pulledAt ? new Date(c4.pulledAt).getTime() : 0).reduce((a2, b5) => Math.max(a2, b5), 0);
123634
+ const note = newest > 0 ? `stale \u2014 last pulled ${formatAge3(Date.now() - newest)}` : "stale \u2014 never pulled";
123635
+ if (json2) {
123636
+ console.log(
123637
+ JSON.stringify({ ok: true, offline: true, stale: true, cached: cached2.length, note }, null, 2)
123638
+ );
123639
+ } else {
123640
+ console.log(import_picocolors36.default.yellow(`Offline \u2014 serving ${cached2.length} cached team item(s) (${note}).`));
123641
+ }
123642
+ return 0;
123643
+ }
123644
+ throw err2;
123645
+ }
123646
+ const keepIds = [];
123647
+ let upserted = 0;
123648
+ let removed = 0;
123649
+ for (const it2 of items) {
123650
+ if (it2.deletedAt) {
123651
+ await bestEffortRemove(vectorIndex, it2.originClientId);
123652
+ continue;
123653
+ }
123654
+ const cacheRow = {
123655
+ id: it2.originClientId,
123656
+ // stable cross-device id (the promoting device's local id)
123657
+ kind: it2.kind,
123658
+ claim: it2.claim,
123659
+ scope: it2.scope,
123660
+ source: it2.source,
123661
+ status: it2.status ?? "fresh",
123662
+ // A null server confidence stores as 0 — honest (unknown confidence ranks low, never inflated).
123663
+ confidence: it2.confidence ?? 0,
123664
+ evidence: [],
123665
+ repo: project,
123666
+ visibility: "team",
123667
+ cloudId: it2.id,
123668
+ authorName: it2.authorName
123669
+ };
123670
+ await store.upsertCached(cacheRow, { actor: { kind: "system" }, note: "memory pull" });
123671
+ await bestEffortUpsert(vectorIndex, {
123672
+ memoryId: it2.originClientId,
123673
+ claim: it2.claim,
123674
+ repo: project,
123675
+ scope: it2.scope
123676
+ });
123677
+ keepIds.push(it2.originClientId);
123678
+ upserted += 1;
123679
+ }
123680
+ removed = await store.reconcileCache(project, keepIds);
123681
+ if (json2) {
123682
+ console.log(
123683
+ JSON.stringify({ ok: true, project, pulled: upserted, removed, total: items.length }, null, 2)
123684
+ );
123685
+ } else {
123686
+ console.log(
123687
+ import_picocolors36.default.green(
123688
+ `Pulled ${upserted} team memory item(s)` + (removed ? `, dropped ${removed} stale` : "") + "."
123689
+ )
123690
+ );
123691
+ console.log(import_picocolors36.default.dim("Team items are read-only locally \u2014 re-promote to change one. Vectors stay local."));
123692
+ }
123693
+ return 0;
123694
+ } catch (err2) {
123695
+ return fail(err2.message);
123696
+ } finally {
123697
+ await sql2.end();
123698
+ }
123699
+ }
123308
123700
  function outcomeLabelToJSON(l2) {
123309
123701
  return {
123310
123702
  id: l2.id,
@@ -129302,6 +129694,12 @@ Examples:
129302
129694
  process.exitCode = await runMemorySync(opts);
129303
129695
  }
129304
129696
  );
129697
+ memory.command("promote <id>").description("Promote a local memory item into the shared team memory (one-way; refuses confirmed-outcome)").option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository/project WITHIN the loaded config (default: inferred from cwd) \u2014 cross-repo targeting is --config or cd").option("--json", "output JSON").action(async (id, opts) => {
129698
+ process.exitCode = await runMemoryPromote(id, opts);
129699
+ });
129700
+ memory.command("pull").description("Refresh the local cache of shared team memory (full pull; offline serves stale cache)").option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository/project WITHIN the loaded config (default: inferred from cwd) \u2014 cross-repo targeting is --config or cd").option("--force", "force a full refresh even if the cache looks current").option("--json", "output JSON").action(async (opts) => {
129701
+ process.exitCode = await runMemoryPull(opts);
129702
+ });
129305
129703
  const evalCmd = program2.command("eval").description("Read-only accuracy harness over Horus's outcome-label eval set (never writes labels)");
129306
129704
  evalCmd.command("build").description("Emit a byte-stable corpus-<version>.jsonl + manifest from the eval set (read-only)").option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository/project WITHIN the loaded config (default: inferred from cwd) \u2014 cross-repo targeting is --config or cd").option("--source <source>", "filter to one signal source: feedback | confirm").option("--days <n>", "only include labels from the last N days", (v4) => Number(v4)).option("--limit <n>", "max labels to scan", (v4) => Number(v4)).option("--out <dir>", "output directory for the corpus + manifest (default: ./.horus/eval)").option("--version <v>", "corpus schema version tag (default: v1)").option("--json", "output JSON").action(
129307
129705
  async (opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.21.6",
3
+ "version": "0.21.7",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {