@tpsdev-ai/flair 0.26.0 → 0.27.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.
@@ -0,0 +1,132 @@
1
+ # Native /mcp OAuth surface — Model 2 (experimental, default-OFF)
2
+
3
+ > **Status: experimental, opt-in, DEFAULT-OFF.** Gated behind `FLAIR_MCP_OAUTH`.
4
+ > When the flag is unset, flair boots byte-identically to before — no `/mcp`
5
+ > route, no authorization-server load, no change to the default auth chain.
6
+ > Do NOT enable in production until Sherlock signs off on live enablement.
7
+
8
+ This is the **Model 2** native-MCP path: a custom in-process `/mcp` JSON-RPC
9
+ handler guarded by `@harperfast/oauth`'s `withMCPAuth`, serving the 9 curated
10
+ flair tools with a per-agent OAuth identity. It is distinct from the
11
+ native-application-MCP surface (design A / `FLAIR_MCP_ENABLED`); Model 2 does not
12
+ use Harper's native MCP transport, so it is not blocked by the Harper native-MCP
13
+ gating gaps.
14
+
15
+ ## What it is
16
+
17
+ - `resources/mcp-handler.ts` — a minimal MCP (JSON-RPC 2.0) handler over
18
+ Streamable HTTP: `initialize` / `tools/list` / `tools/call` / `ping`. On
19
+ `tools/call` it resolves the verified token `sub` → a flair `Agent`, then
20
+ dispatches to the curated tool.
21
+ - `resources/mcp-tools.ts` — the 9 curated tools (memory_search, memory_store,
22
+ memory_get, memory_delete, bootstrap, soul_set, soul_get, flair_workspace_set,
23
+ flair_orgevent), each a thin wrapper over the existing resource handler
24
+ (Memory / SemanticSearch / BootstrapMemories / Soul / WorkspaceState /
25
+ OrgEvent). No raw CRUD surface — the only path to the datastore through `/mcp`
26
+ is one of these 9 semantic tools. Curated **by construction**.
27
+ - `resources/mcp-oauth.ts` — registers `server.http(withMCPAuth(mcpHandler),
28
+ { urlPath: '/mcp' })` **only when `FLAIR_MCP_OAUTH` is on.** `/mcp` runs on its
29
+ own dispatch chain; flair's default auth-middleware does not run for it.
30
+ - `resources/mcp-oauth-flag.ts` — the flag + issuer/resource config helpers.
31
+
32
+ ## The sub → Agent mapping (identity)
33
+
34
+ `withMCPAuth` verifies the RS256 JWT and sets `request.mcp = { sub, client_id,
35
+ aud, scope }`. The handler maps `sub` → a flair `Agent` id:
36
+
37
+ 1. Look up `Credential` where `kind === "idp"` AND `idpSubject === sub` → its
38
+ `principalId` is the Agent id. (Same credential surface XAA's ID-JAG path
39
+ uses — one identity model.)
40
+ 2. If no mapping and `FLAIR_MCP_JIT_PROVISION` is on, JIT-provision a
41
+ non-admin `Agent` + `Credential(kind:"idp")` from the sub.
42
+ 3. Otherwise **deny** — an unresolvable sub never runs as anonymous or admin.
43
+
44
+ The resolved agent is set as `request.tpsAgent` on a flair-shaped delegation
45
+ context, so the wrapped handler scopes to the verified agent exactly as an
46
+ Ed25519-signed REST call would. Identity always comes from the resolved agent,
47
+ never from the tool arguments (no forging of agentId / authorId).
48
+
49
+ ## Enabling (operator checklist)
50
+
51
+ 1. **Install the AS plugin** — add `@harperfast/oauth` (already an exact-pinned
52
+ dependency) and declare it in `config.yaml`:
53
+
54
+ ```yaml
55
+ '@harperfast/oauth':
56
+ package: '@harperfast/oauth'
57
+ providers:
58
+ github:
59
+ clientId: ${OAUTH_GITHUB_CLIENT_ID}
60
+ clientSecret: ${OAUTH_GITHUB_CLIENT_SECRET}
61
+ mcp:
62
+ enabled: true
63
+ issuer: ${FLAIR_MCP_ISSUER} # pin to your public origin — REQUIRED
64
+ resource: ${FLAIR_MCP_ISSUER}/mcp # RFC-8707 audience the /mcp token binds to
65
+ accessTokenTtl: 900 # 5–15 min (Sherlock req 1) — short-lived
66
+ dynamicClientRegistration:
67
+ enabled: false # DCR is NOT SUPPORTED (flair#756) — explicit, not omitted (an absent block leaves DCR OPEN by the plugin's own default)
68
+ clientIdMetadataDocuments:
69
+ allowedHosts: # CIMD is the only supported client-registration path
70
+ - claude.ai
71
+ - claude.com
72
+ signingKeyPem: ${FLAIR_MCP_SIGNING_KEY_PEM} # pin in clusters
73
+ ```
74
+
75
+ **DCR is not supported; clients connect via CIMD (Client ID Metadata
76
+ Documents).** `flair mcp enable` (flair#756) writes exactly this shape —
77
+ see "Legacy clients" below.
78
+
79
+ The `config.yaml` block is intentionally NOT committed to the live config in
80
+ this slice — adding it changes boot behavior, which would break the
81
+ default-OFF / byte-identical contract. An operator adds it deliberately when
82
+ turning the surface on.
83
+
84
+ 2. **Set the env:**
85
+ - `FLAIR_MCP_OAUTH=1` — turns on the `/mcp` route registration.
86
+ - `FLAIR_MCP_ISSUER=https://your-public-origin` (or `FLAIR_PUBLIC_URL`).
87
+ - `FLAIR_MCP_JIT_PROVISION=1` — ONLY if you want unknown subjects
88
+ auto-provisioned (default OFF; pre-provision Agent+Credential otherwise).
89
+
90
+ 3. **Restart flair.** `/mcp` mounts, OAuth-guarded.
91
+
92
+ ## Sherlock's 4 requirements — how they are met
93
+
94
+ 1. **Token lifetime.** `mcp.accessTokenTtl: 900` (5–15 min) in the AS config +
95
+ the standard OAuth refresh flow (the plugin rotates refresh tokens on use).
96
+ `withMCPAuth` validates `exp` strictly. Documented as a required config value,
97
+ not a default (the plugin's own default is 1h — too long).
98
+ 2. **RS256 pinning.** The `@harperfast/oauth` plugin mints and verifies RS256-only
99
+ (per its docs: "Signing algorithms other than RS256 … are not supported"), so
100
+ `none`/HS256 confusion is structurally rejected — the verifier only knows
101
+ RS256. No configurable `alg` to widen.
102
+ 3. **Dual-auth precedence.** `/mcp` is OAuth-only on its own urlPath chain;
103
+ flair's default chain (Ed25519) never runs for `/mcp`, and the OAuth Bearer
104
+ never reaches the default chain. They cannot collide on the same request —
105
+ `/mcp` sees only the token, every other path sees only Ed25519/Basic. There is
106
+ no path that carries both.
107
+ 4. **Client registration.** DCR is not supported; clients connect via CIMD
108
+ (Client ID Metadata Documents) — `mcp.dynamicClientRegistration.enabled:
109
+ false` explicitly closes RFC 7591 registration (open DCR would let an
110
+ attacker register as any agent; leaving the block unset does NOT close it —
111
+ see `src/lib/mcp-enable.ts`'s module header for the ground-truth citation),
112
+ and `mcp.clientIdMetadataDocuments.allowedHosts` restricts which hosts may
113
+ present a CIMD client_id URL. On the resolution side, JIT-provisioning of
114
+ an unknown sub is itself gated (`FLAIR_MCP_JIT_PROVISION`, default OFF) —
115
+ a second explicit trust anchor.
116
+
117
+ ## Legacy clients
118
+
119
+ DCR (RFC 7591 Dynamic Client Registration) is UNSUPPORTED on this surface —
120
+ not a fallback, not a flag. `flair mcp enable` (flair#756) writes
121
+ `dynamicClientRegistration: { enabled: false }`, which 404s
122
+ `/oauth/mcp/register`. A client that cannot present a CIMD client_id URL
123
+ cannot connect to this surface.
124
+
125
+ ## Deferred (not in this slice)
126
+
127
+ - Live `config.yaml` wiring of the `@harperfast/oauth` plugin (kept out to
128
+ preserve the byte-identical flag-OFF contract; documented above for operators).
129
+ - Migrating the homegrown `OAuth.ts` / `XAA.ts` opaque-token AS to the plugin.
130
+ Per Kern: deprecate-don't-delete — they stay for the Ed25519/signed-REST path.
131
+ XAA's JIT-provisioning is kept; the Model-2 handler reuses the same
132
+ `Credential(kind:"idp")` surface.
@@ -0,0 +1,39 @@
1
+ # REM UX — trigger model, attach semantics, review loop
2
+
3
+ > Design note accompanying REM slice 2 (#707). Describes the intended user experience of in-process distillation so the CLI/docs surfaces stay coherent as the feature grows. Parent spec: `specs/FLAIR-NIGHTLY-REM.md`; slice spec: `specs/FLAIR-NIGHTLY-REM-SLICE-2-DISTILLATION.md`.
4
+
5
+ ## Triggers — three, nothing implicit
6
+
7
+ 1. **Interactive:** an agent (or a human at the CLI) runs `flair rem rapid` — one bounded distillation of that agent's own recent memories. Non-admin actors can only reflect on themselves; an admin can run it for any agent.
8
+ 2. **Scheduled:** the nightly cycle, per agent, only where deliberately enabled (`flair rem nightly enable`). In a multi-node deploy, exactly one node gets the timer (see #709).
9
+ 3. **Admin-run:** same endpoint, another agent's corpus, admin credentials.
10
+
11
+ REM never fires as a side effect of writes, searches, or boot. Distillation is always a deliberate act with an audit trail.
12
+
13
+ ## Locality — the model call runs next to the data
14
+
15
+ The CLI is a thin HTTP trigger pointed at whatever Flair instance it is configured for (local or remote). Distillation executes **server-side** in the Flair component via Harper's model-access API. Consequences:
16
+
17
+ - Memory content never transits the operator's machine on its way to a model.
18
+ - Data egress is determined solely by the *server's* `models:` configuration — a local backend means nothing leaves the box; a hosted provider is an explicit, documented choice (keys ride Harper env-secret encryption on managed deploys).
19
+ - The same trigger UX works identically against a laptop instance and a managed deployment.
20
+
21
+ ## Attach semantics — synchronous now, and the rule for when that changes
22
+
23
+ **Interactive mode stays attached.** One request, one bounded generate call (gather cap 50 memories, bounded output tokens), a staged-candidate summary printed on return. Seconds, not minutes — request/response is the honest shape for it, and `--prompt-only` preserves the bring-your-own-model handoff.
24
+
25
+ **Nightly mode is fully detached.** The scheduler fires the cycle; results land as pending candidates plus an audit row; the operator reviews next morning.
26
+
27
+ **The rule for revisiting:** any run shape that exceeds a single bounded generate call — hierarchical/recursive consolidation, org-level REM across many agents — gets a real job model (run id, `flair rem status <id>`, resumability) *designed before that slice ships*. The failure mode to avoid is a timeout bug forcing an ad-hoc job system into existence.
28
+
29
+ ## The review loop — where REM's UX actually lives
30
+
31
+ Distillation is the cheap half; the reviewable stream of candidates is the product surface:
32
+
33
+ - **Today (CLI):** `flair rem candidates` lists pending rows; `flair rem promote <id> --rationale` / `flair rem reject <id> --reason` decide them. Every candidate carries its claim, source memory ids, generating model, and timestamp — enough to judge provenance at a glance.
34
+ - **Direction:** a reviewable feed — candidates as a stream with one-glance provenance and one-action promote/reject, recurring rejected claims surfaced via their `supersedes` chains rather than appearing fresh each time. The nightly diff report should point at that surface, making morning review a two-minute ritual instead of a CLI expedition.
35
+ - **Invariant either way:** nothing self-promotes. The feed can get faster and friendlier; the explicit-decision gate is load-bearing and stays.
36
+
37
+ ## Configuration UX — one decision, one knob
38
+
39
+ Wanting REM should reduce to: *point the server's `models:` block at a backend.* Local Ollama works with zero credentials; a hosted provider takes an API key stored as an encrypted env secret. `FLAIR_REM_MODEL` selects a logical model name when the default routing isn't right. There is no REM-specific provider code, wiring, or plugin to install — if the server can `generate()`, REM works.
@@ -0,0 +1,131 @@
1
+ # Flair + OpenClaw
2
+
3
+ Give OpenClaw agents persistent memory and identity.
4
+
5
+ ## Setup
6
+
7
+ ### 1. Install Flair (if not already running)
8
+
9
+ ```bash
10
+ npm install -g @tpsdev-ai/flair
11
+ flair init
12
+ ```
13
+
14
+ ### 2. Install the OpenClaw plugin
15
+
16
+ ```bash
17
+ openclaw plugins install @tpsdev-ai/openclaw-flair
18
+ ```
19
+
20
+ ### 3. Create an agent identity
21
+
22
+ ```bash
23
+ flair agent add my-agent
24
+ ```
25
+
26
+ ### 4. Configure the plugin
27
+
28
+ In your OpenClaw agent config, add the Flair plugin:
29
+
30
+ ```json
31
+ {
32
+ "plugins": {
33
+ "@tpsdev-ai/openclaw-flair": {
34
+ "agentId": "my-agent",
35
+ "flairUrl": "http://localhost:19926"
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ Or set environment variables:
42
+
43
+ ```bash
44
+ export FLAIR_AGENT_ID=my-agent
45
+ export FLAIR_URL=http://localhost:19926
46
+ ```
47
+
48
+ ### 5. Restart the gateway
49
+
50
+ ```bash
51
+ openclaw gateway restart
52
+ ```
53
+
54
+ ## What the Plugin Provides
55
+
56
+ The Flair plugin adds these tools to your OpenClaw agent:
57
+
58
+ | Tool | Description |
59
+ |------|-------------|
60
+ | `memory_store` | Write a memory with optional type, durability, and tags |
61
+ | `memory_recall` | Semantic search over stored memories |
62
+ | `memory_get` | Retrieve a specific memory by ID |
63
+
64
+ ### Automatic Bootstrap
65
+
66
+ On each new conversation, the plugin injects relevant context from Flair:
67
+ - Soul entries (persistent personality and project context)
68
+ - Recent memories (last 24h)
69
+ - Relevant memories (semantically matched to the conversation topic)
70
+
71
+ This happens automatically — no agent configuration needed beyond the plugin setup.
72
+
73
+ ## Multi-Agent
74
+
75
+ Each OpenClaw agent gets its own isolated memory space:
76
+
77
+ ```bash
78
+ flair agent add research-agent
79
+ flair agent add coding-agent
80
+ flair agent add review-agent
81
+ ```
82
+
83
+ Agents can share memories via grants:
84
+
85
+ ```bash
86
+ # Let review-agent read coding-agent's memories
87
+ flair grant coding-agent review-agent --scope read
88
+ ```
89
+
90
+ ## Soul (Personality)
91
+
92
+ Set persistent context that shapes how the agent behaves:
93
+
94
+ ```bash
95
+ flair soul set --agent my-agent --key role \
96
+ --value "Senior engineer focused on reliability. Ship quality over speed."
97
+
98
+ flair soul set --agent my-agent --key project \
99
+ --value "E-commerce API. Node.js, PostgreSQL. 200K DAU."
100
+ ```
101
+
102
+ Soul entries are included in every bootstrap — they're the agent's persistent identity.
103
+
104
+ ## Key Resolution
105
+
106
+ The plugin resolves Ed25519 keys in this order:
107
+ 1. `FLAIR_KEY_PATH` environment variable
108
+ 2. `~/.flair/keys/<agent-id>.key`
109
+ 3. `~/.tps/secrets/flair/<agent-id>-priv.key` (legacy TPS path)
110
+
111
+ ## Troubleshooting
112
+
113
+ ```bash
114
+ # Verify Flair is running
115
+ flair status
116
+
117
+ # Verify the agent exists
118
+ flair agent list
119
+
120
+ # Test memory roundtrip
121
+ flair memory add --agent my-agent --content "test memory"
122
+ flair search "test" --agent my-agent
123
+
124
+ # Check plugin is loaded
125
+ openclaw plugins list
126
+ ```
127
+
128
+ If the plugin fails to load, check the gateway logs for Flair connection errors. Common issues:
129
+ - Wrong port (default changed to 19926 in v0.4.0)
130
+ - Agent not registered (`flair agent add <id>`)
131
+ - Key file missing (`~/.flair/keys/<agent-id>.key`)
@@ -0,0 +1,153 @@
1
+ # Quick Start
2
+
3
+ From zero to a persistent agent memory in five minutes.
4
+
5
+ ## 0. Prerequisite
6
+
7
+ **Node.js 22 or newer.** That's it — no Docker, no database to install, no API keys. Flair runs in a single process with embeddings computed locally.
8
+
9
+ ```bash
10
+ node --version # v22.x.x or higher
11
+ ```
12
+
13
+ If you need Node 22: [nodejs.org](https://nodejs.org/) or `brew install node@24`.
14
+
15
+ ## 1. Install (30 seconds)
16
+
17
+ ```bash
18
+ npm install -g @tpsdev-ai/flair
19
+ ```
20
+
21
+ `flair`, `flair-mcp`, and the client library all come in under one install.
22
+
23
+ ## 2. Bootstrap Flair (1–2 minutes)
24
+
25
+ ```bash
26
+ flair init
27
+ ```
28
+
29
+ First run does a few things:
30
+
31
+ 1. Installs the embedded Harper (memory store) into `~/.flair/data/`.
32
+ 2. Downloads the local embedding model (~80 MB — first run only).
33
+ 3. Starts Flair as a launchd / systemd service on port 19926.
34
+ 4. Creates a default agent (`--agent-id local` unless you pass one).
35
+ 5. Opens a short **soul wizard** so your agent knows who it is.
36
+
37
+ The soul wizard offers a few shapes:
38
+
39
+ ```
40
+ 🎭 Agent personality setup
41
+ Soul entries shape what every future session starts with.
42
+
43
+ What best describes this agent?
44
+ (1) Solo developer — helps you with code on this machine
45
+ (2) Team agent — runs in a shared repo / ops flow
46
+ (3) Research assistant — surveys sources, writes notes
47
+ (4) Draft from Claude — paste a Claude-generated JSON draft
48
+ (5) Custom — I'll prompt for each field with examples
49
+ (s) Skip — set up later; `flair doctor` will nudge
50
+ ```
51
+
52
+ Pick the template that matches how you'll use this agent. You can edit or replace any entry later with `flair soul set`.
53
+
54
+ ## 3. Confirm it's running
55
+
56
+ ```bash
57
+ flair status
58
+ ```
59
+
60
+ A bare `flair status` (no agent, no admin credentials) only shows the public health
61
+ summary:
62
+
63
+ ```
64
+ Flair vX.Y.Z — 🟢 running (PID 12345, uptime 1m)
65
+ URL: http://127.0.0.1:19926
66
+
67
+ ✓ all checks passing
68
+ ```
69
+
70
+ The **🟢** icon means everything is healthy. A **🟡** would mean there's something worth looking at — usually surfaced with a recommended command inline. See [docs/troubleshooting.md](troubleshooting.md) if you see **🔴 unreachable**.
71
+
72
+ For the full picture — Memory, Agents, and Soul detail — pass the agent you just created:
73
+
74
+ ```bash
75
+ flair status --agent local
76
+ ```
77
+
78
+ ```
79
+ Flair vX.Y.Z — 🟢 running (PID 12345, uptime 1m)
80
+ URL: http://127.0.0.1:19926
81
+
82
+ Memory:
83
+ Total: 0
84
+ Durability: 0 permanent / 0 persistent / 0 standard / 0 ephemeral
85
+
86
+ Agents:
87
+ 1 total — local
88
+
89
+ Soul:
90
+ 3 entries — 0 critical / 0 high / 3 standard / 0 low
91
+
92
+ ✓ all checks passing
93
+ ```
94
+
95
+ ## 4. Write your first memory
96
+
97
+ ```bash
98
+ flair memory add --agent local --content "Harper v5 sandbox blocks node:module but process.dlopen works"
99
+ ```
100
+
101
+ Flair generates a semantic embedding locally and stores it. No network calls.
102
+
103
+ ## 5. Find it back by meaning
104
+
105
+ ```bash
106
+ flair memory search --agent local --q "native addon loading in sandboxed runtimes"
107
+ ```
108
+
109
+ ```
110
+ Harper v5 sandbox blocks node:module but process.dlopen works
111
+ (2026-04-22 · memory · 67%)
112
+ ```
113
+
114
+ **You searched for a concept, not the keywords.** The 67% is the semantic-similarity score.
115
+
116
+ ## 6. Give your agent context on boot
117
+
118
+ ```bash
119
+ flair bootstrap --agent local --max-tokens 2000
120
+ ```
121
+
122
+ This returns a formatted text block that includes the soul entries you just set plus recent/relevant memories. Paste that into any LLM session — Claude Code, Codex, Cursor, an Anthropic API call — to give the agent its identity + memory in one shot.
123
+
124
+ If you use Claude Code, add this to your `CLAUDE.md`:
125
+
126
+ ```
127
+ At the start of every session, run mcp__flair__bootstrap before responding.
128
+ ```
129
+
130
+ With the MCP server wired up (see the [integration section in README.md](../README.md#integration)), Claude Code runs bootstrap automatically on every new session.
131
+
132
+ ## What's next
133
+
134
+ | You want to... | Go to |
135
+ |----------------|-------|
136
+ | Add more agents to the same instance | `flair agent add <id>` |
137
+ | Let one agent read another's memories | `flair grant <from> <to>` ([docs/auth.md](auth.md)) |
138
+ | Import memories from agentic-stack / Mem0 / etc. | [docs/bridges.md](bridges.md) |
139
+ | Sync memories across machines | [docs/federation.md](federation.md) |
140
+ | Integrate with OpenClaw, Claude Code, Cursor | [README.md#integration](../README.md#integration) |
141
+ | Fix something that isn't working | [docs/troubleshooting.md](troubleshooting.md) |
142
+ | Upgrade to a new version | `flair upgrade --restart` or [docs/upgrade.md](upgrade.md) |
143
+
144
+ ## If you change your mind
145
+
146
+ ```bash
147
+ flair stop # stop the service (keeps data)
148
+ flair restart # restart
149
+ flair uninstall # remove the service (keeps data + keys)
150
+ flair uninstall --purge # remove everything including data and keys
151
+ ```
152
+
153
+ All reversible. Your memories aren't locked in.
@@ -0,0 +1,173 @@
1
+ # Releasing Flair
2
+
3
+ Flair publishes eight workspace packages to npm under `@tpsdev-ai/*`. Releases are
4
+ **tokenless** and **staged**: CI authenticates to npm with a short-lived OIDC token
5
+ (no `NPM_TOKEN` lives anywhere) and submits each package to npm's **staging** area.
6
+ A maintainer then approves the staged tarballs on npmjs.com with 2FA to make them live.
7
+
8
+ > `flair-bench` is version-bumped and tagged in lockstep with the other 7, but stages
9
+ > in its own step in CI (allowed to fail) until its one-time bootstrap is done — see
10
+ > [flair-bench bootstrap](#flair-bench-bootstrap-one-time) below.
11
+
12
+ ```
13
+ merge release PR ──▶ push tag v0.11.0 ──▶ CI stages all packages ──▶ npm staging
14
+
15
+ maintainer reviews + approves (2FA)
16
+
17
+ live on npm
18
+ ```
19
+
20
+ Pushing a `vX.Y.Z` tag triggers the release. This replaces the old "run
21
+ `release.sh --publish` from a laptop logged into npm" flow. Nothing publishes without
22
+ a human 2FA approval, and every package ships with a provenance attestation (public
23
+ repo → verifiable build origin). The person who tags the release does **not** need npm
24
+ credentials or `Actions: write` — only repo push access; the only privileged step is
25
+ the maintainer's 2FA approval.
26
+
27
+ ## Cutting a release
28
+
29
+ ### Phase 1 — open the release PR
30
+
31
+ ```bash
32
+ # promote ## Unreleased to the new version in CHANGELOG.md first, then:
33
+ ./scripts/release.sh 0.11.0
34
+ ```
35
+
36
+ This bumps every workspace package to the version, aligns internal deps, refreshes
37
+ `bun.lock`, builds, tests, and opens a `release: v0.11.0` PR. Review and merge it
38
+ (CI green + K&S approval) the same as any other PR.
39
+
40
+ ### Phase 2 — tag the release
41
+
42
+ After the release PR is merged to `main`, push the version tag:
43
+
44
+ ```bash
45
+ git checkout main && git pull
46
+ git tag v0.11.0 && git push origin v0.11.0
47
+ ```
48
+
49
+ The tag push triggers the [`release-publish`](../.github/workflows/release-publish.yml)
50
+ workflow, which:
51
+
52
+ 1. Resolves the version from the tag and validates it as semver.
53
+ 2. Verifies the tagged commit is an ancestor of `main` (a tag can't ship un-merged code).
54
+ 3. Verifies all 7 `package.json` files are at that version.
55
+ 4. Builds every package.
56
+ 5. Runs `npm stage publish` for each package in dependency order (flair-client first).
57
+
58
+ It authenticates via OIDC — no secrets, and it does **not** create or move any tag (the
59
+ tag you pushed is the trigger). Watch the run; when it's green, the packages are staged
60
+ but **not yet live**.
61
+
62
+ In parallel — and **independent of the npm staging approval** — a `github-release` job
63
+ auto-cuts a [GitHub release](https://github.com/tpsdev-ai/flair/releases) for the tag,
64
+ using the matching `## [X.Y.Z]` section of `CHANGELOG.md` as the release notes (extracted
65
+ by `scripts/changelog-extract.mjs`). It is idempotent: re-running the workflow or
66
+ re-pushing the tag updates the existing release rather than failing. The GitHub release
67
+ documents the tagged commit immediately; it does not wait on the npm 2FA gate. If the
68
+ CHANGELOG has no section for the version, this job fails loudly rather than cutting an
69
+ empty release — so always promote `## [Unreleased]` to `## [X.Y.Z]` in the release PR.
70
+
71
+ > `workflow_dispatch` with a `version` input remains as a manual fallback (needs
72
+ > `Actions: write`), but the tag push is the normal path.
73
+
74
+ ### Phase 3 — approve the staged packages
75
+
76
+ Go to **[npmjs.com → tpsdev-ai → Staged Packages](https://www.npmjs.com/settings/tpsdev-ai/staging)**,
77
+ review each staged tarball, and approve with 2FA. Or from a machine logged into npm:
78
+
79
+ ```bash
80
+ npm stage list # show staged packages + their stage-ids
81
+ npm stage view <stage-id> # inspect one
82
+ npm stage approve <stage-id> # 2FA prompt; package goes live
83
+ ```
84
+
85
+ There are seven lockstep-staged packages, so seven approvals (the web UI lists them on
86
+ one page). Approve in dependency order if installing immediately — flair-client before
87
+ its dependents — though staging does not itself resolve dependencies. `flair-bench`
88
+ stages separately and only appears here once its bootstrap (below) is done.
89
+
90
+ Verify when done:
91
+
92
+ ```bash
93
+ npm view @tpsdev-ai/flair version # should report the new version
94
+ ```
95
+
96
+ ## One-time setup
97
+
98
+ These are configured once and reused for every release.
99
+
100
+ ### npm trusted publisher (per package)
101
+
102
+ For **each** of the seven packages, on npmjs.com → the package → **Settings → Trusted
103
+ Publisher → Add**:
104
+
105
+ | Field | Value |
106
+ | --------------- | --------------------------- |
107
+ | Provider | GitHub Actions |
108
+ | Organization | `tpsdev-ai` |
109
+ | Repository | `flair` |
110
+ | Workflow | `release-publish.yml` |
111
+ | Environment | `release` |
112
+ | Allowed actions | **`npm stage publish` only** |
113
+
114
+ Leave `npm publish` **unchecked** under allowed actions. This structurally prevents the
115
+ CI/OIDC identity from publishing anything live directly — the only path to live is the
116
+ human 2FA approval of a staged package.
117
+
118
+ Packages: `flair-client`, `flair-mcp`, `flair`, `openclaw-flair`, `pi-flair`,
119
+ `n8n-nodes-flair`, `langgraph-flair`.
120
+
121
+ > A package must already exist on npm before a trusted publisher can be added — all
122
+ > seven already do. This account-level config can only be done by an npm org owner.
123
+ > `flair-bench` doesn't exist on npm yet (verified via `npm view @tpsdev-ai/flair-bench`
124
+ > → 404 as of 2026-07-13) so it can't have a Trusted Publisher yet either — see below.
125
+
126
+ ### `flair-bench` bootstrap (one-time)
127
+
128
+ `flair-bench` (added 2026-07-12, flair#702) is wired into the version-bump/tag flow
129
+ (`scripts/release.sh`, `release-publish.yml`'s version-check) alongside the other 7, but
130
+ `npm stage publish` categorically requires the package already exist on the npm registry
131
+ (`npm help stage`: "Package must exist"), and a Trusted Publisher can only be registered
132
+ for a package that already exists — chicken-and-egg for a brand-new package. Until an npm
133
+ org owner does this once, the workflow's dedicated "Stage-publish flair-bench" step is
134
+ expected to fail (it's `continue-on-error: true` so it doesn't block the other 7):
135
+
136
+ 1. From a machine logged into npm with 2FA: `cd packages/flair-bench && npm run build &&
137
+ npm publish --access public` — one normal (non-staged) publish to create the package
138
+ on the registry. Any valid semver works; the very next lockstep release will bump it
139
+ to match the other 7 automatically (it's in `scripts/release.sh`'s `PACKAGES` array).
140
+ 2. Add the Trusted Publisher for `@tpsdev-ai/flair-bench` using the same table as the
141
+ other 7 above (org=`tpsdev-ai`, repo=`flair`, workflow=`release-publish.yml`,
142
+ environment=`release`, allowed=`npm stage publish` only).
143
+ 3. Remove `continue-on-error: true` from the "Stage-publish flair-bench" step in
144
+ `release-publish.yml` once a tag push has staged it successfully.
145
+
146
+ ### GitHub `release` environment
147
+
148
+ A repository environment named `release` scopes the OIDC trust. It has **no required
149
+ reviewers** — the human gate is the npm staging approval, not a GitHub deployment
150
+ review. Because the release is triggered by a tag push, its deployment policy must allow
151
+ **`v*` tags** (Settings → Environments → `release` → Deployment branches and tags →
152
+ Selected branches and tags → add tag rule `v*`).
153
+
154
+ ### Approver 2FA
155
+
156
+ The maintainer who approves staged packages must have 2FA enabled on their npm account.
157
+
158
+ ## If something goes wrong
159
+
160
+ - **A staged package looks wrong** — reject it on npmjs.com instead of approving; it
161
+ never goes live. Fix forward on `main` and cut a new patch version.
162
+ - **Re-run the stage for the same version** — delete and re-push the tag
163
+ (`git push origin :v0.11.0` then `git tag -f v0.11.0 && git push origin v0.11.0`).
164
+ The tag push re-triggers the workflow.
165
+ - **Break-glass (CI down):** `./scripts/release.sh X.Y.Z --publish` still works from a
166
+ machine logged into npm. Prefer the staged flow; this bypasses the staging gate.
167
+
168
+ ## Requirements
169
+
170
+ - npm CLI **≥ 11.15.0** (`npm stage`) and **≥ 11.5.1** (OIDC) — the workflow upgrades
171
+ npm itself; local approvers need a recent npm.
172
+ - Node **≥ 22.14**.
173
+ - Trusted publishing runs on GitHub-hosted runners only (no self-hosted support yet).