@tpsdev-ai/flair 0.53.0 → 0.54.2

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 (97) hide show
  1. package/README.md +4 -1
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +1791 -15648
  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 +1665 -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 +931 -0
  37. package/dist/commands/test.js +93 -0
  38. package/dist/commands/uninstall.js +143 -0
  39. package/dist/commands/upgrade.js +1628 -0
  40. package/dist/commands/workspace.js +114 -0
  41. package/dist/deploy.js +24 -0
  42. package/dist/engine-version.js +12 -4
  43. package/dist/fabric-npm-install.js +87 -0
  44. package/dist/fabric-upgrade.js +30 -15
  45. package/dist/federation-verify.js +498 -0
  46. package/dist/fleet-verify.js +144 -21
  47. package/dist/install/clients.js +167 -0
  48. package/dist/lib/auth-resolve.js +76 -1
  49. package/dist/lib/daemon-liveness.js +131 -2
  50. package/dist/lib/doctor-config-path.js +61 -0
  51. package/dist/lib/doctor-federation-driver.js +189 -0
  52. package/dist/lib/doctor-run.js +40 -0
  53. package/dist/lib/entity-vocab-cli.js +3 -3
  54. package/dist/lib/federation-pair-identity.js +47 -0
  55. package/dist/lib/launchd-repair.js +5 -4
  56. package/dist/lib/npm-registry.js +578 -0
  57. package/dist/lib/ops-api-bind.js +115 -0
  58. package/dist/lib/owned-pins.js +219 -0
  59. package/dist/lib/uninstall-purge.js +218 -0
  60. package/dist/rem/restore.js +8 -10
  61. package/dist/resources/AgentReadPosition.js +74 -0
  62. package/dist/resources/Federation.js +8 -2
  63. package/dist/resources/Memory.js +4 -3
  64. package/dist/resources/MemoryBootstrap.js +41 -25
  65. package/dist/resources/MemoryCandidate.js +5 -6
  66. package/dist/resources/OrgEventCatchup.js +126 -47
  67. package/dist/resources/agent-read-position-lib.js +83 -0
  68. package/dist/resources/agent-read-position.js +120 -0
  69. package/dist/resources/embeddings-boot.js +32 -0
  70. package/dist/resources/federation-peer-liveness.js +73 -0
  71. package/dist/resources/health.js +68 -19
  72. package/dist/resources/mcp-tools.js +48 -279
  73. package/dist/resources/memory-visibility.js +3 -3
  74. package/dist/resources/migration-boot.js +59 -18
  75. package/dist/resources/migrations/embedding-stamp.js +20 -1
  76. package/dist/resources/migrations/recheck.js +43 -0
  77. package/dist/resources/migrations/runner.js +6 -1
  78. package/dist/resources/migrations/stamp-outstanding.js +171 -0
  79. package/dist/resources/migrations/visibility-backfill.js +2 -2
  80. package/dist/resources/org-event-catchup-lib.js +47 -0
  81. package/dist/resources/record-owner-guard.js +1 -0
  82. package/dist/resources/tool-descriptors/index.js +669 -0
  83. package/dist/stamp-migration-verify.js +163 -0
  84. package/dist/stamp-outstanding.js +144 -0
  85. package/dist/version-check.js +29 -8
  86. package/docs/api-reference.md +4 -2
  87. package/docs/deploying-on-fabric.md +11 -10
  88. package/docs/deployment.md +3 -1
  89. package/docs/federation.md +19 -0
  90. package/docs/hosted-on-fabric.md +3 -3
  91. package/docs/quickstart.md +2 -1
  92. package/docs/releasing.md +20 -6
  93. package/docs/spoke-bringup.md +10 -5
  94. package/docs/standalone-local.md +3 -1
  95. package/docs/upgrade.md +25 -6
  96. package/package.json +4 -4
  97. package/schemas/agent.graphql +15 -0
@@ -26,15 +26,17 @@
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, vendored by flair#1683): MCP-facing metadata (name,
30
+ * description, inputSchema, output shape) lives in the descriptor module
31
+ * vendored at build time into `./tool-descriptors/` (source of truth:
32
+ * packages/flair-tool-descriptors/src/index.ts — see
33
+ * scripts/vendor-tool-descriptors.mjs). It is imported by RELATIVE PATH so it
34
+ * resolves inside the packed tarball with no registry dependency on the
35
+ * private descriptor package. This registry
36
+ * binds each *native* descriptor to its Harper impl. The stdio adapter binds
37
+ * the same list (stdio surface) to FlairClient HTTP — tool-set drift is
38
+ * impossible by construction. One-sided tools (`attention`, archive verbs,
39
+ * `relationship_store`) are flagged on the descriptor, not hand-exempted.
38
40
  */
39
41
  /**
40
42
  * The delegated handler classes, held in a mutable registry, LAZILY loaded on
@@ -54,8 +56,9 @@
54
56
  import { AUTHORITY_FIELDS } from "./authority-field-guard.js";
55
57
  import { resolveVersion } from "./version.js";
56
58
  import { agentContext, adminContext, collectionResource } from "./in-process.js";
57
- import { RECORD_USAGE_ID_MERGE_CONTRACT, unionUsageMemoryIds } from "./usage-ids.js";
59
+ import { unionUsageMemoryIds } from "./usage-ids.js";
58
60
  import { SKILL_TAG, isSkillWrite } from "./skill-write.js";
61
+ import { NATIVE_TOOL_DESCRIPTORS, toMcpToolDef, } from "./tool-descriptors/index.js";
59
62
  const H = {};
60
63
  const LOADERS = {
61
64
  SemanticSearch: async () => (await import("./SemanticSearch.js")).SemanticSearch,
@@ -405,7 +408,8 @@ async function skillSearch(agent, args) {
405
408
  * returns the same 404 as an unreadable id. Returning it would (a) make
406
409
  * skill_get an alias for memory_get, and (b) reveal a readable non-skill
407
410
  * 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.
411
+ * The embedding fields are always stripped (flair#1593) there is no
412
+ * includeEmbedding opt-in. memory_get keeps that flag (flair#1188).
409
413
  */
410
414
  async function skillGet(agent, args) {
411
415
  const Cls = await handler("Memory");
@@ -419,7 +423,9 @@ async function skillGet(agent, args) {
419
423
  // returned (see the doc above).
420
424
  if (!isSkillWrite(result))
421
425
  return { error: "skill not found", status: 404 };
422
- return args?.includeEmbedding === true ? result : stripInternalFields(result);
426
+ // flair#1593 — always strip. includeEmbedding used to bypass this and
427
+ // ride the raw vector; nothing in the skill_get contract needs it.
428
+ return stripInternalFields(result);
423
429
  }
424
430
  /**
425
431
  * memory_update — id-targeted, dedup-BYPASSED overwrite/version path (memory-
@@ -850,24 +856,33 @@ export function mcpToolName(table, toolPrefix, verb) {
850
856
  const override = TOOL_NAME_OVERRIDES[table]?.[verb];
851
857
  return override ?? `${toolPrefix}_${verb}`;
852
858
  }
853
- export const TOOLS = {
859
+ /**
860
+ * Bind native descriptors to Harper impls + conformance contracts.
861
+ * A descriptor without a binding (or a binding without a native descriptor)
862
+ * throws at module load — the tool set is derived, not hand-copied.
863
+ */
864
+ function bindNativeTools(bindings) {
865
+ const tools = {};
866
+ const missing = [];
867
+ for (const d of NATIVE_TOOL_DESCRIPTORS) {
868
+ const b = bindings[d.name];
869
+ if (!b) {
870
+ missing.push(d.name);
871
+ continue;
872
+ }
873
+ tools[d.name] = { def: toMcpToolDef(d), impl: b.impl, contract: b.contract };
874
+ }
875
+ if (missing.length > 0) {
876
+ throw new Error(`TOOLS missing Harper bindings for native descriptors: ${missing.join(", ")}`);
877
+ }
878
+ const extra = Object.keys(bindings).filter((n) => !Object.prototype.hasOwnProperty.call(tools, n)).sort();
879
+ if (extra.length > 0) {
880
+ throw new Error(`TOOLS bindings have no native descriptor: ${extra.join(", ")}`);
881
+ }
882
+ return tools;
883
+ }
884
+ export const TOOLS = bindNativeTools({
854
885
  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
886
  impl: memorySearch,
872
887
  contract: {
873
888
  summary: "{ results: MemoryRecord[] } — semantic hits scoped to the caller's own + granted memories; each hit carries content, never the raw embedding.",
@@ -881,30 +896,6 @@ export const TOOLS = {
881
896
  },
882
897
  },
883
898
  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
899
  impl: memoryStore,
909
900
  contract: {
910
901
  summary: "Write echo { id, written:true, deduplicated } — the new id + confirmation. No internal embedding fields; round-trips via memory_get.",
@@ -916,24 +907,6 @@ export const TOOLS = {
916
907
  },
917
908
  },
918
909
  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
910
  impl: skillStore,
938
911
  contract: {
939
912
  summary: "Write echo { id, written:true, deduplicated } for the skill-tagged memory. No internal embedding fields; round-trips via memory_get.",
@@ -945,23 +918,6 @@ export const TOOLS = {
945
918
  },
946
919
  },
947
920
  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
921
  impl: skillSearch,
966
922
  contract: {
967
923
  summary: "{ results: SkillCard[] } — the skill catalog (lightweight id/name/trigger/description/tags/agentId, " +
@@ -979,25 +935,10 @@ export const TOOLS = {
979
935
  },
980
936
  },
981
937
  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
938
  impl: skillGet,
998
939
  contract: {
999
940
  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 " +
941
+ "skill readable under the caller's read-scope — embedding + embeddingModel always stripped. A non-owner " +
1001
942
  "cannot read another agent's private skill, and a readable non-skill id is not found (both 404).",
1002
943
  requiredFields: ["id", "agentId", "content", "createdAt"],
1003
944
  fieldTypes: { id: "string", agentId: "string", content: "string" },
@@ -1007,21 +948,6 @@ export const TOOLS = {
1007
948
  },
1008
949
  },
1009
950
  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
951
  impl: memoryUpdate,
1026
952
  contract: {
1027
953
  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 +959,6 @@ export const TOOLS = {
1033
959
  },
1034
960
  },
1035
961
  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
962
  impl: memoryBasement,
1051
963
  contract: {
1052
964
  summary: "Write echo of the archived record { id, archived:true, archivedAt, ... }. No internal embedding fields; the flip round-trips via memory_get.",
@@ -1058,19 +970,6 @@ export const TOOLS = {
1058
970
  },
1059
971
  },
1060
972
  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
973
  impl: memoryRestore,
1075
974
  contract: {
1076
975
  summary: "Write echo of the restored record { id, archived:false, ... }. No internal embedding fields; the flip round-trips via memory_get.",
@@ -1082,20 +981,6 @@ export const TOOLS = {
1082
981
  },
1083
982
  },
1084
983
  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
984
  impl: memoryGet,
1100
985
  contract: {
1101
986
  summary: "The full memory record { id, agentId, content, durability, createdAt, ... } for the caller's own id — embedding + embeddingModel stripped by default.",
@@ -1107,16 +992,6 @@ export const TOOLS = {
1107
992
  },
1108
993
  },
1109
994
  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
995
  impl: memoryDelete,
1121
996
  contract: {
1122
997
  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 +1000,6 @@ export const TOOLS = {
1125
1000
  },
1126
1001
  },
1127
1002
  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
1003
  impl: bootstrap,
1154
1004
  contract: {
1155
1005
  summary: "Session context: { agentId, soul, memories, predicted, teammateFindings, events, sections, tokenEstimate, memoriesIncluded, ..., context, flairVersion }. "
@@ -1159,6 +1009,7 @@ export const TOOLS = {
1159
1009
  "sections", "tokenEstimate", "maxTokens", "memoriesIncluded", "memoriesAvailable",
1160
1010
  "memoriesTruncated", "teammateFindingsIncluded", "teammateFindingsTruncated",
1161
1011
  "teammateFindingsMatched", "context", "flairVersion",
1012
+ "eventWatermark", "eventsHasMore", "eventsRemaining",
1162
1013
  // flair#1270 — the payload token LEDGER: every token-charged content
1163
1014
  // class has a counter, so tokenEstimate ≈ scaffoldTokens + soulTokens +
1164
1015
  // memoryTokens + trustTokens + eventsTokens decomposes from the payload
@@ -1174,6 +1025,7 @@ export const TOOLS = {
1174
1025
  teammateFindingsMatched: "number", context: "string", flairVersion: "string",
1175
1026
  soulTokens: "number", memoryTokens: "number", trustTokens: "number",
1176
1027
  eventsTokens: "number", scaffoldTokens: "number",
1028
+ eventWatermark: "string", eventsHasMore: "boolean", eventsRemaining: "number",
1177
1029
  },
1178
1030
  invariants: {
1179
1031
  // count == delivered — the historical count/charge/deliver drift.
@@ -1262,18 +1114,6 @@ export const TOOLS = {
1262
1114
  },
1263
1115
  },
1264
1116
  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
1117
  impl: soulSet,
1278
1118
  contract: {
1279
1119
  summary: "Refuses runtime Soul writes, including admin-agent delegation, with { error, status:403 }. Operators use the authenticated REST or CLI path.",
@@ -1282,16 +1122,6 @@ export const TOOLS = {
1282
1122
  },
1283
1123
  },
1284
1124
  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
1125
  impl: soulGet,
1296
1126
  contract: {
1297
1127
  summary: "The soul entry { id, agentId, key, value, createdAt } for the caller's own `${agentId}:${key}`.",
@@ -1301,22 +1131,6 @@ export const TOOLS = {
1301
1131
  },
1302
1132
  },
1303
1133
  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
1134
  impl: workspaceSet,
1321
1135
  contract: {
1322
1136
  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 +1138,6 @@ export const TOOLS = {
1324
1138
  },
1325
1139
  },
1326
1140
  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
1141
  impl: orgEvent,
1343
1142
  contract: {
1344
1143
  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 +1145,6 @@ export const TOOLS = {
1346
1145
  },
1347
1146
  },
1348
1147
  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
1148
  impl: attention,
1365
1149
  contract: {
1366
1150
  summary: "Grouped-by-source view { entity, windowDays, since, groups:{memory,relationship,workspaceState,presence,orgEvent}, counts } for entity E over N days.",
@@ -1373,21 +1157,6 @@ export const TOOLS = {
1373
1157
  },
1374
1158
  },
1375
1159
  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
1160
  impl: recordUsage,
1392
1161
  contract: {
1393
1162
  summary: "Invariant acknowledgement { recorded:true } — byte-identical regardless of how many ids counted (no id enumeration, Sherlock).",
@@ -1396,7 +1165,7 @@ export const TOOLS = {
1396
1165
  invariants: { fullyResolved: true },
1397
1166
  },
1398
1167
  },
1399
- };
1168
+ });
1400
1169
  /** The tool definitions for a tools/list response (exactly the 12 curated tools). */
1401
1170
  export function listToolDefs() {
1402
1171
  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".
@@ -61,6 +61,7 @@ import { buildRegistry } from "./migrations/registry.js";
61
61
  import { runMigrationCycle } from "./migrations/runner.js";
62
62
  import { markIdleMigrationsFailed, seedIdleProgress, setCyclePhase } from "./migrations/progress.js";
63
63
  import { describeUnresolvableDataDir, resolveWritableMigrationDataDir, } from "./migrations/data-dir.js";
64
+ import { scheduleFollowUpCycles } from "./migrations/recheck.js";
64
65
  import { getMode } from "./embeddings-provider.js";
65
66
  /** Same "resolve the running package's own version" idiom as resources/health.ts. */
66
67
  export function resolveRunningVersion() {
@@ -147,6 +148,50 @@ function reportBootFailure(registry, reason) {
147
148
  markIdleMigrationsFailed(registry.list().map((m) => m.id), reason);
148
149
  }
149
150
  let scheduled = false;
151
+ let followUp = null;
152
+ /**
153
+ * Runs one cycle against an already-resolved writable data dir. Shared by
154
+ * the boot-keyed first pass and the flair#1073 follow-up rechecks. Never
155
+ * throws. Returns whether a follow-up series should be armed (true unless
156
+ * this invocation is itself a follow-up, or the cycle was a hard failure
157
+ * that needs an operator — those retry on the next restart).
158
+ */
159
+ async function invokeCycle(registry, dataDir, opts = { followUp: false }) {
160
+ try {
161
+ const result = await runMigrationCycle({
162
+ registry,
163
+ getTable,
164
+ dataDir,
165
+ runningVersion: resolveRunningVersion(),
166
+ });
167
+ // `nothing pending` is the healthy no-op; `single-flight` is the
168
+ // lock guard working as designed on a multi-threaded boot. Anything
169
+ // else is a cycle that WANTED to run and couldn't, and must be loud.
170
+ if (!result.ran && result.reason && !isBenignSkip(result.reason)) {
171
+ if (!opts.followUp) {
172
+ reportBootFailure(registry, `migration cycle did not run: ${result.reason}`);
173
+ }
174
+ else {
175
+ console.error(`[flair-migrations] follow-up cycle did not run: ${result.reason}`);
176
+ }
177
+ return false;
178
+ }
179
+ if (opts.followUp && result.ran) {
180
+ console.info("[flair-migrations] follow-up cycle ran — embedding-stamp (or another pending migration) had work the boot pass missed");
181
+ }
182
+ return true;
183
+ }
184
+ catch (err) {
185
+ // Defense-in-depth only — runMigrationCycle is documented to never
186
+ // throw. A boot-path exception must never surface here regardless.
187
+ const reason = `unexpected error from runMigrationCycle: ${err?.message ?? String(err)}`;
188
+ if (!opts.followUp)
189
+ reportBootFailure(registry, reason);
190
+ else
191
+ console.error(`[flair-migrations] ${reason}`);
192
+ return false;
193
+ }
194
+ }
150
195
  export function scheduleMigrationBoot() {
151
196
  if (scheduled)
152
197
  return;
@@ -170,28 +215,22 @@ export function scheduleMigrationBoot() {
170
215
  // failure at that point is reported back rather than thrown, which is
171
216
  // precisely how flair#812 stayed invisible. See data-dir.ts.
172
217
  const resolved = resolveWritableMigrationDataDir();
173
- if (!resolved.dataDir) {
218
+ const dataDir = resolved.dataDir;
219
+ if (!dataDir) {
174
220
  reportBootFailure(registry, describeUnresolvableDataDir(resolved.tried));
175
221
  return;
176
222
  }
177
- try {
178
- const result = await runMigrationCycle({
179
- registry,
180
- getTable,
181
- dataDir: resolved.dataDir,
182
- runningVersion: resolveRunningVersion(),
223
+ const ok = await invokeCycle(registry, dataDir, { followUp: false });
224
+ // flair#1073: even a clean "nothing pending" can be a Fabric race
225
+ // (tables ready, rows not yet visible). Arm delayed rechecks so a
226
+ // split corpus heals in-process instead of sitting until the next
227
+ // restart. A hard boot failure does not arm them — those need an
228
+ // operator or a restart (same as before).
229
+ if (ok) {
230
+ followUp?.cancel();
231
+ followUp = scheduleFollowUpCycles({
232
+ run: () => invokeCycle(registry, dataDir, { followUp: true }).then(() => undefined),
183
233
  });
184
- // `nothing pending` is the healthy no-op; `single-flight` is the
185
- // lock guard working as designed on a multi-threaded boot. Anything
186
- // else is a cycle that WANTED to run and couldn't, and must be loud.
187
- if (!result.ran && result.reason && !isBenignSkip(result.reason)) {
188
- reportBootFailure(registry, `migration cycle did not run: ${result.reason}`);
189
- }
190
- }
191
- catch (err) {
192
- // Defense-in-depth only — runMigrationCycle is documented to never
193
- // throw. A boot-path exception must never surface here regardless.
194
- reportBootFailure(registry, `unexpected error from runMigrationCycle: ${err?.message ?? String(err)}`);
195
234
  }
196
235
  })();
197
236
  });
@@ -209,5 +248,7 @@ export function isBenignSkip(reason) {
209
248
  // ever boots once).
210
249
  export function _resetMigrationBootForTests() {
211
250
  scheduled = false;
251
+ followUp?.cancel();
252
+ followUp = null;
212
253
  }
213
254
  scheduleMigrationBoot();