flowviant 0.35.0 → 0.36.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.
@@ -89,8 +89,18 @@ export const RESUME =
89
89
  'Resume. First call get_blocker_resolution for any blocker you reported; if resolved, ' +
90
90
  'apply the human’s answer and continue. Otherwise keep picking up and completing ' +
91
91
  'the tasks you were @mentioned on, per your instructions.';
92
- export const SINGLE_KICKOFF =
93
- 'Pick up and complete exactly ONE Flowviant task per your instructions, then stop.';
92
+ // `intentId` is the task the SERVER says this lane is next in line for. Naming
93
+ // it matters beyond saving a lookup: the daemon has already spawned this Claude
94
+ // with that task's --model and --effort, and those cannot change once the
95
+ // process exists. Left to pick freely, a lane could claim a sibling task and
96
+ // run it under settings its owner chose for something else. Omitted (older
97
+ // server, or nothing waiting) it falls back to the original free pick.
98
+ export const SINGLE_KICKOFF = (intentId) =>
99
+ intentId
100
+ ? `Pick up Flowviant task ${intentId} — call claim_next_intent with intentId "${intentId}" — ` +
101
+ 'complete exactly that ONE task per your instructions, then stop. If that ' +
102
+ 'claim comes back unavailable, claim whatever is next for you instead.'
103
+ : 'Pick up and complete exactly ONE Flowviant task per your instructions, then stop.';
94
104
  export const SINGLE_RESUME =
95
105
  'Resume your current task. Call get_blocker_resolution for the blocker you reported; ' +
96
106
  'if resolved, apply the human’s answer and finish this one intent, then stop.';
@@ -590,7 +600,7 @@ function handleStreamLine(line, { cwd, emit, onActivity, appendText }) {
590
600
  // returned string for sentinel detection, and each activity is handed to
591
601
  // `onActivity` so the caller can forward progress. Build-agent turns leave it
592
602
  // off and keep the raw text passthrough + line sentinels.
593
- export function runTurn({ prompt, resume, system, cwd, mcpConfig, label, onSpawn, streamJson, onActivity, wikiPerm, readOnly }) {
603
+ export function runTurn({ prompt, resume, system, cwd, mcpConfig, label, onSpawn, streamJson, onActivity, wikiPerm, readOnly, model, effort }) {
594
604
  return new Promise((resolve) => {
595
605
  const args = [];
596
606
  if (resume) args.push('--continue');
@@ -599,7 +609,12 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, label, onSpawn
599
609
  if (mcpConfig) args.push('--mcp-config', mcpConfig);
600
610
  // Pin the model — never inherit the user's global default (which may be a
601
611
  // 1M/long-context tier their subscription can't bill autonomous work on).
602
- args.push('--model', MODEL);
612
+ // A per-task override (chosen in the app, validated server-side against a
613
+ // fixed list before it ever reaches this argv) wins over the machine pin;
614
+ // absent, the pin stands. Effort has no machine-level pin at all: unset
615
+ // means Claude Code's own default, which is the honest resting state.
616
+ args.push('--model', model || MODEL);
617
+ if (effort) args.push('--effort', effort);
603
618
  if (streamJson) args.push('--output-format', 'stream-json', '--verbose');
604
619
  // readOnly wins over wikiPerm: a consult must never inherit write tools.
605
620
  args.push(...(readOnly ? CONSULT_PERM : wikiPerm ? WIKI_PERM : PERM));
package/bin/lib/fleet.mjs CHANGED
@@ -124,7 +124,7 @@ async function fetchRoster(haveIds) {
124
124
 
125
125
  // One roster agent's loop: persistent worktree, one intent per turn, reset to
126
126
  // base between tasks (fresh conversation), resume in place while on a blocker.
127
- async function runFleetWorker({ agentId, label, cwd, baseRef, getToken, getHasWork, getMcpUrl, isAlive, onChild, onTokenSuspect }) {
127
+ async function runFleetWorker({ agentId, label, cwd, baseRef, getToken, getHasWork, getNext, getMcpUrl, isAlive, onChild, onTokenSuspect }) {
128
128
  let resuming = false;
129
129
  let needsReset = true; // reset to base before a FRESH task, not on idle polls
130
130
  let phase = ''; // '', 'idle', 'blocked' — log each transition once, not per poll
@@ -154,15 +154,25 @@ async function runFleetWorker({ agentId, label, cwd, baseRef, getToken, getHasWo
154
154
  needsReset = false;
155
155
  }
156
156
  const { dir, path: mcpConfig } = mcpConfigFor(token, getMcpUrl());
157
+ // The task the server says is next for this lane, read ONCE per turn: the
158
+ // model and effort below become process flags, so they must describe the
159
+ // same task the kickoff tells Claude to claim. Re-reading the map mid-turn
160
+ // could pair one task's flags with another's work.
161
+ const next = resuming ? null : getNext?.(agentId) || null;
157
162
  let out = '';
158
163
  try {
159
164
  out = await runTurn({
160
- prompt: resuming ? SINGLE_RESUME : SINGLE_KICKOFF,
165
+ prompt: resuming ? SINGLE_RESUME : SINGLE_KICKOFF(next?.intentId),
161
166
  resume: resuming,
162
167
  system: SYSTEM_SINGLE,
163
168
  cwd,
164
169
  mcpConfig,
165
170
  label,
171
+ // Per-task overrides — null/absent means this machine's own defaults
172
+ // (FLOWVIANT_MODEL, and Claude Code's own effort). A resume keeps the
173
+ // session it already has, so there is nothing to re-pick there.
174
+ model: next?.model || undefined,
175
+ effort: next?.effort || undefined,
166
176
  onSpawn: (ch) => onChild?.(ch),
167
177
  });
168
178
  } finally {
@@ -298,6 +308,13 @@ export async function runFleetDaemon() {
298
308
  const tokenByAgent = new Map(); // agentId -> latest worker token
299
309
  const mintedAt = new Map(); // agentId -> ms when we last got a fresh token
300
310
  const hasWorkByAgent = new Map(); // agentId -> server says it has claimable work
311
+ // agentId -> the intent the server would hand this lane next: { intentId,
312
+ // title, model, effort }. `--model`/`--effort` are fixed when Claude starts,
313
+ // and by then nothing has been claimed — so the server names the task first
314
+ // and the turn pins its claim to that id. Absent on older servers, in which
315
+ // case the lane behaves exactly as it did before: generic kickoff, machine
316
+ // defaults.
317
+ const nextByAgent = new Map();
301
318
  let leaseTtlSeconds = 24 * 60 * 60; // updated from each roster response
302
319
  let mcpUrl = MCP_URL;
303
320
  const workers = new Map(); // agentId -> { state, promise, wt, label }
@@ -1359,6 +1376,8 @@ export async function runFleetDaemon() {
1359
1376
  mintedAt.set(a.agentId, Date.now());
1360
1377
  }
1361
1378
  hasWorkByAgent.set(a.agentId, !!a.hasWork);
1379
+ if (a.next && typeof a.next.intentId === 'string') nextByAgent.set(a.agentId, a.next);
1380
+ else nextByAgent.delete(a.agentId);
1362
1381
  if (!workers.has(a.agentId)) {
1363
1382
  // Local ceiling, enforced and not merely requested. The roster can carry
1364
1383
  // more lanes than this machine asked for — someone added capacity by
@@ -1409,6 +1428,7 @@ export async function runFleetDaemon() {
1409
1428
 
1410
1429
  getToken: (id) => tokenByAgent.get(id),
1411
1430
  getHasWork: (id) => hasWorkByAgent.get(id) ?? false,
1431
+ getNext: (id) => nextByAgent.get(id) ?? null,
1412
1432
  getMcpUrl: () => mcpUrl,
1413
1433
  isAlive: () => state.alive,
1414
1434
  onChild: (ch) => {
@@ -1524,6 +1544,7 @@ export async function runFleetDaemon() {
1524
1544
  workers.delete(id);
1525
1545
  tokenByAgent.delete(id);
1526
1546
  hasWorkByAgent.delete(id);
1547
+ nextByAgent.delete(id);
1527
1548
  mintedAt.delete(id); // was leaked on removal (finding 14)
1528
1549
  }
1529
1550
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.35.0",
3
+ "version": "0.36.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": {