@tpsdev-ai/flair 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/config.yaml +35 -2
  2. package/dist/build-info.json +6 -0
  3. package/dist/cli.js +847 -168
  4. package/dist/doctor-client.js +358 -11
  5. package/dist/federation/scheduler.js +114 -9
  6. package/dist/hook-install.js +150 -1
  7. package/dist/install/global-bin-path.js +234 -0
  8. package/dist/lib/entity-vocab-cli.js +113 -0
  9. package/dist/lib/mcp-enable.js +71 -21
  10. package/dist/lib/scheduler-platform.js +363 -1
  11. package/dist/postinstall.cjs +88 -0
  12. package/dist/rem/runner.js +177 -10
  13. package/dist/rem/scheduler.js +126 -20
  14. package/dist/resources/AttentionQuery.js +5 -3
  15. package/dist/resources/AutoPromoteCandidates.js +18 -12
  16. package/dist/resources/Federation.js +49 -5
  17. package/dist/resources/Memory.js +36 -2
  18. package/dist/resources/MemoryBootstrap.js +118 -7
  19. package/dist/resources/MemoryMaintenance.js +8 -2
  20. package/dist/resources/MemoryReflect.js +70 -5
  21. package/dist/resources/auto-promote-lib.js +46 -0
  22. package/dist/resources/build-info.js +50 -0
  23. package/dist/resources/entity-vocab.js +25 -1
  24. package/dist/resources/health.js +25 -5
  25. package/dist/resources/mcp-oauth-flag.js +20 -0
  26. package/dist/resources/mcp-oauth.js +6 -1
  27. package/dist/resources/mcp-tools.js +53 -3
  28. package/dist/resources/memory-reflect-lib.js +201 -4
  29. package/dist/src/lib/scheduler-platform.js +363 -1
  30. package/dist/src/rem/scheduler.js +126 -20
  31. package/docs/deepseek-harness.md +110 -0
  32. package/docs/entity-vocabulary.md +15 -0
  33. package/docs/integrations.md +1 -0
  34. package/docs/mcp-clients.md +4 -0
  35. package/docs/notes/mcp-oauth-model2.md +52 -3
  36. package/package.json +5 -4
  37. package/schemas/memory.graphql +12 -0
  38. package/templates/bin/flair-federation-sync.sh.tmpl +8 -1
  39. package/templates/bin/flair-rem-nightly.sh.tmpl +8 -1
@@ -0,0 +1,110 @@
1
+ # Flair + DeepSeek Harness (zero-code MCP bridge)
2
+
3
+ Give DeepSeek Harness (DSH) sessions persistent, portable memory — no plugin code, just one Cordis overlay wiring [`@tpsdev-ai/flair-mcp`](../packages/flair-mcp) through DSH's first-party MCP bridge, [`@deepseek-ai/dsh-mcp-client`](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/mcp/mcp-client).
4
+
5
+ > **Verified against DSH as of 2026-08-20** (`deepseek-ai/deepseek-harness`, branch `master`). DSH is a developer preview and its own README promises compatibility-breaking changes. If wiring fails after a DSH upgrade, re-check the config field names against [their MCP client README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/mcp/mcp-client/README.md) before suspecting Flair.
6
+
7
+ The same eleven tools every other MCP client gets ([full table in mcp-clients.md](mcp-clients.md#what-the-mcp-server-exposes)) appear to the model under DSH's server-qualified names: `mcp__flair__memory_search`, `mcp__flair__memory_store`, `mcp__flair__bootstrap`, and so on — the same `mcp__<server>__<tool>` convention Claude Code uses.
8
+
9
+ Two caveats up front, both structural to DSH's bridge (details below):
10
+
11
+ 1. **DSH scrubs the ambient environment before spawning MCP servers.** Flair's env vars must be declared in the overlay's `config.env` — exported shell vars will not reliably reach the server.
12
+ 2. **Recall on this path is reactive.** The model must *choose* to call the memory tools; DSH's MCP bridge cannot inject Flair context at session start. There is a documented mitigation (a persona nudge), but real auto-inject requires a native DSH plugin — planned as phase 2 of [flair#1289](https://github.com/tpsdev-ai/flair/issues/1289).
13
+
14
+ ## Prerequisites
15
+
16
+ Same as every MCP client — a running Flair and an agent identity. Follow [Step 1 of mcp-clients.md](mcp-clients.md#step-1--install-flair-do-once) (install, `flair init`, `flair agent add <id>`, `flair status`). If DSH runs on a machine that cannot see your Flair instance's loopback address, you need a reachable `FLAIR_URL` — see [quickstart-fabric.md](quickstart-fabric.md).
17
+
18
+ DSH spawns the server with `npx`, so the machine running DSH needs Node.js 22+ (Flair's own floor).
19
+
20
+ ## The overlay
21
+
22
+ A ready-to-use copy of this file ships in the repo at [`examples/deepseek-harness/flair.cordis.yml`](../examples/deepseek-harness/flair.cordis.yml):
23
+
24
+ ```yaml
25
+ - insert:
26
+ - id: memory-flair
27
+ name: '@deepseek-ai/dsh-mcp-client'
28
+ config:
29
+ serverName: flair
30
+ transport: stdio
31
+ command: npx
32
+ args: ['-y', '@tpsdev-ai/flair-mcp@<version>']
33
+ env:
34
+ FLAIR_AGENT_ID: <agent-id>
35
+ FLAIR_URL: http://127.0.0.1:19926
36
+ ```
37
+
38
+ Replace the two placeholders before use:
39
+
40
+ - `<version>` — pin the flair-mcp version you intend to run (the one you already have is `flair --version`). The [pinning rationale from mcp-clients.md](mcp-clients.md#step-2--wire-the-mcp-server-into-your-cli) applies with extra force here: DSH re-spawns the command per session, so an unpinned spec re-resolves to whatever is currently on npm every time. Leaving the literal `<version>` in place fails loudly at `npx` — intended.
41
+ - `<agent-id>` — the identity you created with `flair agent add`.
42
+
43
+ `FLAIR_URL` as shown is the local default; point it at your Fabric URL for a remote instance.
44
+
45
+ Apply it for one run:
46
+
47
+ ```sh
48
+ dsh web --patch "$PWD/examples/deepseek-harness/flair.cordis.yml"
49
+ ```
50
+
51
+ To keep it across runs, merge the single `insert` patch into a user patch layer — `$DSH_HOME/profiles/<name>/cordis.patch.yml` for one profile, or `$DSH_HOME/cordis.patch.yml` machine-wide. Merge into an existing file rather than copying over it; it may already carry unrelated patches.
52
+
53
+ DSH's own reference memory overlays prefer a preinstalled binary over a package runner ("DSH starts it but does not run a package manager"). If you want that shape: `npm install -g @tpsdev-ai/flair-mcp@<version>`, then `command: flair-mcp` with no `args`.
54
+
55
+ ## Caveat 1 — the bridge scrubs ambient env; declare Flair's env explicitly
56
+
57
+ Before spawning a stdio MCP server, DSH's bridge builds the child environment from a **scrubbed** copy of the parent env: every variable whose name matches `KEY`, `PASSWORD`, `SECRET`, or `TOKEN` (case-insensitive) is dropped, and so is every `DSH_*` variable. The overlay's `config.env` is merged **after** the scrub, so it is the one reliable channel.
58
+
59
+ Concretely for Flair:
60
+
61
+ - `FLAIR_KEY_PATH` contains `KEY` — an exported value is **silently dropped**. If your Ed25519 key is not at the default `~/.flair/keys/<agent>.key`, you must set `FLAIR_KEY_PATH` in `config.env`.
62
+ - `FLAIR_AGENT_ID` and `FLAIR_URL` happen to survive today's scrub pattern, but the pattern is DSH's to change. Declare all three in `config.env` and depend on none of the ambient env.
63
+
64
+ This mirrors the general rule from [mcp-clients.md troubleshooting](mcp-clients.md#troubleshooting) — a client's own env does not propagate to the spawned MCP subprocess unless declared — DSH just enforces it deliberately.
65
+
66
+ ## Caveat 2 — recall is reactive on this path
67
+
68
+ DSH's MCP bridge registers **tools** on the model's tool list. That is all it can do: DSH has no first-class memory seam, and the bridge has no way to run `bootstrap` at session start and inject the result into context. Whether memory gets consulted is the model's per-turn decision — identical to the behavior DSH documents for its own reference memory servers.
69
+
70
+ The documented mitigation is a standing prompt nudge. DSH's deployment persona is the `persona` config key on its `system-prompt` row (agent presets can shadow it with a persona row of their own; there is no end-user prompt-editing API — prompt text is config/composition only). Add something like:
71
+
72
+ > At the start of a task, call `mcp__flair__bootstrap` or `mcp__flair__memory_search` to load relevant memory before planning. When you make a decision worth keeping, or the user asks you to remember something, record it with `mcp__flair__memory_store`.
73
+
74
+ This is additive guidance in the shape DSH's own memory examples recommend, and it works — but it is a nudge, not a guarantee. **Honest limitation:** automatic session-start injection (what Flair's Claude Code `SessionStart` hook does) requires a native DSH plugin using their per-turn system-prompt context seam. That is phase 2 of [flair#1289](https://github.com/tpsdev-ai/flair/issues/1289); until it ships, this wiring gives pull-based memory only.
75
+
76
+ ## Tools-only bridging
77
+
78
+ DSH bridges MCP **tools** only — Resources and Prompts are explicitly not bridged (a documented DSH limitation, not a Flair one). This costs nothing here: `flair-mcp` is a tools-only server, so its entire surface crosses the bridge.
79
+
80
+ ## Verify your wiring
81
+
82
+ Initial tool discovery is asynchronous — wait until the `mcp__flair__*` tools appear in the session's tool list before the first prompt. Then run the write → fresh-session → recall check (the same protocol shape DSH uses to validate its own reference memory servers):
83
+
84
+ 1. In DSH session A, ask: *"Remember that my validation drink is lapsang-`<unique suffix>`."* Confirm the model calls `mcp__flair__memory_store` and the tool reports success.
85
+ 2. Open DSH session B in the same running Host — do not copy session A's conversation. Ask: *"What is my validation drink? Check memory."* Confirm the model calls `mcp__flair__memory_search` and returns the value.
86
+ 3. Still in session B, ask it to *use* the recalled value ("suggest one drink for the meeting"). Confirm the answer builds on it.
87
+
88
+ A new DSH session is enough; a Host restart is not. Because the memory now lives in Flair rather than a local file, the same check also passes *across harnesses*: store in DSH, then `memory_search` from Claude Code or any other [MCP client](mcp-clients.md) pointed at the same Flair instance and agent.
89
+
90
+ ## Config reference (the fields this wiring uses)
91
+
92
+ Field names verified against DSH `master` 2026-08-20; [their table](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/mcp/mcp-client/README.md) is authoritative.
93
+
94
+ | Field | Value here | Notes |
95
+ |---|---|---|
96
+ | `serverName` | `flair` | Namespace for tool names (`mcp__flair__*`); must be unique across live instances |
97
+ | `transport` | `stdio` | flair-mcp is a stdio server |
98
+ | `command` / `args` | `npx` / `['-y', '@tpsdev-ai/flair-mcp@<version>']` | Or a preinstalled `flair-mcp` binary |
99
+ | `env` | `FLAIR_AGENT_ID`, `FLAIR_URL`, optionally `FLAIR_KEY_PATH` | Merged after DSH's env scrub — the only reliable channel (see caveat 1) |
100
+ | `toolCallTimeoutMs` | (default 60000) | Per-tool-call timeout; raise it only if slow remote searches genuinely exceed a minute |
101
+
102
+ ## Troubleshooting
103
+
104
+ **"FLAIR_AGENT_ID is required" on startup.** The env block is missing or ambient-only — declare it in `config.env` (caveat 1).
105
+
106
+ **Tools never appear.** DSH logs initial connection and discovery failures; by default a failed startup registers no tools rather than failing the plugin. Check `flair status` on the Flair side, and check the DSH logs for the `flair` server's connect errors. A duplicate `serverName: flair` across live instances fails the later instance at load.
107
+
108
+ **`auth_error` on every call.** Identity/key mismatch — and remember that an exported `FLAIR_KEY_PATH` never reaches the server (caveat 1). Re-run `flair agent add <id>` (idempotent) or set `FLAIR_KEY_PATH` in `config.env`.
109
+
110
+ For everything else: [troubleshooting.md](troubleshooting.md).
@@ -89,6 +89,21 @@ Per the attention-plane spec's K&S-approved refinements, `entities: [String] @in
89
89
  Existing rows on all three tables simply carry no `entities` — readers must tolerate absence,
90
90
  the same pattern already used for `Presence.activityUpdatedAt`. No migration, no backfill.
91
91
 
92
+ All three fields are reachable from the CLI (flair#1288): `flair memory add`,
93
+ `flair workspace set`, and `flair orgevent` take `--entities <csv>`, a comma-separated list of
94
+ vocabulary strings:
95
+
96
+ ```bash
97
+ flair memory add --agent flint --entities "repo:tpsdev-ai/flair,issue:tpsdev-ai/flair#1288" "shipped the entities CLI surface"
98
+ ```
99
+
100
+ The CLI validates each value before writing (a malformed value is rejected with the
101
+ `type:value` format and the valid type list); the server re-validates on every write path
102
+ regardless. The CLI's validator is an inlined copy of this module
103
+ (`src/lib/entity-vocab-cli.ts` — `src/` can't import across the packaging boundary into
104
+ `resources/`), pinned to it by `test/unit/cli-entities-option.test.ts`; the server-side gate
105
+ remains this module alone.
106
+
92
107
  `Relationship` gets **no** `entities` field: its `subject`/`object` columns already carry
93
108
  free-form entity-reference strings and are already indexed — they're the vocabulary carrier
94
109
  for that table. They are lowercased on write today but not yet validated against this
@@ -17,6 +17,7 @@ Where Flair already runs. Each integration shown here is a working surface — t
17
17
  | **Gemini CLI** | [`flair-mcp`](#claude-code-cursor-codex-gemini-cli-continuedev-via-flair-mcp) | MCP config | Standard MCP server |
18
18
  | **Antigravity CLI** (`agy`) | [`flair-mcp`](#claude-code-cursor-codex-gemini-cli-continuedev-via-flair-mcp) | MCP config | `~/.gemini/config/mcp_config.json`; pickup by a live `agy` pending verification |
19
19
  | **Goose** (block/goose) | [`flair-mcp`](#claude-code-cursor-codex-gemini-cli-continuedev-via-flair-mcp) | MCP config | Goose ships native MCP support |
20
+ | **DeepSeek Harness** (`dsh`) | [`flair-mcp`](deepseek-harness.md) | Cordis overlay | First-party MCP bridge; tools-only, reactive recall — [dedicated page](deepseek-harness.md) |
20
21
  | **LangGraph (TS)** | [`langgraph-flair`](#langgraph-typescript) | FlairClient | Drop-in `BaseStore` |
21
22
  | **OpenClaw** | [`openclaw-flair`](#openclaw) | Ed25519 | Native plugin + context engine |
22
23
  | **n8n** | [`n8n-nodes-flair`](#n8n) | FlairApi credential | Three nodes (chat memory, search, store) |
@@ -30,6 +30,8 @@ flair agent add my-project
30
30
  flair status
31
31
  ```
32
32
 
33
+ > **`flair: command not found` right after installing?** Your npm global prefix's bin dir isn't on PATH (common with a user prefix like `~/.npm-global`) — run `export PATH="$(npm prefix -g)/bin:$PATH"`, persist that line in your shell profile, and `flair doctor` will print the exact line for your shell any time.
34
+
33
35
  Flair runs as a local server at `http://127.0.0.1:19926` by default. The MCP server connects to it on demand via Ed25519-signed requests; nothing leaves your machine unless you explicitly route to a remote Flair instance.
34
36
 
35
37
  ---
@@ -244,6 +246,8 @@ Which memories are non-private is decided at write time, and the default is not
244
246
 
245
247
  `bootstrap` returns the canonical structured containers — `soul`, `memories`, `predicted`, `teammateFindings`, `events` — plus counts and a `tokenEstimate`. The containers are **always present** (empty `[]`/`{}` when there's nothing), so an empty container is distinguishable from an unsupported one.
246
248
 
249
+ **The token ledger reconciles `tokenEstimate` from the payload alone (flair#1270).** Every token-charged content class carries a counter — `soulTokens`, `memoryTokens`, `trustTokens`, `eventsTokens` — plus a measured `scaffoldTokens` for the fixed JSON frame, and `tokenEstimate ≈ scaffoldTokens + soulTokens + memoryTokens + trustTokens + eventsTokens`. The remaining ≈ gap is the bounded per-item difference between the prose lines the memory counters measure and the heavier structured objects the containers ship. A payload whose estimate an agent can't decompose from the reported figures is a bug, not an accounting convention.
250
+
247
251
  **Empty containers say why they're empty (flair#1182).** When a structured container ships empty, the payload carries a short hint naming the reason and what fills it — `eventsHint`, `teammateFindingsHint`, `predictedHint`. This is present *only* when the container is empty, so a deliberately-empty container is never confused with a silent drop (a connector never has to diff against a previous payload to tell the two apart).
248
252
 
249
253
  **`matchQuality` is null on lifecycle sections — by design (flair#1225).** With `includeTrust: true`, each included memory carries a per-memory trust block, section-tagged, whose `matchQuality` is a `strong`/`moderate`/`breadcrumb` confidence band. On the **lifecycle sections** (`permanent`, `recent`, `predicted`) `matchQuality` is `null`: those are a lifecycle-window *load*, not a retrieval surface, so there is no relevance score to band. This is **correct, not a scoring failure** — an own-recent `null` next to a teammate's band does not mean your own records "scored worse". A retrieval band is only meaningful on the retrieval sections (`relevant`, `teammate`). The entry's `section` field makes this legible, and a `matchQualityNote` on any null entry states the reason inline.
@@ -46,6 +46,48 @@ context, so the wrapped handler scopes to the verified agent exactly as an
46
46
  Ed25519-signed REST call would. Identity always comes from the resolved agent,
47
47
  never from the tool arguments (no forging of agentId / authorId).
48
48
 
49
+ ### Which Agent is my connector? (distinct-by-default — flair#1280)
50
+
51
+ **The connector's Agent is whatever the `Credential(kind:"idp")` mapping says
52
+ — and that is NOT constrained to be your CLI agent.** Distinct identities are
53
+ the ruled default (flair#1280): per-purpose connector identities are the
54
+ product pattern for org/service installs, and same-identity is a deliberate
55
+ opt-in, never something the server infers. The practical consequences:
56
+
57
+ - **Where the mapping is decided.** `flair mcp enable --principal <agent-id>
58
+ --idp-subject <sub>` (the identity-mapping step, backed by
59
+ `provisionIdpIdentityMapping`) writes the mapping. The `--principal` you pass
60
+ is the Agent every `/mcp` call will read and write as. Pass an EXISTING
61
+ agent id to attach the sub to it; the step's output states the resulting
62
+ `sub → Agent` mapping in as many words.
63
+ - **Linking a sub to an existing Agent (the same-identity opt-in).** Re-run
64
+ `flair mcp enable` with the SAME `--idp-provider`/`--idp-subject` and
65
+ `--principal <your-cli-agent-id>`. The existing `(provider, subject)`
66
+ Credential is RE-POINTED to that principal — one Credential row per subject,
67
+ so resolution stays deterministic. The link *replaces* the mapping; it does
68
+ not merge the two agents' memories.
69
+ - **First diagnostic: ask the server who you are.** The `bootstrap` tool's
70
+ response always carries the resolved `agentId` and a `scope` descriptor
71
+ (`scope.agentId` / `scope.isAdmin` / `scope.reads`, flair#1182). "My memory
72
+ is empty over the connector" + a `bootstrap.agentId` you don't recognize =
73
+ the sub resolved to a different (often JIT-provisioned) Agent — link it as
74
+ above.
75
+ - **JIT caveat.** A JIT-provisioned mapping (`FLAIR_MCP_JIT_PROVISION=1`)
76
+ stamps `idpProvider: "mcp-oauth"`. Runtime resolution matches on
77
+ `(kind, idpSubject)` only, but the *linking* upsert matches on
78
+ `(kind, idpProvider, idpSubject)` — so when re-linking a JIT-provisioned
79
+ sub, pass `--idp-provider mcp-oauth` (matching the JIT stamp), or first
80
+ revoke the JIT credential (`status: "revoked"`). Linking under a different
81
+ provider name creates a SECOND active credential for the same subject, and
82
+ which one wins resolution is unspecified.
83
+
84
+ The two-identity contract (a distinct connector agent sees other agents'
85
+ org-non-private rows, never their private rows, 404-never-403 by id; a linked
86
+ connector sees exactly what the linked agent sees) is pinned end-to-end by
87
+ `test/integration/mcp-connector-principal-mapping.test.ts`, which drives the
88
+ real `mcpHandler`/`resolveAgentFromSub` against a real store with two
89
+ registered identities.
90
+
49
91
  ## Enabling (operator checklist)
50
92
 
51
93
  1. **Install the AS plugin** — add `@harperfast/oauth` (already an exact-pinned
@@ -59,9 +101,12 @@ never from the tool arguments (no forging of agentId / authorId).
59
101
  clientId: ${OAUTH_GITHUB_CLIENT_ID}
60
102
  clientSecret: ${OAUTH_GITHUB_CLIENT_SECRET}
61
103
  mcp:
62
- enabled: true
104
+ enabled: ${FLAIR_MCP_OAUTH} # whole-token env reference (flair#1152) — the choice lives in the ENVIRONMENT, so a re-packed deploy can't revert it
63
105
  issuer: ${FLAIR_MCP_ISSUER} # pin to your public origin — REQUIRED
64
- resource: ${FLAIR_MCP_ISSUER}/mcp # RFC-8707 audience the /mcp token binds to
106
+ # NO resource key (flair#1180): the plugin derives <issuer>/mcp when it is
107
+ # absent. A composite like ${FLAIR_MCP_ISSUER}/mcp never interpolates
108
+ # (whole-token-only expansion) and fails every connect with
109
+ # invalid_target. Non-standard resource: set an explicit LITERAL URL.
65
110
  accessTokenTtl: 900 # 5–15 min (Sherlock req 1) — short-lived
66
111
  dynamicClientRegistration:
67
112
  enabled: false # DCR is NOT SUPPORTED (flair#756) — explicit, not omitted (an absent block leaves DCR OPEN by the plugin's own default)
@@ -82,7 +127,11 @@ never from the tool arguments (no forging of agentId / authorId).
82
127
  turning the surface on.
83
128
 
84
129
  2. **Set the env:**
85
- - `FLAIR_MCP_OAUTH=1` — turns on the `/mcp` route registration.
130
+ - `FLAIR_MCP_OAUTH=true` — turns on the `/mcp` route registration AND the
131
+ component AS (flair#1152: `true` is the ONE value both readers accept —
132
+ flair's flag takes 1/true/yes/on, but the component's config read of the
133
+ same var accepts only "true"/"false" and deletes anything else, so `1`
134
+ gives you a guarded `/mcp` with no authorization server behind it).
86
135
  - `FLAIR_MCP_ISSUER=https://your-public-origin` (or `FLAIR_PUBLIC_URL`).
87
136
  - `FLAIR_MCP_JIT_PROVISION=1` — ONLY if you want unknown subjects
88
137
  auto-provisioned (default OFF; pre-provision Agent+Credential otherwise).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "packageManager": "bun@1.3.10",
5
5
  "description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
6
6
  "type": "module",
@@ -47,12 +47,13 @@
47
47
  "scripts": {
48
48
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
49
49
  "prebuild": "npm run clean",
50
- "build": "tsc -p tsconfig.json --noCheck",
51
- "build:cli": "tsc -p tsconfig.cli.json --noCheck",
50
+ "build": "tsc -p tsconfig.json --noCheck && node scripts/write-build-info.mjs",
51
+ "build:cli": "tsc -p tsconfig.cli.json --noCheck && node scripts/write-build-info.mjs",
52
52
  "prepublishOnly": "npm run build && npm run build:cli",
53
53
  "test": "bun test",
54
54
  "test:e2e": "playwright test",
55
- "release": "./scripts/release.sh"
55
+ "release": "./scripts/release.sh",
56
+ "postinstall": "node -e \"try{require('./dist/postinstall.cjs')}catch(e){}\""
56
57
  },
57
58
  "publishConfig": {
58
59
  "access": "public"
@@ -219,4 +219,16 @@ type MemoryCandidate @table(database: "flair") @export {
219
219
  # evidence and would promote tagless (a cross-user leak). Nullable/additive —
220
220
  # pre-#1205b candidates and non-tagged (scope:"recent"/"all") distillations
221
221
  # read null, unchanged behavior (clean-upgrade-path gate).
222
+ visibilityRuling: String # flair#1257 slice 3: the distiller's AFFIRMATIVE visibility ruling for the
223
+ # promoted row — only ever "shared", and only stamped (resources/memory-
224
+ # reflect-lib.ts buildStagedCandidateRow) when the ruling arrived WITH a
225
+ # non-empty team-relevance justification. Promotion (resources/auto-promote-
226
+ # lib.ts decidePromotedVisibility) re-verifies all three conditions
227
+ # (continuity scopeTag + this ruling + visibilityRationale) and defaults
228
+ # "private" otherwise — default-private-unless (Sherlock): a shared promoted
229
+ # row must always trace to a recorded justification, never to a default.
230
+ # Nullable/additive — pre-slice-3 candidates read null ⇒ private, unchanged.
231
+ visibilityRationale: String # flair#1257 slice 3: the team-relevance justification paired with
232
+ # visibilityRuling — the auditable "why shared" recorded on the candidate.
233
+ # Nullable/additive, same clean-upgrade contract as scopeTag above.
222
234
  }
@@ -25,4 +25,11 @@ if [ -n "${FLAIR_ADMIN_PASS_FILE:-}" ] && [ -f "${FLAIR_ADMIN_PASS_FILE}" ]; the
25
25
  set -- "$@" --admin-pass-file "${FLAIR_ADMIN_PASS_FILE}"
26
26
  fi
27
27
 
28
- exec "{{FLAIR_BIN}}" federation sync "$@"
28
+ # Run the CLI under node rather than exec-ing it directly: `node <script>`
29
+ # needs READ permission only, so the shim keeps working when a deploy method
30
+ # (npm-pack tarball extraction) strips the exec bit from the script (#1231).
31
+ # NODE_BIN is an ABSOLUTE path resolved at enable time — the shim performs
32
+ # zero PATH lookups at run time, exactly like the old absolute-FLAIR_BIN
33
+ # form. Do not replace it with a bare `node`: that would hand binary
34
+ # selection to whatever PATH the service manager happens to carry.
35
+ exec "{{NODE_BIN}}" "{{FLAIR_BIN}}" federation sync "$@"
@@ -5,5 +5,12 @@
5
5
  #
6
6
  # Single line that invokes the CLI's run-once subcommand — the runner module
7
7
  # does all the work. Logs land in {{HOME}}/.flair/logs/.
8
+ #
9
+ # Run the CLI under node rather than exec-ing it directly: `node <script>`
10
+ # needs READ permission only, so the shim keeps working when a deploy method
11
+ # (npm-pack tarball extraction) strips the exec bit from the script (#1231).
12
+ # NODE_BIN is an ABSOLUTE path resolved at enable time — the shim performs
13
+ # zero PATH lookups at run time. Do not replace it with a bare `node`: that
14
+ # would hand binary selection to whatever PATH the service manager carries.
8
15
  set -e
9
- exec {{FLAIR_BIN}} rem nightly run-once
16
+ exec "{{NODE_BIN}}" "{{FLAIR_BIN}}" rem nightly run-once