flowviant 0.52.0 → 0.54.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.
package/README.md CHANGED
@@ -1,15 +1,18 @@
1
1
  # flowviant
2
2
 
3
- Run your own [Claude Code](https://claude.com/claude-code) as headless build agents for [Flowviant](https://flowviant.com). You manage a team of agents in the app; this daemon runs them on your machine, on your own credentials — Flowviant never sees your Claude or GitHub logins.
3
+ Run your own coding CLIs as build agents for [Flowviant](https://flowviant.com) [Claude Code](https://claude.com/claude-code), Codex or Antigravity, on your own credentials. This daemon holds your sessions, keeps a worktree per tab, and ships branches on your word. Flowviant never sees your Claude, Codex or GitHub logins.
4
4
 
5
5
  ```bash
6
- npx flowviant login # approve the code in Flowviant → connected
7
- npx flowviant # run your fleet
6
+ npx flowviant@latest login # approve the code in Flowviant → connected
8
7
  ```
9
8
 
9
+ Login keeps going straight into the daemon — there is no second command to run.
10
+
10
11
  ## What it does
11
12
 
12
- You create named agents in Flowviant and dispatch work to them. This daemon, running on a machine you control, gives each agent its own git worktree and drives **your** locally-authenticated `claude` to do the work it claims a task, works it, captures evidence for each acceptance criterion, opens a pull request, and routes any question it can't answer back to you as a blocker. You review and merge in the app.
13
+ A Flowviant project has ONE machine, and this is it: one box, running one Claude (or Codex, or Antigravity) account, serving the whole team's sessions so nobody has to set up their own. Solo and team are the same architecture at N=1 and N>1.
14
+
15
+ You work in the **Workbench**, where your sessions are TABS — each one a held context plus a persistent git worktree on its own `session/<id>` branch. The browser is a terminal projected onto this machine, so you reach the same session from any device. When you say ship, the tab merges its own branch into base with `--no-ff` — no squash, so the commit shas the work reported still exist on main.
13
16
 
14
17
  Because it drives the CLIs you're already logged into, **the cost is yours** (your Claude subscription, your GitHub) and **the daemon never handles a credential** — it shells out to tools you authenticated yourself.
15
18
 
@@ -17,9 +20,9 @@ Because it drives the CLIs you're already logged into, **the cost is yours** (yo
17
20
 
18
21
  On the machine that runs the daemon:
19
22
 
20
- - **[Claude Code](https://claude.com/claude-code)** installed and signed in (`claude`)
21
- - **[GitHub CLI](https://cli.github.com)** authenticated (`gh auth login`) — for opening PRs
23
+ - **at least one coding CLI** installed and signed in — [Claude Code](https://claude.com/claude-code) (`claude`), Codex (`codex`) or Antigravity (`agy`)
22
24
  - **git**, and **Node 20+**
25
+ - **[GitHub CLI](https://cli.github.com)** (`gh`) — optional; the daemon offers to fetch an isolated copy, and `flowviant gh-auth` signs it in
23
26
  - run it from inside the git repository you want worked
24
27
 
25
28
  ## Connecting
@@ -27,53 +30,44 @@ On the machine that runs the daemon:
27
30
  The easy way — device login, like `gh auth login`:
28
31
 
29
32
  ```bash
30
- npx flowviant login
33
+ npx flowviant@latest login
31
34
  ```
32
35
 
33
- It shows a short code; enter it in Flowviant under **Agents Connect a machine**. The credential is stored at `~/.flowviant/credentials.json`, and from then on `npx flowviant` just runs.
36
+ It shows a short code. Open your project's **Workbench** in Flowviant and enter the code where it offers to connect a machine. The credential is stored at `~/.flowviant/credentials.json`, and from then on `npx flowviant@latest` just runs.
34
37
 
35
- Prefer an explicit token? Create a fleet credential in the app and pass it directly:
38
+ Prefer an explicit token? Create a machine credential in the app and pass it directly:
36
39
 
37
40
  ```bash
38
- FLOWVIANT_FLEET=fva_… npx flowviant
41
+ FLOWVIANT_FLEET=fva_… npx flowviant@latest
39
42
  ```
40
43
 
41
- ## Live mode (the default)
42
-
43
- Each task runs a **persistent** Claude session you can talk to mid-task from the app: the agent streams its work into the task's conversation, you `@`-mention it to steer or answer questions, and it resumes in place. Blockers park the session at zero cost until you answer. When it finishes, it posts a delivery card (summary + checklist self-report) in the thread — a human confirms done by merging there.
44
+ Launch with `@latest` so each start pulls the newest published version — a bare `npx flowviant` can reuse a stale cache. A running daemon also self-updates at startup and when idle (`FLOWVIANT_NO_UPDATE=1` makes it nag-only; `flowviant update` updates now).
44
45
 
45
- Prefer the legacy one-shot poll mode (no streaming, no previews)? Escape hatch:
46
+ ## Sessions
46
47
 
47
- ```bash
48
- FLOWVIANT_POLL=1 npx flowviant
49
- ```
48
+ Each tab in the Workbench is a persistent Claude session with its own worktree, and it stays where you left it — the branch outlives the tab. The daemon runs each turn in event mode and relays what the CLI is printing (thinking, reads, greps, commands) back to the tab, reports the worktree's branch and diffstat after every turn, and fetches a commit's patch when you click a sha in the app.
50
49
 
51
- ### Live previews
50
+ Nothing starts work except you opening a tab and typing in it.
52
51
 
53
- For UI/API tasks, the daemon can start the branch's dev server in the agent's worktree and open a [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) quick tunnel so you can drive the real running change during review — no Cloudflare account needed (it's auto-fetched if missing). Configure it once per repo, or let it infer common setups:
52
+ ## Sharing a preview
54
53
 
55
- ```json
56
- // .flowviant/preview.json
57
- { "ui": { "cmd": "npm run dev", "port": 5173 } }
58
- ```
54
+ You run your dev server yourself, in the session's own worktree, exactly as you would in any terminal. The daemon NOTICES the listening port; ask for a share in the app and it puts a [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) quick tunnel in front of it (auto-fetched if missing, pinned and checksummed) behind a **mandatory password gate**. Flowviant stores only the tunnel URL; your browser talks to it directly.
59
55
 
60
- Flowviant only stores the tunnel URL; your browser talks to it directly.
56
+ The daemon never executes anything the repository declares. An earlier version read a `.flowviant/preview.json` from the branch and spawned the command it named — that start path was removed in 0.53.0 and is not coming back; see the header of `bin/lib/preview.mjs` for exactly what it did, so nobody rebuilds it.
61
57
 
62
58
  ## Modes
63
59
 
64
60
  | Env | What runs |
65
61
  | --- | --- |
66
- | _(stored login)_ or `FLOWVIANT_FLEET` | **Fleet daemon** — one worktree + worker per agent on your roster, managed in the app |
67
- | `FLOWVIANT_TOKEN` | a single agent in the current checkout |
68
- | `FLOWVIANT_TOKENS=a,b,c` | a static fleet, one worktree each |
62
+ | _(stored login)_ or `FLOWVIANT_FLEET` | **the daemon** — the project's machine, serving its sessions |
69
63
  | `FLOWVIANT_SAFE=1` | restrict the toolset instead of running unattended |
70
64
 
71
65
  ## Security posture
72
66
 
73
- Every project member with edit access can run turns on this machine —
74
- Workbench tabs and @-dispatches both execute a coding agent with the daemon's
75
- own OS permissions. Membership is the consent boundary, the same trust plane
76
- as the shared repository: invite people you would give a shell to.
67
+ Every project member with edit access can run turns on this machine — a
68
+ Workbench tab executes a coding agent with the daemon's own OS permissions.
69
+ Membership is the consent boundary, the same trust plane as the shared
70
+ repository: invite people you would give a shell to.
77
71
 
78
72
  Two knobs bound the blast radius, and both are worth setting on a shared box:
79
73
 
package/bin/cli.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * flowviant — run your own Claude Code as headless Flowviant build agents.
3
+ * flowviant — run your own coding CLI (Claude Code, Codex, Antigravity) as the
4
+ * machine behind a Flowviant project.
4
5
  *
5
6
  * ONE mode, one credential:
6
7
  *
7
- * FLOWVIANT_FLEET=fft_… npx flowviant@latest # the fleet daemon
8
+ * FLOWVIANT_FLEET=fva_… npx flowviant@latest # the machine daemon
8
9
  *
9
10
  * `FLOWVIANT_TOKEN` (one worker, current checkout) and `FLOWVIANT_TOKENS` (a
10
11
  * comma list, one worktree each) stood beside it until 2026-08-19. Both ran the
@@ -19,28 +20,32 @@
19
20
  * on its own — at startup and when idle — so it stays current without restarts
20
21
  * (FLOWVIANT_NO_UPDATE=1 makes it nag-only; `flowviant update` updates now).
21
22
  *
22
- * Fleet daemon: install ONCE with a fleet credential, then manage everything
23
- * from Flowviant. The daemon polls GET /api/v2/fleet/agents, reconciles one
24
- * persistent git worktree + worker loop per roster agent, rotates each worker's
25
- * short-lived MCP token, and only spawns Claude when an agent has work. Add/remove
26
- * agents in the app the daemon picks up the change on its next poll. Each worker
27
- * claims its work, resets its worktree to base per task (fresh Claude conversation),
28
- * opens one PR per intent, and routes questions back as blockers.
23
+ * The daemon: install ONCE with a machine credential, then work entirely from
24
+ * Flowviant. It polls GET /api/v2/fleet/agents, and the roster hands it the
25
+ * project's SESSIONS the Workbench's tabs. Each session gets one persistent
26
+ * git worktree on its own `session/<id>` branch, held across turns (never reset
27
+ * to base: the branch outlives the tab). A turn spawns the session's CLI with a
28
+ * short-lived per-session MCP token, relays what it prints back to the tab,
29
+ * reports the worktree's branch and diffstat when it settles, and answers the
30
+ * odd side job the roster carries — a commit's patch, a preview share, a wiki
31
+ * regen. When you say ship, the daemon merges that branch into base `--no-ff`.
29
32
  *
30
33
  * Env:
31
- * FLOWVIANT_TOKEN / FLOWVIANT_TOKENS / FLOWVIANT_FLEET (one of) credentials.
34
+ * FLOWVIANT_FLEET the machine credential (or use `flowviant login`).
32
35
  * FLOWVIANT_API_URL default https://api.flowviant.com/api/v2
33
36
  * FLOWVIANT_MCP_URL default <API_URL>/mcp
34
37
  * FLOWVIANT_FLEET_URL default <API_URL>/fleet/agents
35
- * POLL_SECONDS gap between turns while waiting on a blocker (default 20)
36
- * IDLE_SECONDS gap between work checks when idle (default 30)
37
- * RECONCILE_SECONDS fleet roster poll cadence (default 10)
38
+ * RECONCILE_SECONDS roster poll cadence (default 10)
38
39
  * FLOWVIANT_SAFE=1 restrict the toolset instead of running unattended.
39
40
  *
40
- * Requires the `claude` CLI (and `gh` for PRs) on PATH; run from inside the git
41
- * repo you want worked. Fleet & static-fleet modes also require `git`.
41
+ * Requires one of `claude` / `codex` / `agy` on PATH, plus `git`; run from
42
+ * inside the git repo you want worked. `gh` is optional.
42
43
  *
43
- * Implementation lives in ./lib/: config, ui, claude, git, fleet, single.
44
+ * Implementation lives in ./lib/: config, ui, preflight, install, update,
45
+ * instance, login, mcp-cli; fleet (the roster loop) and work (session turns);
46
+ * claude + runtimes + prompts + stream (spawning a CLI and reading its events);
47
+ * git + worktreeDiff + patch; localSessions, listeners, preview + authproxy;
48
+ * env + env-cli + vault, resources, deploy, shot.
44
49
  */
45
50
  import { FLEET_TOKEN } from './lib/config.mjs';
46
51
  import { runFleetDaemon } from './lib/fleet.mjs';
@@ -132,9 +137,9 @@ if (process.argv[2] === 'clean') {
132
137
  }
133
138
 
134
139
  // `flowviant shot <url>` — capture a headless-browser screenshot of a running
135
- // page. Build agents shell out to this to attach REAL visual evidence to the
136
- // delivery card. Self-contained + graceful (no browser → exit 1, agent falls
137
- // back to text evidence); needs no credential, so it runs before the auth gate.
140
+ // page. A session's agent shells out to this to SEE the change it just made.
141
+ // Self-contained + graceful (no browser → exit 1, and the agent carries on in
142
+ // text); needs no credential, so it runs before the auth gate.
138
143
  if (process.argv[2] === 'shot') {
139
144
  const { runShot } = await import('./lib/shot.mjs');
140
145
  await runShot(process.argv.slice(3));
@@ -145,8 +150,9 @@ if (process.argv[2] === 'shot') {
145
150
  // are sealed to the project pubkey ON THIS MACHINE (same write-only crypto as
146
151
  // the browser); `show` decrypts locally — it only works on an ENROLLED machine.
147
152
  // `flowviant mcp` — connect YOUR Claude to Flowviant so you can file work from
148
- // the terminal. Mints a `cli` credential: a separate principal from the build
149
- // workers, with only the management tools and no way to claim or ship work.
153
+ // the terminal. Mints a `cli` credential: a separate principal from the
154
+ // per-session tokens, with only the management tools and no way to work or ship
155
+ // a card.
150
156
  if (process.argv[2] === 'mcp') {
151
157
  const { runMcpCommand } = await import('./lib/mcp-cli.mjs');
152
158
  await runMcpCommand(process.argv.slice(3));
@@ -164,7 +170,7 @@ if (!FLEET_TOKEN) {
164
170
  'error: no credential found. Easiest:\n' +
165
171
  ' flowviant login (approve in the app — recommended)\n' +
166
172
  'Or set:\n' +
167
- ' FLOWVIANT_FLEET=fft_… (fleet token, manage machines in Flowviant)'
173
+ ' FLOWVIANT_FLEET=fva_… (machine token, from the app)'
168
174
  );
169
175
  process.exit(1);
170
176
  }
@@ -1,44 +1,124 @@
1
1
  /**
2
- * Opt-in basic-auth reverse proxy for previews (`.flowviant/preview.json`
3
- * "auth": true). The public tunnel is otherwise a capability URL — anyone with
4
- * it reaches the running app. This gates it: cloudflared → this proxy → dev
5
- * server, with a generated password the app surfaces to the reviewer.
2
+ * The password gate in front of a shared preview. cloudflared → this proxy →
3
+ * the dev server the DRIVER started in their own worktree.
6
4
  *
7
- * Minimal + dependency-free: forwards HTTP, and pipes WS upgrades (HMR) the
5
+ * MANDATORY, not opt-in (2026-08-21). It was `.flowviant/preview.json`
6
+ * "auth": true, defaulting OFF, and its caller logged "tunneling WITHOUT a
7
+ * password" to a console nobody reads and opened the tunnel anyway. A tunnel
8
+ * publishes a worktree holding the project's materialized dev secrets; there is
9
+ * no honest default but closed. `startAuthProxy` returning null now means the
10
+ * share is ABORTED and the machine says why.
11
+ *
12
+ * Minimal and dependency-free: forwards HTTP, and pipes WS upgrades (HMR) — the
8
13
  * browser re-sends the cached Basic-auth header on same-origin upgrades, so HMR
9
- * still authenticates. If a proxy request fails the page just 502s; it never
10
- * touches the default (no-auth) preview path.
14
+ * still authenticates.
15
+ *
16
+ * Three things this file gets wrong easily, all of them fixed here and all of
17
+ * them worth keeping fixed:
18
+ * - the credential must NOT reach the origin. `headers: req.headers` forwarded
19
+ * `authorization` verbatim, handing the gate password to whatever code the
20
+ * branch happens to be running. It is stripped now.
21
+ * - the comparison is over a secret, so it is constant-time over a digest
22
+ * rather than `===` over a string.
23
+ * - `stop()` was a bare `server.close()`, which refuses NEW connections and
24
+ * leaves live ones alone — so a held HMR websocket kept the page alive for
25
+ * the one most-engaged viewer after teardown. Live sockets are tracked and
26
+ * destroyed.
11
27
  */
12
28
 
13
29
  import { createServer, request } from 'node:http';
14
- import { randomBytes } from 'node:crypto';
30
+ import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
31
+
32
+ /** Failed attempts before the proxy stops answering at all. A quick tunnel's
33
+ * hostname is unguessable, so this is not the primary control — it is what
34
+ * turns a discovered URL from an offline guessing target into a visible,
35
+ * self-closing incident. */
36
+ const MAX_FAILED = 25;
37
+
38
+ const digest = (s) => createHash('sha256').update(String(s)).digest();
39
+
40
+ /** Constant-time over sha256 digests, so length never leaks and a missing
41
+ * header costs the same as a wrong one. */
42
+ function sameSecret(a, b) {
43
+ try {
44
+ return timingSafeEqual(digest(a ?? ''), digest(b ?? ''));
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
15
49
 
16
- /** Start the proxy in front of a dev server on `targetPort`. Resolves
17
- * { port, user, password, stop }. Binds loopback only (cloudflared connects
18
- * locally); the password is what gates the public tunnel. */
19
- export function startAuthProxy({ targetPort, log }) {
50
+ /**
51
+ * Start the gate in front of a dev server on `targetPort`. Resolves
52
+ * { port, user, password, stop } or NULL, which the caller must treat as a
53
+ * hard failure. Binds loopback only; cloudflared connects locally, and the
54
+ * password is what gates the public hostname.
55
+ *
56
+ * `onAbuse` fires once, after MAX_FAILED rejected attempts, so the caller can
57
+ * tear the whole share down rather than leaving a URL under attack.
58
+ */
59
+ export function startAuthProxy({ targetPort, log, onAbuse }) {
20
60
  const user = 'preview';
21
- const password = randomBytes(9).toString('base64url'); // ~12 url-safe chars
61
+ // 24 bytes 32 url-safe chars. It was 9 bytes, chosen when this was an
62
+ // opt-in convenience; it is the only thing between a public hostname and a
63
+ // worktree now.
64
+ const password = randomBytes(24).toString('base64url');
22
65
  const expected = 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64');
23
- const authed = (req) => req.headers['authorization'] === expected;
24
66
 
25
- const forwardOpts = (req) => ({
26
- host: '127.0.0.1',
27
- port: targetPort,
28
- method: req.method,
29
- path: req.url,
30
- headers: req.headers,
31
- });
67
+ let failed = 0;
68
+ let abused = false;
69
+ const authed = (req) => {
70
+ if (abused) return false;
71
+ if (sameSecret(req.headers['authorization'], expected)) {
72
+ failed = 0;
73
+ return true;
74
+ }
75
+ // A bare GET with no header is the browser's first move on every load, and
76
+ // it is answered with a 401 challenge — it is not an attempt.
77
+ if (req.headers['authorization']) failed += 1;
78
+ if (failed >= MAX_FAILED && !abused) {
79
+ abused = true;
80
+ log?.(`preview gate: ${failed} failed attempts — closing the share.`);
81
+ try {
82
+ onAbuse?.();
83
+ } catch {
84
+ /* the caller's teardown is best-effort */
85
+ }
86
+ }
87
+ return false;
88
+ };
89
+
90
+ // The gate credential is OURS and stops here. Everything else is passed
91
+ // through untouched: the origin is the driver's own dev server and rewriting
92
+ // its request would be us editing their app's input.
93
+ const forwardOpts = (req) => {
94
+ const headers = { ...req.headers };
95
+ delete headers.authorization;
96
+ delete headers['proxy-authorization'];
97
+ return {
98
+ host: '127.0.0.1',
99
+ port: targetPort,
100
+ method: req.method,
101
+ path: req.url,
102
+ headers,
103
+ };
104
+ };
105
+
106
+ const challenge = (res) => {
107
+ res.writeHead(401, {
108
+ 'WWW-Authenticate': 'Basic realm="Flowviant preview"',
109
+ 'Content-Type': 'text/plain',
110
+ // A preview is a moving target by definition; nothing about it should sit
111
+ // in a cache the viewer cannot see.
112
+ 'Cache-Control': 'no-store',
113
+ });
114
+ res.end('This preview is password-protected. Enter the password shown in Flowviant.');
115
+ };
116
+
117
+ // Every live socket, so stop() can actually end the ones already talking.
118
+ const sockets = new Set();
32
119
 
33
120
  const server = createServer((req, res) => {
34
- if (!authed(req)) {
35
- res.writeHead(401, {
36
- 'WWW-Authenticate': 'Basic realm="Flowviant preview"',
37
- 'Content-Type': 'text/plain',
38
- });
39
- res.end('This preview is password-protected. Enter the password shown in Flowviant.');
40
- return;
41
- }
121
+ if (!authed(req)) return challenge(res);
42
122
  const proxyReq = request(forwardOpts(req), (proxyRes) => {
43
123
  res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
44
124
  proxyRes.pipe(res);
@@ -50,8 +130,13 @@ export function startAuthProxy({ targetPort, log }) {
50
130
  req.pipe(proxyReq);
51
131
  });
52
132
 
133
+ server.on('connection', (socket) => {
134
+ sockets.add(socket);
135
+ socket.on('close', () => sockets.delete(socket));
136
+ });
137
+
53
138
  // WS upgrade (HMR). The browser resends the Basic-auth header on same-origin
54
- // upgrades, so we can gate it too, then pipe the two sockets together.
139
+ // upgrades, so we gate it too, then pipe the two sockets together.
55
140
  server.on('upgrade', (req, socket, head) => {
56
141
  if (!authed(req)) {
57
142
  socket.write('HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm="Flowviant preview"\r\n\r\n');
@@ -65,6 +150,8 @@ export function startAuthProxy({ targetPort, log }) {
65
150
  if (proxyHead && proxyHead.length) proxySocket.unshift(proxyHead);
66
151
  proxySocket.pipe(socket);
67
152
  socket.pipe(proxySocket);
153
+ sockets.add(proxySocket);
154
+ proxySocket.on('close', () => sockets.delete(proxySocket));
68
155
  proxySocket.on('error', () => socket.destroy());
69
156
  socket.on('error', () => proxySocket.destroy());
70
157
  });
@@ -74,10 +161,12 @@ export function startAuthProxy({ targetPort, log }) {
74
161
  });
75
162
 
76
163
  return new Promise((resolve) => {
77
- server.on('error', () => resolve(null)); // couldn't bind → caller falls back to no proxy
164
+ // Could not bind → the caller ABORTS the share. There is no no-proxy path
165
+ // to fall back to any more.
166
+ server.on('error', () => resolve(null));
78
167
  server.listen(0, '127.0.0.1', () => {
79
168
  const port = server.address().port;
80
- log?.(`auth proxy on :${port} — preview is password-gated`);
169
+ log?.(`preview gate on :${port}`);
81
170
  resolve({
82
171
  port,
83
172
  user,
@@ -88,6 +177,16 @@ export function startAuthProxy({ targetPort, log }) {
88
177
  } catch {
89
178
  /* already closed */
90
179
  }
180
+ // close() only stops NEW connections. An open HMR socket would keep
181
+ // serving the viewer who is still looking at it.
182
+ for (const s of sockets) {
183
+ try {
184
+ s.destroy();
185
+ } catch {
186
+ /* already gone */
187
+ }
188
+ }
189
+ sockets.clear();
91
190
  },
92
191
  });
93
192
  });
@@ -1,5 +1,6 @@
1
1
  /** Parsed configuration: env vars, CLI flags, and the chosen credentials. */
2
2
 
3
+ import { randomBytes } from 'node:crypto';
3
4
  import { readFileSync } from 'node:fs';
4
5
  import { dirname, join } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
@@ -43,11 +44,6 @@ function argFlag(name) {
43
44
  return i >= 0 ? process.argv[i + 1] : undefined;
44
45
  }
45
46
 
46
- /** A bare boolean flag (no value follows it). */
47
- function hasFlag(name) {
48
- return process.argv.includes(name);
49
- }
50
-
51
47
  const API_BASE = process.env.FLOWVIANT_API_URL || 'https://api.flowviant.com/api/v2';
52
48
  export const MCP_URL = process.env.FLOWVIANT_MCP_URL || `${API_BASE}/mcp`;
53
49
  export const FLEET_URL = process.env.FLOWVIANT_FLEET_URL || `${API_BASE}/fleet/agents`;
@@ -148,25 +144,24 @@ export const AUTO_UPDATE = process.env.FLOWVIANT_NO_UPDATE !== '1';
148
144
  // path (one-shot `claude -p` turns) survives behind FLOWVIANT_POLL=1 as the
149
145
  // escape hatch; FLOWVIANT_LIVE=1 is still honored for old scripts.
150
146
  export const LIVE = process.env.FLOWVIANT_POLL !== '1';
147
+ // Sent on the daemon's own HTTP calls so Cloudflare Bot Fight Mode doesn't 403
148
+ // them (Node's default UA is treated as a bot). Claude Code sends its own UA.
149
+ export const USER_AGENT = `flowviant/${VERSION}`;
150
+
151
151
  /**
152
- * Does this machine accept PATCHEScommits cherry-picked straight into your
153
- * working checkout, with no PR and no review?
152
+ * WHICH DAEMON PROCESS this isnot which credential.
154
153
  *
155
- * Patch placement is chosen by a model, and any teammate who @mentions one of
156
- * your agents can trigger it, so whether it happens at all belongs to whoever
157
- * owns the checkout. Turning it off does not lose the work: the task falls back
158
- * to branch placement and arrives as a PR like anything else.
154
+ * Two daemons legitimately share one fleet token (that is exactly what
155
+ * `machineDaemonsDisagree` reports, and the instance lock added in 0.51.2
156
+ * cannot see a peer OLDER than itself). So the token cannot identify a lease
157
+ * holder, and anything that must be done exactly once needs this instead.
159
158
  *
160
- * On by default the guard that actually protects you (never touching a file
161
- * you have uncommitted edits in) is enforced at apply time, and the whole point
162
- * of patches is to spare you a review cycle for a nine-character diff.
163
- * `--no-patches` or FLOWVIANT_PATCHES=0 to refuse them.
159
+ * Regenerated every start, deliberately: a daemon that restarted is a daemon
160
+ * that lost whatever it was holding, and a stale lease should not follow it
161
+ * back. Sent on the poll as `di`; its ABSENCE is what an older daemon looks
162
+ * like, and the server hands preview work to nobody who cannot name themselves.
164
163
  */
165
- export const ALLOW_PATCHES =
166
- !hasFlag('--no-patches') && process.env.FLOWVIANT_PATCHES !== '0';
167
- // Sent on the daemon's own HTTP calls so Cloudflare Bot Fight Mode doesn't 403
168
- // them (Node's default UA is treated as a bot). Claude Code sends its own UA.
169
- export const USER_AGENT = `flowviant/${VERSION}`;
164
+ export const DAEMON_INSTANCE = randomBytes(12).toString('hex');
170
165
 
171
166
  // The ONE credential. `tokens` (FLOWVIANT_TOKEN / FLOWVIANT_TOKENS / --token /
172
167
  // --tokens) stood beside it and carried WORKER tokens into the pre-daemon loop;