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.
- package/bin/lib/claude.mjs +19 -4
- package/bin/lib/fleet.mjs +23 -2
- package/package.json +1 -1
package/bin/lib/claude.mjs
CHANGED
|
@@ -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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|