@tpsdev-ai/flair 0.52.0 → 0.54.1

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 (99) hide show
  1. package/README.md +4 -1
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +1914 -15328
  4. package/dist/commands/agent.js +453 -0
  5. package/dist/commands/attention.js +121 -0
  6. package/dist/commands/backup.js +115 -0
  7. package/dist/commands/bootstrap.js +91 -0
  8. package/dist/commands/bridge.js +608 -0
  9. package/dist/commands/deploy.js +180 -0
  10. package/dist/commands/doctor.js +1654 -0
  11. package/dist/commands/export.js +110 -0
  12. package/dist/commands/federation.js +1575 -0
  13. package/dist/commands/fleet.js +73 -0
  14. package/dist/commands/grant.js +109 -0
  15. package/dist/commands/hook.js +193 -0
  16. package/dist/commands/idp.js +193 -0
  17. package/dist/commands/import.js +134 -0
  18. package/dist/commands/init.js +1203 -0
  19. package/dist/commands/inspect.js +45 -0
  20. package/dist/commands/keys.js +187 -0
  21. package/dist/commands/mcp.js +707 -0
  22. package/dist/commands/memory.js +501 -0
  23. package/dist/commands/migrate-harness-memory.js +270 -0
  24. package/dist/commands/orgevent.js +138 -0
  25. package/dist/commands/presence.js +76 -0
  26. package/dist/commands/principal.js +338 -0
  27. package/dist/commands/quality.js +1164 -0
  28. package/dist/commands/reembed.js +296 -0
  29. package/dist/commands/relationship.js +76 -0
  30. package/dist/commands/rem.js +1048 -0
  31. package/dist/commands/restore.js +130 -0
  32. package/dist/commands/search.js +244 -0
  33. package/dist/commands/service.js +315 -0
  34. package/dist/commands/session.js +184 -0
  35. package/dist/commands/soul.js +155 -0
  36. package/dist/commands/status.js +914 -0
  37. package/dist/commands/test.js +93 -0
  38. package/dist/commands/uninstall.js +143 -0
  39. package/dist/commands/upgrade.js +1592 -0
  40. package/dist/commands/workspace.js +114 -0
  41. package/dist/deploy.js +24 -0
  42. package/dist/fabric-npm-install.js +87 -0
  43. package/dist/federation-verify.js +498 -0
  44. package/dist/fleet-verify.js +144 -21
  45. package/dist/install/clients.js +167 -0
  46. package/dist/lib/auth-resolve.js +76 -1
  47. package/dist/lib/daemon-liveness.js +131 -2
  48. package/dist/lib/doctor-config-path.js +61 -0
  49. package/dist/lib/doctor-federation-driver.js +189 -0
  50. package/dist/lib/doctor-run.js +40 -0
  51. package/dist/lib/entity-vocab-cli.js +3 -3
  52. package/dist/lib/federation-pair-identity.js +47 -0
  53. package/dist/lib/launchd-repair.js +199 -0
  54. package/dist/lib/ops-api-bind.js +115 -0
  55. package/dist/lib/owned-pins.js +219 -0
  56. package/dist/lib/stabilize-mqtt-network.js +123 -0
  57. package/dist/lib/uninstall-purge.js +218 -0
  58. package/dist/rem/restore.js +8 -10
  59. package/dist/resources/AgentReadPosition.js +74 -0
  60. package/dist/resources/Federation.js +8 -2
  61. package/dist/resources/Memory.js +4 -3
  62. package/dist/resources/MemoryBootstrap.js +41 -25
  63. package/dist/resources/MemoryCandidate.js +5 -6
  64. package/dist/resources/OrgEventCatchup.js +126 -47
  65. package/dist/resources/agent-read-position-lib.js +83 -0
  66. package/dist/resources/agent-read-position.js +120 -0
  67. package/dist/resources/embeddings-boot.js +32 -0
  68. package/dist/resources/federation-peer-liveness.js +73 -0
  69. package/dist/resources/health.js +68 -19
  70. package/dist/resources/mcp-tools.js +43 -279
  71. package/dist/resources/memory-visibility.js +3 -3
  72. package/dist/resources/migration-boot.js +59 -18
  73. package/dist/resources/migrations/embedding-stamp.js +20 -1
  74. package/dist/resources/migrations/recheck.js +43 -0
  75. package/dist/resources/migrations/runner.js +6 -1
  76. package/dist/resources/migrations/stamp-outstanding.js +171 -0
  77. package/dist/resources/migrations/visibility-backfill.js +2 -2
  78. package/dist/resources/org-event-catchup-lib.js +47 -0
  79. package/dist/resources/record-owner-guard.js +1 -0
  80. package/dist/stamp-migration-verify.js +163 -0
  81. package/dist/stamp-outstanding.js +144 -0
  82. package/docs/api-reference.md +4 -2
  83. package/docs/deploying-on-fabric.md +11 -10
  84. package/docs/deployment.md +3 -1
  85. package/docs/federation.md +19 -0
  86. package/docs/hosted-on-fabric.md +3 -3
  87. package/docs/quickstart.md +2 -1
  88. package/docs/releasing.md +15 -7
  89. package/docs/spoke-bringup.md +10 -5
  90. package/docs/standalone-local.md +3 -1
  91. package/docs/upgrade.md +25 -6
  92. package/node_modules/@tpsdev-ai/flair-tool-descriptors/LICENSE +19 -0
  93. package/node_modules/@tpsdev-ai/flair-tool-descriptors/README.md +22 -0
  94. package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.d.ts +70 -0
  95. package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.js +665 -0
  96. package/node_modules/@tpsdev-ai/flair-tool-descriptors/package.json +46 -0
  97. package/package.json +9 -4
  98. package/schemas/agent.graphql +15 -0
  99. package/templates/launchd/start-flair-with-admin-pass.sh +73 -0
@@ -11,7 +11,10 @@ import { REM_DEDUP_STATS_PATH } from "./dedup-cluster.js";
11
11
  import { hybridEnabled } from "./bm25.js";
12
12
  import { bm25IndexEnabled, bm25IndexStatus } from "./bm25-index-service.js";
13
13
  import { normalizeStamp } from "./embedding-space-guard.js";
14
+ import { getModelId } from "./embeddings-provider.js";
15
+ import { describeStampOutstanding, EMBEDDING_STAMP_ID } from "./migrations/stamp-outstanding.js";
14
16
  import { buildPublicHealthBody, resolveSearchReadiness } from "./search-readiness.js";
17
+ import { classifyPeerLiveness, federationPeersAllDisconnectedWarning, summarizePeerLiveness, } from "./federation-peer-liveness.js";
15
18
  const db = databases;
16
19
  const redactHome = (p) => {
17
20
  const home = homedir();
@@ -160,6 +163,14 @@ export class HealthDetail extends Resource {
160
163
  const stats = { ok: true };
161
164
  const nowMs = Date.now();
162
165
  const warnings = [];
166
+ // flair#1073: set while walking memories, consumed after the migrations
167
+ // snapshot so the outstanding-migration warning can name runner state.
168
+ // Never copied onto `stats` — it is not part of the /HealthDetail shape.
169
+ // `memoryScanOk` is the gate (not "mixed spaces"): a uniformly pre-flip
170
+ // corpus is one space and still outstanding against getModelId().
171
+ let mixedEmbeddingSpaces = false;
172
+ let memoryScanOk = false;
173
+ let memoryModelCounts = {};
163
174
  // flair#1326: same search-ready signal as public /Health. HealthDetail
164
175
  // stays HTTP 200 (it is a stats dump, not a traffic gate); the field
165
176
  // and a warning name the lag so `flair status` / operators can see it.
@@ -258,13 +269,12 @@ export class HealthDetail extends Resource {
258
269
  // re-embed) still trips it, and that is exactly when the query-time guard
259
270
  // degrades recall to keyword-only.
260
271
  const distinctSpaces = new Set(realModels.map((k) => normalizeStamp(k)).filter((s) => s !== null));
261
- if (distinctSpaces.size > 1) {
262
- const list = realModels.map((k) => `${k}:${modelCounts[k]}`).join(", ");
263
- warnings.push({
264
- level: "warn",
265
- message: `multiple embedding models in use (${list}) — cross-model search unreliable; run: flair reembed against one model`,
266
- });
267
- }
272
+ // Always keep counts — a uniformly stale pre-flip corpus is one space
273
+ // and must still name embedding-stamp (Bugbot High on flair#1606).
274
+ memoryScanOk = true;
275
+ memoryModelCounts = modelCounts;
276
+ if (distinctSpaces.size > 1)
277
+ mixedEmbeddingSpaces = true;
268
278
  }
269
279
  catch {
270
280
  stats.memories = null;
@@ -378,11 +388,16 @@ export class HealthDetail extends Resource {
378
388
  }
379
389
  else {
380
390
  const inst = instances[0];
391
+ // flair#1499: derive connected/down from lastSyncAt, not stored
392
+ // status. Pairing writes `paired`; a recent lastSyncAt is connected.
393
+ // Revoked rows are counted separately and never drive the >24h warning.
394
+ const liveness = summarizePeerLiveness(peers, nowMs);
381
395
  const peersBlock = {
382
- total: peers.length,
383
- connected: peers.filter((p) => p.status === "connected").length,
384
- disconnected: peers.filter((p) => p.status === "disconnected").length,
385
- revoked: peers.filter((p) => p.status === "revoked").length,
396
+ total: liveness.total,
397
+ connected: liveness.connected,
398
+ disconnected: liveness.disconnected,
399
+ revoked: liveness.revoked,
400
+ unknown: liveness.unknown,
386
401
  };
387
402
  const pendingTokens = tokens.filter((t) => !t.consumedBy && t.expiresAt && new Date(t.expiresAt).getTime() > nowMs).length;
388
403
  stats.federation = {
@@ -395,17 +410,13 @@ export class HealthDetail extends Resource {
395
410
  role: p.role,
396
411
  status: p.status,
397
412
  lastSyncAt: p.lastSyncAt ?? null,
413
+ liveness: classifyPeerLiveness(p, nowMs),
398
414
  }))
399
415
  : undefined,
400
416
  };
401
- if (peers.length > 0 && peersBlock.connected === 0) {
402
- const oldest = peers
403
- .map((p) => (p.lastSyncAt ? new Date(p.lastSyncAt).getTime() : 0))
404
- .reduce((a, b) => (a === 0 ? b : b === 0 ? a : Math.min(a, b)), 0);
405
- if (oldest > 0 && nowMs - oldest > 24 * 3600 * 1000) {
406
- warnings.push({ level: "warn", message: "federation peers all disconnected >24h" });
407
- }
408
- }
417
+ const disconnectWarn = federationPeersAllDisconnectedWarning(liveness);
418
+ if (disconnectWarn)
419
+ warnings.push(disconnectWarn);
409
420
  if (pendingTokens > 0) {
410
421
  warnings.push({ level: "info", message: `${pendingTokens} pairing token(s) unconsumed` });
411
422
  }
@@ -660,9 +671,47 @@ export class HealthDetail extends Resource {
660
671
  message: "migration boot cycle never fired on this instance (cyclePhase=idle) — no migration will run until this is resolved; see `flair doctor`",
661
672
  });
662
673
  }
674
+ if (memoryScanOk) {
675
+ const stamp = snapshot.migrations.find((m) => m.id === EMBEDDING_STAMP_ID);
676
+ const outstanding = describeStampOutstanding({
677
+ modelCounts: memoryModelCounts,
678
+ currentModelId: getModelId(),
679
+ migration: stamp,
680
+ cyclePhase: snapshot.cyclePhase,
681
+ lastCycleError: snapshot.lastCycleError,
682
+ });
683
+ if (outstanding.outstanding) {
684
+ warnings.push({ level: "warn", message: outstanding.warning });
685
+ }
686
+ else if (mixedEmbeddingSpaces) {
687
+ const list = Object.entries(memoryModelCounts)
688
+ .filter(([k, n]) => k !== "hash-512d" && n > 0)
689
+ .map(([k, n]) => `${k}:${n}`)
690
+ .join(", ");
691
+ warnings.push({
692
+ level: "warn",
693
+ message: `multiple embedding models in use (${list}) — cross-model search unreliable; run: flair reembed against one model`,
694
+ });
695
+ }
696
+ }
663
697
  }
664
698
  catch {
665
699
  stats.migrations = null;
700
+ if (memoryScanOk) {
701
+ const outstanding = describeStampOutstanding({
702
+ modelCounts: memoryModelCounts,
703
+ currentModelId: getModelId(),
704
+ });
705
+ if (outstanding.outstanding) {
706
+ warnings.push({ level: "warn", message: outstanding.warning });
707
+ }
708
+ else if (mixedEmbeddingSpaces) {
709
+ warnings.push({
710
+ level: "warn",
711
+ message: `multiple embedding models in use — cross-model search unreliable; run: flair reembed against one model`,
712
+ });
713
+ }
714
+ }
666
715
  }
667
716
  // ── Disk ──
668
717
  // flair#812: same shared read-only resolution as the migrations section
@@ -26,15 +26,12 @@
26
26
  * the resolved agent, never from the tool arguments — an agent can only act as
27
27
  * itself (no forging of agentId / authorId in the body).
28
28
  *
29
- * NOTE (flair#677 scope call): the legacy `@tpsdev-ai/flair-mcp` stdio proxy
30
- * (packages/flair-mcp) is a SEPARATE, independently-published package that
31
- * talks to flair over HTTP via `FlairClient` it is not wired through this
32
- * registry at all (its own tool list is hardcoded in packages/flair-mcp/src/
33
- * index.ts). Per the zero-install north star (retiring flair-mcp in favor of
34
- * this native /mcp handler), `attention` is added HERE only, not mirrored into
35
- * the legacy stdio proxy — adding it there would mean a separate package
36
- * version bump + a new FlairClient method, out of scope for this query-only
37
- * slice.
29
+ * NOTE (flair#1580): MCP-facing metadata (name, description, inputSchema,
30
+ * output shape) lives in `@tpsdev-ai/flair-tool-descriptors`. This registry
31
+ * binds each *native* descriptor to its Harper impl. The stdio adapter binds
32
+ * the same list (stdio surface) to FlairClient HTTP tool-set drift is
33
+ * impossible by construction. One-sided tools (`attention`, archive verbs,
34
+ * `relationship_store`) are flagged on the descriptor, not hand-exempted.
38
35
  */
39
36
  /**
40
37
  * The delegated handler classes, held in a mutable registry, LAZILY loaded on
@@ -54,8 +51,9 @@
54
51
  import { AUTHORITY_FIELDS } from "./authority-field-guard.js";
55
52
  import { resolveVersion } from "./version.js";
56
53
  import { agentContext, adminContext, collectionResource } from "./in-process.js";
57
- import { RECORD_USAGE_ID_MERGE_CONTRACT, unionUsageMemoryIds } from "./usage-ids.js";
54
+ import { unionUsageMemoryIds } from "./usage-ids.js";
58
55
  import { SKILL_TAG, isSkillWrite } from "./skill-write.js";
56
+ import { NATIVE_TOOL_DESCRIPTORS, toMcpToolDef, } from "@tpsdev-ai/flair-tool-descriptors";
59
57
  const H = {};
60
58
  const LOADERS = {
61
59
  SemanticSearch: async () => (await import("./SemanticSearch.js")).SemanticSearch,
@@ -405,7 +403,8 @@ async function skillSearch(agent, args) {
405
403
  * returns the same 404 as an unreadable id. Returning it would (a) make
406
404
  * skill_get an alias for memory_get, and (b) reveal a readable non-skill
407
405
  * memory's existence through a skill-shaped call; a uniform 404 does neither.
408
- * The embedding fields are stripped by default, same as memory_get.
406
+ * The embedding fields are always stripped (flair#1593) there is no
407
+ * includeEmbedding opt-in. memory_get keeps that flag (flair#1188).
409
408
  */
410
409
  async function skillGet(agent, args) {
411
410
  const Cls = await handler("Memory");
@@ -419,7 +418,9 @@ async function skillGet(agent, args) {
419
418
  // returned (see the doc above).
420
419
  if (!isSkillWrite(result))
421
420
  return { error: "skill not found", status: 404 };
422
- return args?.includeEmbedding === true ? result : stripInternalFields(result);
421
+ // flair#1593 — always strip. includeEmbedding used to bypass this and
422
+ // ride the raw vector; nothing in the skill_get contract needs it.
423
+ return stripInternalFields(result);
423
424
  }
424
425
  /**
425
426
  * memory_update — id-targeted, dedup-BYPASSED overwrite/version path (memory-
@@ -850,24 +851,33 @@ export function mcpToolName(table, toolPrefix, verb) {
850
851
  const override = TOOL_NAME_OVERRIDES[table]?.[verb];
851
852
  return override ?? `${toolPrefix}_${verb}`;
852
853
  }
853
- export const TOOLS = {
854
+ /**
855
+ * Bind native descriptors to Harper impls + conformance contracts.
856
+ * A descriptor without a binding (or a binding without a native descriptor)
857
+ * throws at module load — the tool set is derived, not hand-copied.
858
+ */
859
+ function bindNativeTools(bindings) {
860
+ const tools = {};
861
+ const missing = [];
862
+ for (const d of NATIVE_TOOL_DESCRIPTORS) {
863
+ const b = bindings[d.name];
864
+ if (!b) {
865
+ missing.push(d.name);
866
+ continue;
867
+ }
868
+ tools[d.name] = { def: toMcpToolDef(d), impl: b.impl, contract: b.contract };
869
+ }
870
+ if (missing.length > 0) {
871
+ throw new Error(`TOOLS missing Harper bindings for native descriptors: ${missing.join(", ")}`);
872
+ }
873
+ const extra = Object.keys(bindings).filter((n) => !Object.prototype.hasOwnProperty.call(tools, n)).sort();
874
+ if (extra.length > 0) {
875
+ throw new Error(`TOOLS bindings have no native descriptor: ${extra.join(", ")}`);
876
+ }
877
+ return tools;
878
+ }
879
+ export const TOOLS = bindNativeTools({
854
880
  memory_search: {
855
- def: {
856
- name: "memory_search",
857
- description: "Search memories by meaning. Understands temporal queries like 'what happened today'. Scoped to your agent's own + granted memories.",
858
- annotations: { readOnlyHint: true },
859
- inputSchema: {
860
- type: "object",
861
- properties: {
862
- query: { type: "string", description: "Search query — natural language, semantic matching" },
863
- limit: { type: "number", description: "Max results (default 5)" },
864
- includeTrust: { type: "boolean", description: "Attach a per-result trust-evidence block (provenance, author, usage, freshness, supersession). Default false." },
865
- abstain: { type: "boolean", description: "Opt into first-class abstention: when the best match is below a global confidence threshold, return { abstained: true, reason, bestScore } with no weak matches instead of the N weakest results. Default false." },
866
- includeArchived: { type: "boolean", description: "Include basemented (archived) memories in results. Default false — archived memories are excluded from normal search. When true, archived memories are returned under the SAME read-scope gate as a normal search (never a wider scope)." },
867
- },
868
- required: ["query"],
869
- },
870
- },
871
881
  impl: memorySearch,
872
882
  contract: {
873
883
  summary: "{ results: MemoryRecord[] } — semantic hits scoped to the caller's own + granted memories; each hit carries content, never the raw embedding.",
@@ -881,30 +891,6 @@ export const TOOLS = {
881
891
  },
882
892
  },
883
893
  memory_store: {
884
- def: {
885
- name: "memory_store",
886
- description: "Save information to persistent memory. Use for lessons, decisions, preferences, facts. Attributed to your authenticated agent.",
887
- inputSchema: {
888
- type: "object",
889
- properties: {
890
- content: { type: "string", description: "What to remember" },
891
- type: { type: "string", enum: ["session", "lesson", "decision", "preference", "fact", "goal"], description: "Memory type (default session)" },
892
- durability: { type: "string", enum: ["permanent", "persistent", "standard", "ephemeral"], description: "permanent > persistent > standard > ephemeral (default standard)" },
893
- tags: { type: "array", items: { type: "string" }, description: "Tag strings" },
894
- visibility: {
895
- type: "string",
896
- enum: ["private", "shared"],
897
- description: "Writer-controlled sharing intent. Omit to use the server's durability-keyed default: " +
898
- "permanent/persistent -> shared, standard/ephemeral -> private. " +
899
- "private — owner-only, never visible to another agent, even one holding a memory grant. " +
900
- "shared — visible to the owner and every other agent on this instance. " +
901
- "The visibility the write actually landed on is returned in the result.",
902
- },
903
- usedMemoryIds: { type: "array", items: { type: "string" }, description: "IDs of memories that informed this write (citation-on-write). Credited via the same deduped usage ledger as record_usage. Optional." },
904
- },
905
- required: ["content"],
906
- },
907
- },
908
894
  impl: memoryStore,
909
895
  contract: {
910
896
  summary: "Write echo { id, written:true, deduplicated } — the new id + confirmation. No internal embedding fields; round-trips via memory_get.",
@@ -916,24 +902,6 @@ export const TOOLS = {
916
902
  },
917
903
  },
918
904
  skill_store: {
919
- def: {
920
- name: "skill_store",
921
- description: "Write a skill (a reusable capability/procedure) as a skill-tagged memory. " +
922
- "The `trigger` text is what the skill embeds from (the recall signal — 'when to use this'), " +
923
- "and `content` is the full procedure. Skills are forced durability=persistent and are " +
924
- "SkillScan-gated before the embed (a dangerous shell/network payload is rejected).",
925
- inputSchema: {
926
- type: "object",
927
- properties: {
928
- content: { type: "string", description: "The full procedure (markdown body of the SKILL.md)" },
929
- trigger: { type: "string", description: "The 'when to use' text — the recall signal the skill embeds from" },
930
- name: { type: "string", description: "Skill name (SKILL.md frontmatter; stored in metadata)" },
931
- description: { type: "string", description: "Skill description (SKILL.md frontmatter; stored in metadata)" },
932
- tags: { type: "array", items: { type: "string" }, description: "Additional tags (the 'skill' tag is added automatically)" },
933
- },
934
- required: ["content"],
935
- },
936
- },
937
905
  impl: skillStore,
938
906
  contract: {
939
907
  summary: "Write echo { id, written:true, deduplicated } for the skill-tagged memory. No internal embedding fields; round-trips via memory_get.",
@@ -945,23 +913,6 @@ export const TOOLS = {
945
913
  },
946
914
  },
947
915
  skill_search: {
948
- def: {
949
- name: "skill_search",
950
- description: "Find skills (reusable capabilities/procedures) that apply to a task. " +
951
- "Ranks skill-tagged memories by their `trigger` ('when to use') against your task text. " +
952
- "Returns a lightweight CATALOG — id, name, trigger, description, tags, agentId — NOT the full " +
953
- "procedure (fetch that with skill_get). Scoped to your own + shared skills; another agent's " +
954
- "private skill is never returned.",
955
- annotations: { readOnlyHint: true },
956
- inputSchema: {
957
- type: "object",
958
- properties: {
959
- task: { type: "string", description: "The task/context to match skills against — natural language; ranked against each skill's trigger" },
960
- limit: { type: "number", description: "Max skills to return (default 5)" },
961
- },
962
- required: ["task"],
963
- },
964
- },
965
916
  impl: skillSearch,
966
917
  contract: {
967
918
  summary: "{ results: SkillCard[] } — the skill catalog (lightweight id/name/trigger/description/tags/agentId, " +
@@ -979,25 +930,10 @@ export const TOOLS = {
979
930
  },
980
931
  },
981
932
  skill_get: {
982
- def: {
983
- name: "skill_get",
984
- description: "Retrieve a full skill by ID — the complete procedure (`content`) plus trigger and metadata. " +
985
- "The disclosure step after skill_search's catalog. Read-scoped: you can only get your own or a " +
986
- "shared skill, never another agent's private skill. A non-skill id returns not-found.",
987
- annotations: { readOnlyHint: true },
988
- inputSchema: {
989
- type: "object",
990
- properties: {
991
- id: { type: "string", description: "Skill (memory) ID" },
992
- includeEmbedding: { type: "boolean", description: "Include the raw embedding vector (large, rarely useful). Default false." },
993
- },
994
- required: ["id"],
995
- },
996
- },
997
933
  impl: skillGet,
998
934
  contract: {
999
935
  summary: "The full skill record { id, agentId, content, trigger, tags, durability, metadata, createdAt, ... } for a " +
1000
- "skill readable under the caller's read-scope — embedding + embeddingModel stripped by default. A non-owner " +
936
+ "skill readable under the caller's read-scope — embedding + embeddingModel always stripped. A non-owner " +
1001
937
  "cannot read another agent's private skill, and a readable non-skill id is not found (both 404).",
1002
938
  requiredFields: ["id", "agentId", "content", "createdAt"],
1003
939
  fieldTypes: { id: "string", agentId: "string", content: "string" },
@@ -1007,21 +943,6 @@ export const TOOLS = {
1007
943
  },
1008
944
  },
1009
945
  memory_update: {
1010
- def: {
1011
- name: "memory_update",
1012
- description: "Update an existing memory by ID. Dedup-bypassed (this is an intentional overwrite, not a new write). " +
1013
- "Default: overwrites the same id in place. Pass preserveHistory=true to instead write a new version " +
1014
- "linked via `supersedes`, closing the old one's validity window.",
1015
- inputSchema: {
1016
- type: "object",
1017
- properties: {
1018
- id: { type: "string", description: "ID of the memory to update" },
1019
- content: { type: "string", description: "New content" },
1020
- preserveHistory: { type: "boolean", description: "Write a new version (supersedes-linked) instead of overwriting in place (default false)" },
1021
- },
1022
- required: ["id", "content"],
1023
- },
1024
- },
1025
946
  impl: memoryUpdate,
1026
947
  contract: {
1027
948
  summary: "Write echo { id, written:true } for the in-place overwrite (or supersede). No internal embedding fields; the change round-trips via memory_get.",
@@ -1033,20 +954,6 @@ export const TOOLS = {
1033
954
  },
1034
955
  },
1035
956
  memory_basement: {
1036
- def: {
1037
- name: "memory_basement",
1038
- description: "Send a memory to the basement (archive it). Sets archived=true and stamps archivedAt. " +
1039
- "The memory is removed from bootstrap and default search but remains retrievable via " +
1040
- "memory_get and memory_search(includeArchived:true). Deliberate and GLOBAL — this is a " +
1041
- "visibility flag, not a deletion: provenance and history are untouched. Scoped to your own memories only.",
1042
- inputSchema: {
1043
- type: "object",
1044
- properties: {
1045
- id: { type: "string", description: "ID of the memory to basement (archive)" },
1046
- },
1047
- required: ["id"],
1048
- },
1049
- },
1050
957
  impl: memoryBasement,
1051
958
  contract: {
1052
959
  summary: "Write echo of the archived record { id, archived:true, archivedAt, ... }. No internal embedding fields; the flip round-trips via memory_get.",
@@ -1058,19 +965,6 @@ export const TOOLS = {
1058
965
  },
1059
966
  },
1060
967
  memory_restore: {
1061
- def: {
1062
- name: "memory_restore",
1063
- description: "Restore a basemented (archived) memory. Clears archived and archivedAt. Deliberate and GLOBAL — " +
1064
- "this un-retires the memory for EVERY session, not a session-local view (per-session reuse is " +
1065
- "drawers, which do not exist yet). Scoped to your own memories only.",
1066
- inputSchema: {
1067
- type: "object",
1068
- properties: {
1069
- id: { type: "string", description: "ID of the memory to restore (un-archive)" },
1070
- },
1071
- required: ["id"],
1072
- },
1073
- },
1074
968
  impl: memoryRestore,
1075
969
  contract: {
1076
970
  summary: "Write echo of the restored record { id, archived:false, ... }. No internal embedding fields; the flip round-trips via memory_get.",
@@ -1082,20 +976,6 @@ export const TOOLS = {
1082
976
  },
1083
977
  },
1084
978
  memory_get: {
1085
- def: {
1086
- name: "memory_get",
1087
- description: "Retrieve a specific memory by ID. The record's raw embedding vector is omitted by default (it is large and not useful to a caller); pass includeEmbedding=true to include it.",
1088
- annotations: { readOnlyHint: true },
1089
- inputSchema: {
1090
- type: "object",
1091
- properties: {
1092
- id: { type: "string", description: "Memory ID" },
1093
- includeTrust: { type: "boolean", description: "Attach a trust-evidence block (provenance, author, usage, freshness, supersession) to the record. Default false." },
1094
- includeEmbedding: { type: "boolean", description: "Include the raw embedding vector (hundreds of floats) in the returned record. Omitted by default because it is large and rarely useful to a caller. Default false." },
1095
- },
1096
- required: ["id"],
1097
- },
1098
- },
1099
979
  impl: memoryGet,
1100
980
  contract: {
1101
981
  summary: "The full memory record { id, agentId, content, durability, createdAt, ... } for the caller's own id — embedding + embeddingModel stripped by default.",
@@ -1107,16 +987,6 @@ export const TOOLS = {
1107
987
  },
1108
988
  },
1109
989
  memory_delete: {
1110
- def: {
1111
- name: "memory_delete",
1112
- description: "Delete a memory by ID. You can only delete your own memories.",
1113
- annotations: { destructiveHint: true },
1114
- inputSchema: {
1115
- type: "object",
1116
- properties: { id: { type: "string", description: "Memory ID to delete" } },
1117
- required: ["id"],
1118
- },
1119
- },
1120
990
  impl: memoryDelete,
1121
991
  contract: {
1122
992
  summary: "Deletes the caller's own memory at any durability tier (success echo is thin). Cross-owner deletion returns { error, status:403 } for a non-admin; a deleted row round-trips as gone via memory_get.",
@@ -1125,31 +995,6 @@ export const TOOLS = {
1125
995
  },
1126
996
  },
1127
997
  bootstrap: {
1128
- def: {
1129
- name: "bootstrap",
1130
- description: "Get session context: soul + memories + predicted context. Run at session start. Pass subjects for predictive loading.",
1131
- annotations: { readOnlyHint: true },
1132
- inputSchema: {
1133
- type: "object",
1134
- properties: {
1135
- maxTokens: { type: "number", description: "Content-selection budget in tokens (default 4000): the hard cap on how much soul/memory/finding CONTENT is selected. The actual serialized response (reported by tokenEstimate) may exceed this by the structured-container JSON scaffolding — maxTokens bounds what is selected, not the raw output size. Raise it to include more content." },
1136
- currentTask: { type: "string", description: "Current task — enables semantic search for relevant memories" },
1137
- channel: { type: "string", description: "Channel name (discord, tps-mail, claude-code)" },
1138
- surface: { type: "string", description: "Surface name (tps-build, tps-review, cli-session)" },
1139
- subjects: { type: "array", items: { type: "string" }, description: "Entity names to preload context for" },
1140
- entities: {
1141
- type: "array",
1142
- items: { type: "string" },
1143
- description: "Your declared attention-plane vocabulary strings (e.g. \"issue:owner/repo#123\") for collision surfacing's 'Others in the room' block — teammates with overlapping active work. Falls back to your own most-recent workspace-state entities when omitted.",
1144
- },
1145
- includeTrust: { type: "boolean", description: "Also return a `trust` array with a per-included-memory trust-evidence block (provenance, author, usage, freshness, supersession). Default false." },
1146
- abstain: { type: "boolean", description: "Opt into a task-relevance abstention verdict: also return an `abstention` object ({ abstained, bestScore, threshold }) reporting whether any memory covered `currentTask` above a global confidence threshold. Default false." },
1147
- includeContext: { type: "boolean", description: "Also return the prose `context` string — a human-readable mirror of the structured soul/memories/predicted/teammateFindings containers (which are the canonical payload). Default false here: the structured fields already carry everything, so shipping the prose too would double the payload." },
1148
- maxEvents: { type: "number", description: "Cap on how many recent org events to return (default 10). Events are counted against maxTokens like every other content section." },
1149
- includeEventDetail: { type: "boolean", description: "Also include each org event's verbose `detail` JSON (migration internals, etc.). Default false: bootstrap ships lean events (id/kind/summary/createdAt/targetIds/scope); `detail` mostly restates the summary and is pure bloat for a connector." },
1150
- },
1151
- },
1152
- },
1153
998
  impl: bootstrap,
1154
999
  contract: {
1155
1000
  summary: "Session context: { agentId, soul, memories, predicted, teammateFindings, events, sections, tokenEstimate, memoriesIncluded, ..., context, flairVersion }. "
@@ -1159,6 +1004,7 @@ export const TOOLS = {
1159
1004
  "sections", "tokenEstimate", "maxTokens", "memoriesIncluded", "memoriesAvailable",
1160
1005
  "memoriesTruncated", "teammateFindingsIncluded", "teammateFindingsTruncated",
1161
1006
  "teammateFindingsMatched", "context", "flairVersion",
1007
+ "eventWatermark", "eventsHasMore", "eventsRemaining",
1162
1008
  // flair#1270 — the payload token LEDGER: every token-charged content
1163
1009
  // class has a counter, so tokenEstimate ≈ scaffoldTokens + soulTokens +
1164
1010
  // memoryTokens + trustTokens + eventsTokens decomposes from the payload
@@ -1174,6 +1020,7 @@ export const TOOLS = {
1174
1020
  teammateFindingsMatched: "number", context: "string", flairVersion: "string",
1175
1021
  soulTokens: "number", memoryTokens: "number", trustTokens: "number",
1176
1022
  eventsTokens: "number", scaffoldTokens: "number",
1023
+ eventWatermark: "string", eventsHasMore: "boolean", eventsRemaining: "number",
1177
1024
  },
1178
1025
  invariants: {
1179
1026
  // count == delivered — the historical count/charge/deliver drift.
@@ -1262,18 +1109,6 @@ export const TOOLS = {
1262
1109
  },
1263
1110
  },
1264
1111
  soul_set: {
1265
- def: {
1266
- name: "soul_set",
1267
- description: "Soul changes require operator credentials through the REST API or CLI; runtime tool calls are refused.",
1268
- inputSchema: {
1269
- type: "object",
1270
- properties: {
1271
- key: { type: "string", description: "Entry key (e.g. 'role', 'standards', 'project')" },
1272
- value: { type: "string", description: "Entry value" },
1273
- },
1274
- required: ["key", "value"],
1275
- },
1276
- },
1277
1112
  impl: soulSet,
1278
1113
  contract: {
1279
1114
  summary: "Refuses runtime Soul writes, including admin-agent delegation, with { error, status:403 }. Operators use the authenticated REST or CLI path.",
@@ -1282,16 +1117,6 @@ export const TOOLS = {
1282
1117
  },
1283
1118
  },
1284
1119
  soul_get: {
1285
- def: {
1286
- name: "soul_get",
1287
- description: "Get a personality or project context entry.",
1288
- annotations: { readOnlyHint: true },
1289
- inputSchema: {
1290
- type: "object",
1291
- properties: { key: { type: "string", description: "Entry key" } },
1292
- required: ["key"],
1293
- },
1294
- },
1295
1120
  impl: soulGet,
1296
1121
  contract: {
1297
1122
  summary: "The soul entry { id, agentId, key, value, createdAt } for the caller's own `${agentId}:${key}`.",
@@ -1301,22 +1126,6 @@ export const TOOLS = {
1301
1126
  },
1302
1127
  },
1303
1128
  flair_workspace_set: {
1304
- def: {
1305
- name: "flair_workspace_set",
1306
- description: "Set your agent's current workspace state in the Office Space coordination layer. Attributed to you — you can only write your own state.",
1307
- inputSchema: {
1308
- type: "object",
1309
- properties: {
1310
- ref: { type: "string", description: "Workspace ref — branch, worktree, or task ref" },
1311
- label: { type: "string", description: "Human-readable label" },
1312
- provider: { type: "string", description: "Provider/runtime (default mcp)" },
1313
- task: { type: "string", description: "Task/issue id" },
1314
- phase: { type: "string", description: "Current phase (design, implement, review)" },
1315
- summary: { type: "string", description: "Short summary of current state" },
1316
- },
1317
- required: ["ref"],
1318
- },
1319
- },
1320
1129
  impl: workspaceSet,
1321
1130
  contract: {
1322
1131
  summary: "Writes the caller's workspace state keyed `${agentId}:${ref}`, attributed to the caller (never the body). The echo is thin; persistence is verified in storage.",
@@ -1324,21 +1133,6 @@ export const TOOLS = {
1324
1133
  },
1325
1134
  },
1326
1135
  flair_orgevent: {
1327
- def: {
1328
- name: "flair_orgevent",
1329
- description: "Publish an org-wide coordination event (claim/release/status) to the Office Space. Attributed to you — you cannot publish as another agent.",
1330
- inputSchema: {
1331
- type: "object",
1332
- properties: {
1333
- kind: { type: "string", description: "Event kind (coord.claim, coord.release, status)" },
1334
- summary: { type: "string", description: "Short summary of the event" },
1335
- detail: { type: "string", description: "Longer detail payload" },
1336
- scope: { type: "string", description: "Scope (an agent id, repo, or 'org')" },
1337
- targets: { type: "array", items: { type: "string" }, description: "Recipient agent ids" },
1338
- },
1339
- required: ["kind", "summary"],
1340
- },
1341
- },
1342
1136
  impl: orgEvent,
1343
1137
  contract: {
1344
1138
  summary: "Publishes an org event attributed to the caller (authorId from identity, never the body). The echo is thin; persistence is verified in storage.",
@@ -1346,21 +1140,6 @@ export const TOOLS = {
1346
1140
  },
1347
1141
  },
1348
1142
  attention: {
1349
- def: {
1350
- name: "attention",
1351
- description: "What's touching entity E in the last N days? A unified, grouped-by-source view across memories, " +
1352
- "relationships, active work (WorkspaceState), teammate presence, and org events. Entity must be a " +
1353
- "vocabulary string (e.g. 'repo:owner/name', 'issue:owner/repo#123', 'subsystem:embeddings').",
1354
- annotations: { readOnlyHint: true },
1355
- inputSchema: {
1356
- type: "object",
1357
- properties: {
1358
- entity: { type: "string", description: "Vocabulary string, exact match (type:value — e.g. 'repo:tpsdev-ai/flair')" },
1359
- days: { type: "number", description: "Window size in days (default 7)" },
1360
- },
1361
- required: ["entity"],
1362
- },
1363
- },
1364
1143
  impl: attention,
1365
1144
  contract: {
1366
1145
  summary: "Grouped-by-source view { entity, windowDays, since, groups:{memory,relationship,workspaceState,presence,orgEvent}, counts } for entity E over N days.",
@@ -1373,21 +1152,6 @@ export const TOOLS = {
1373
1152
  },
1374
1153
  },
1375
1154
  record_usage: {
1376
- def: {
1377
- name: "record_usage",
1378
- description: "Report that one or more memories were actually USED — cited or relied on to ground an answer or decision. " +
1379
- "Distinct from search (surfacing a memory is not usage). Drives the recall-quality usage signal; dedup'd " +
1380
- "(you can only count once per memory) and rate-limited. " +
1381
- RECORD_USAGE_ID_MERGE_CONTRACT,
1382
- inputSchema: {
1383
- type: "object",
1384
- properties: {
1385
- memoryIds: { type: "array", items: { type: "string" }, description: "IDs of the memories that were used (max 20 per call). Merged with memoryId when both are supplied." },
1386
- memoryId: { type: "string", description: "Convenience alias for a single memory id. Merged with memoryIds when both are supplied — not dropped." },
1387
- attribution: { type: "string", description: "Optional free-text note on what used it (opaque — stored for audit only, max 500 chars)" },
1388
- },
1389
- },
1390
- },
1391
1155
  impl: recordUsage,
1392
1156
  contract: {
1393
1157
  summary: "Invariant acknowledgement { recorded:true } — byte-identical regardless of how many ids counted (no id enumeration, Sherlock).",
@@ -1396,7 +1160,7 @@ export const TOOLS = {
1396
1160
  invariants: { fullyResolved: true },
1397
1161
  },
1398
1162
  },
1399
- };
1163
+ });
1400
1164
  /** The tool definitions for a tools/list response (exactly the 12 curated tools). */
1401
1165
  export function listToolDefs() {
1402
1166
  return Object.values(TOOLS).map((t) => t.def);
@@ -6,7 +6,7 @@
6
6
  * - resources/memory-read-scope.ts's resolveReadScope() — the cross-agent
7
7
  * READ scope every read path (Memory.search/get, SemanticSearch,
8
8
  * MemoryBootstrap, the by-id auth-middleware guard) resolves through.
9
- * - src/cli.ts's runFederationSyncOnce() — the federation-sync PUSH filter
9
+ * - src/commands/federation.ts's runFederationSyncOnce() — the federation-sync PUSH filter
10
10
  * that must not replicate `private` memories to peer instances.
11
11
  *
12
12
  * Deliberately has ZERO imports — not even "harper". That is
@@ -16,8 +16,8 @@
16
16
  * imports `databases` from "harper", and that package's
17
17
  * top-level init eagerly resolves storage paths and THROWS when there is no
18
18
  * live Harper runtime backing it (confirmed empirically — it takes down
19
- * even `flair --help`). So src/cli.ts must never import
20
- * resources/memory-read-scope.ts (or anything else that drags that
19
+ * even `flair --help`). So the CLI (src/cli.ts / src/commands/federation.ts)
20
+ * must never import resources/memory-read-scope.ts (or anything else that drags that
21
21
  * side-effecting import in) directly. This module is the safe seam: a pure
22
22
  * function + constant that both sides can import without dragging in
23
23
  * "harper".