flowviant 0.33.0 → 0.34.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.
@@ -3,7 +3,7 @@
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { homedir } from 'node:os';
6
+ import { homedir, cpus } from 'node:os';
7
7
 
8
8
  // Read the daemon's version from its OWN package.json (always shipped in the npm
9
9
  // tarball) — never hardcode it. The hardcoded constant drifted: it sat at
@@ -58,6 +58,32 @@ export const STREAM_URL =
58
58
  process.env.FLOWVIANT_STREAM_URL ||
59
59
  FLEET_URL.replace(/\/agents(\/?)$/, '/stream$1').replace(/^http/, 'ws');
60
60
  export const POLL_SECONDS = Number(process.env.POLL_SECONDS || 20);
61
+
62
+ /**
63
+ * How many tasks THIS MACHINE will build at once.
64
+ *
65
+ * The limit belongs here, not on the server: a task in flight is a Claude Code
66
+ * session plus its own git worktree plus whatever the project's dev server and
67
+ * tests want, and this process is the only party that can see the cores, the
68
+ * RAM and the fan. The server used to decide it, indirectly, by how many lanes
69
+ * a user had pre-sized with a dial — which asked them to answer a question
70
+ * about their laptop in a web app, before they knew what they were going to
71
+ * dispatch.
72
+ *
73
+ * Sent to the server on every roster poll so it can grow lanes to meet waiting
74
+ * work UNDER this ceiling, and enforced locally besides — the roster can carry
75
+ * more lanes than this (someone added capacity by hand, or a second machine
76
+ * shares the fleet), and a ceiling that only exists as a request is not one.
77
+ *
78
+ * Half the cores, floor 1, cap 4. Half because a build agent is not the only
79
+ * thing running — the user is working on this machine too — and 4 because past
80
+ * that the shared Claude account, not the CPU, is what runs out.
81
+ */
82
+ export const MAX_CONCURRENT = (() => {
83
+ const asked = Number(process.env.FLOWVIANT_MAX_CONCURRENT);
84
+ if (Number.isFinite(asked) && asked >= 1) return Math.min(Math.floor(asked), 32);
85
+ return Math.max(1, Math.min(4, Math.floor((cpus().length || 2) / 2)));
86
+ })();
61
87
  export const IDLE_SECONDS = Number(process.env.IDLE_SECONDS || 30);
62
88
  // Live mode: after this long idle-parked on a blocker, tear the session down to
63
89
  // free the Claude process (the intent stays claimed; it resumes when answered).
package/bin/lib/fleet.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  MCP_URL,
19
19
  SAFE,
20
20
  POLL_SECONDS,
21
+ MAX_CONCURRENT,
21
22
  IDLE_SECONDS,
22
23
  RECONCILE_SECONDS,
23
24
  REFRESH_BEFORE_SECONDS,
@@ -76,6 +77,11 @@ import { processDeployJobs, reportDeployConfig } from './deploy.mjs';
76
77
  async function fetchRoster(haveIds) {
77
78
  const url = new URL(FLEET_URL);
78
79
  if (haveIds.length) url.searchParams.set('have', haveIds.join(','));
80
+ // What this machine will run at once. The server grows lanes to meet waiting
81
+ // work beneath this, instead of the user pre-sizing a pool by hand — only the
82
+ // machine knows its cores, its RAM and whose Claude quota is being spent.
83
+ // Older servers ignore the param, so sending it is always safe.
84
+ url.searchParams.set('capacity', String(MAX_CONCURRENT));
79
85
  // Env-sync identity + materialized version (the Settings "env vN" chip).
80
86
  try {
81
87
  for (const [k, v] of Object.entries(await envQueryParams())) {
@@ -1170,6 +1176,7 @@ export async function runFleetDaemon() {
1170
1176
  let connected = false; // log the first successful poll once
1171
1177
  let rosterSig = null; // last roster membership, to log changes only
1172
1178
  let idleBeatAt = 0; // throttle the "still alive" idle heartbeat
1179
+ let cappedWarned = false; // say once, not every reconcile, why extra lanes idle
1173
1180
  let joinCount = 0; // for stable per-agent label colours
1174
1181
 
1175
1182
  // ── Push channel: a server wake short-circuits the reconcile sleep so a job is
@@ -1296,6 +1303,23 @@ export async function runFleetDaemon() {
1296
1303
  }
1297
1304
  hasWorkByAgent.set(a.agentId, !!a.hasWork);
1298
1305
  if (!workers.has(a.agentId)) {
1306
+ // Local ceiling, enforced and not merely requested. The roster can carry
1307
+ // more lanes than this machine asked for — someone added capacity by
1308
+ // hand, or a second machine shares the fleet — and each extra worker is
1309
+ // another Claude session, another worktree and another dev server on
1310
+ // somebody's laptop. Skipping the spawn does NOT strand the work: an
1311
+ // @mention addresses the FLEET, so any running lane can claim it; the
1312
+ // tasks queue behind the ones we did start.
1313
+ if (workers.size >= MAX_CONCURRENT) {
1314
+ if (!cappedWarned) {
1315
+ cappedWarned = true;
1316
+ info(
1317
+ `running ${MAX_CONCURRENT} task${MAX_CONCURRENT === 1 ? '' : 's'} at a time on this machine — ` +
1318
+ `more will queue (FLOWVIANT_MAX_CONCURRENT to change)`
1319
+ );
1320
+ }
1321
+ continue;
1322
+ }
1299
1323
  const wt = join(baseDir, `agent-${a.agentId}`);
1300
1324
  try {
1301
1325
  if (!existsSync(wt)) {
package/bin/lib/live.mjs CHANGED
@@ -138,10 +138,15 @@ This IS your handover, so make it tangible; match the evidence to what you built
138
138
  • UI / any visible screen → attach a real SCREENSHOT. Start the app's dev server
139
139
  in your worktree, then capture it headlessly with
140
140
  \`flowviant shot http://localhost:<PORT>/<route> --out shot.png\` (it finds a
141
- browser for you and never needs a display), and attach_evidence with kind
142
- "screenshot" and the file's base64 (\`base64 -w0 shot.png\`). Shoot EVERY key
143
- screen you changed. If \`flowviant shot\` reports that no browser is available,
144
- do NOT block fall back to the text evidence below.
141
+ browser for you and never needs a display). THEN READ shot.png BACK AND LOOK
142
+ AT IT before you attach you can see images, and this is the only moment
143
+ anyone checks the thing you are about to call proof. A blank page, a 404, an
144
+ error overlay, a collapsed layout and the screen you meant all look identical
145
+ as a file path. If it is wrong, fix the code and shoot again; if it is right,
146
+ attach_evidence with kind "screenshot" and the file's base64
147
+ (\`base64 -w0 shot.png\`). Shoot EVERY key screen you changed. If
148
+ \`flowviant shot\` reports that no browser is available, do NOT block — fall
149
+ back to the text evidence below.
145
150
  • backend / API work → a request/response capture or a data sample showing the
146
151
  write (kind "request_response" or "sample").
147
152
  • a multi-step FLOW (login, signup, checkout): one screenshot does NOT prove it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "Run your own Claude Code as headless build agents for Flowviant — on your own credentials. Claims dispatched work, opens PRs, captures review evidence, and routes questions back to you.",
5
5
  "type": "module",
6
6
  "bin": {