flowviant 0.6.0 → 0.7.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 -12
- package/bin/lib/config.mjs +1 -1
- package/bin/lib/fleet.mjs +5 -2
- package/bin/lib/live.mjs +82 -14
- package/bin/lib/preview.mjs +25 -3
- package/package.json +1 -1
package/bin/lib/claude.mjs
CHANGED
|
@@ -25,13 +25,14 @@ Operate this loop:
|
|
|
25
25
|
3. If you hit ANYTHING only a human can decide, call report_blocker with a clear
|
|
26
26
|
question (and options when you can), then call get_blocker_resolution. If it is
|
|
27
27
|
not yet resolved, output exactly BLOCKED:<blockerId> on its own line and STOP.
|
|
28
|
-
4.
|
|
29
|
-
proof it is met. This is what the human reviews instead of the diff.
|
|
30
|
-
5. Ship: on a revision, \`git push\` to the SAME existing branch (the PR updates in place)
|
|
28
|
+
4. Ship: on a revision, \`git push\` to the SAME existing branch (the PR updates in place)
|
|
31
29
|
and re-call attach_pr with that PR URL; otherwise open ONE draft PR (git push +
|
|
32
|
-
\`gh pr create --draft\`) and call attach_pr. Then complete
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
\`gh pr create --draft\`) and call attach_pr. Then call complete with a plain-language
|
|
31
|
+
summary of what you built AND a criteria self-report (index into the brief's
|
|
32
|
+
"done when" list + met true/false + a short note) — that becomes your delivery
|
|
33
|
+
card in the task thread. NEVER merge — a human confirms done in the thread and
|
|
34
|
+
the merge runs separately.
|
|
35
|
+
5. Return to step 1.
|
|
35
36
|
|
|
36
37
|
Keep every change scoped to the claimed intent. If a tool errors, report_progress with
|
|
37
38
|
the error, then retry or report_blocker.`;
|
|
@@ -53,12 +54,12 @@ Do EXACTLY ONE task this turn:
|
|
|
53
54
|
3. If you hit ANYTHING only a human can decide, call report_blocker (with options when
|
|
54
55
|
you can), then get_blocker_resolution. If unresolved, output exactly
|
|
55
56
|
BLOCKED:<blockerId> on its own line and STOP. Do NOT guess past a real decision.
|
|
56
|
-
4.
|
|
57
|
-
proof it is met.
|
|
58
|
-
5. Ship: if this is a revision, \`git push\` to the SAME existing branch (the open PR
|
|
57
|
+
4. Ship: if this is a revision, \`git push\` to the SAME existing branch (the open PR
|
|
59
58
|
updates in place) and re-call attach_pr with that same PR URL. Otherwise open ONE
|
|
60
|
-
draft PR (git push + \`gh pr create --draft\`) and call attach_pr. Then complete
|
|
61
|
-
|
|
59
|
+
draft PR (git push + \`gh pr create --draft\`) and call attach_pr. Then call complete
|
|
60
|
+
with a plain-language summary AND a criteria self-report (index into the brief's
|
|
61
|
+
"done when" list + met true/false + a short note) — your delivery card in the task
|
|
62
|
+
thread. NEVER merge. Then output exactly DONE on its own line and stop.
|
|
62
63
|
|
|
63
64
|
Do NOT claim a second intent — exactly one per turn. Keep every change scoped to the
|
|
64
65
|
claimed intent. If a tool errors, report_progress with the error, then retry or
|
|
@@ -127,7 +128,13 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, label, onSpawn
|
|
|
127
128
|
const args = [];
|
|
128
129
|
if (resume) args.push('--continue');
|
|
129
130
|
args.push('-p', prompt, '--mcp-config', mcpConfig, '--append-system-prompt', system, ...PERM);
|
|
130
|
-
|
|
131
|
+
// Force the user's Claude Code subscription — never the API. A key exported in
|
|
132
|
+
// the shell would otherwise silently bill every poll-mode turn as raw API
|
|
133
|
+
// usage (same invariant live mode enforces on its SDK session env).
|
|
134
|
+
const env = { ...process.env };
|
|
135
|
+
delete env.ANTHROPIC_API_KEY;
|
|
136
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
137
|
+
const child = spawn('claude', args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
131
138
|
onSpawn?.(child);
|
|
132
139
|
let out = '';
|
|
133
140
|
const pfx = label ? `${label} ` : '';
|
package/bin/lib/config.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
|
|
7
|
-
export const VERSION = '0.
|
|
7
|
+
export const VERSION = '0.7.0';
|
|
8
8
|
|
|
9
9
|
// Credential stored by `flowviant login` (device auth) — the no-token,
|
|
10
10
|
// no-env-var path. An explicit --fleet flag or FLOWVIANT_FLEET env still wins.
|
package/bin/lib/fleet.mjs
CHANGED
|
@@ -128,8 +128,11 @@ async function runFleetWorker({ agentId, label, cwd, baseRef, getToken, getHasWo
|
|
|
128
128
|
// don't fake a blocker or a completion.
|
|
129
129
|
enter('reconnect', warn, `${c.yellow('no result')}${c.dim(' — refreshing token, retrying')}`);
|
|
130
130
|
onTokenSuspect?.(agentId);
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// A no-sentinel turn while RESUMING a blocked task is a transient MCP/token
|
|
132
|
+
// failure, not completion — retry in place and KEEP the worktree. Resetting
|
|
133
|
+
// here would wipe the blocked task's uncommitted changes. Only a fresh-task
|
|
134
|
+
// turn (not resuming) warrants a clean slate next time.
|
|
135
|
+
if (!resuming) needsReset = true;
|
|
133
136
|
await sleep(IDLE_SECONDS);
|
|
134
137
|
}
|
|
135
138
|
info(`${label} stopped`);
|
package/bin/lib/live.mjs
CHANGED
|
@@ -51,7 +51,21 @@ async function registerLiveTarget(intentId, kind, url) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
// Safe mode's curated toolset. Bash is scoped to the specific CLIs the agent
|
|
55
|
+
// needs (git/gh/npm/bun) — NOT bare `Bash`, which would auto-approve arbitrary
|
|
56
|
+
// shell (rm -rf, curl|sh, reading ~/.ssh) and defeat the point of safe mode.
|
|
57
|
+
const SAFE_TOOLS = [
|
|
58
|
+
'Edit',
|
|
59
|
+
'Write',
|
|
60
|
+
'Read',
|
|
61
|
+
'Grep',
|
|
62
|
+
'Glob',
|
|
63
|
+
'Bash(git:*)',
|
|
64
|
+
'Bash(gh:*)',
|
|
65
|
+
'Bash(npm:*)',
|
|
66
|
+
'Bash(bun:*)',
|
|
67
|
+
'mcp__flowviant',
|
|
68
|
+
];
|
|
55
69
|
|
|
56
70
|
// Appended to Claude Code's preset. The reliable copy of the contract also
|
|
57
71
|
// rides in the seed message below, so this degrades gracefully if the preset
|
|
@@ -64,12 +78,15 @@ answered…" or teammate line as a new instruction and adapt. There is NO termin
|
|
|
64
78
|
and NO interactive prompt — your only channel to a human is the flowviant MCP
|
|
65
79
|
tools. When you hit a decision only a human can make, call report_blocker (with
|
|
66
80
|
options when you can) and then STOP your turn — do not spin or guess; you will be
|
|
67
|
-
resumed with the answer. When the work is done:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
resumed with the answer. When the work is done: open ONE draft PR (git push +
|
|
82
|
+
gh pr create --draft), call attach_pr, then call complete with a plain-language
|
|
83
|
+
summary of what you built AND a criteria self-report (index into the brief's
|
|
84
|
+
"done when" list + met true/false + a short note per item). That summary +
|
|
85
|
+
self-report becomes your DELIVERY CARD in the task thread — it's what the team
|
|
86
|
+
reads to confirm done, so write it for them, not for a log. A live preview of
|
|
87
|
+
your branch is started for you automatically — you do NOT need to open a tunnel
|
|
88
|
+
or register a live target. NEVER merge — a human confirms done in the thread
|
|
89
|
+
(the merge card) and the merge runs separately.`;
|
|
73
90
|
|
|
74
91
|
function seedPrompt(runId, brief, transcript) {
|
|
75
92
|
return [
|
|
@@ -84,7 +101,7 @@ function seedPrompt(runId, brief, transcript) {
|
|
|
84
101
|
? [``, `Conversation so far (you may be resuming — pick up where this left off):`, transcript]
|
|
85
102
|
: []),
|
|
86
103
|
``,
|
|
87
|
-
`${transcript ? 'Continue' : 'Begin'}. Post a short plan first, then: report_progress as you go; report_blocker + stop if you hit a human decision;
|
|
104
|
+
`${transcript ? 'Continue' : 'Begin'}. Post a short plan first, then: report_progress as you go; report_blocker + stop if you hit a human decision; open a draft PR, attach_pr, then complete (summary + criteria self-report — your delivery card) when done.`,
|
|
88
105
|
].join('\n');
|
|
89
106
|
}
|
|
90
107
|
|
|
@@ -99,6 +116,9 @@ async function mcpCall(mcpUrl, token, name, args) {
|
|
|
99
116
|
Authorization: `Bearer ${token}`,
|
|
100
117
|
'Content-Type': 'application/json',
|
|
101
118
|
Accept: 'application/json',
|
|
119
|
+
// Required: Node's default UA trips Cloudflare Bot Fight Mode (403) —
|
|
120
|
+
// without this every live MCP call fails against api.flowviant.com.
|
|
121
|
+
'User-Agent': USER_AGENT,
|
|
102
122
|
},
|
|
103
123
|
body: JSON.stringify({
|
|
104
124
|
jsonrpc: '2.0',
|
|
@@ -199,22 +219,26 @@ async function waitForMessage(mcpUrl, token, runId, afterId, isAlive) {
|
|
|
199
219
|
|
|
200
220
|
// One task: claim → seed → stream/mirror/inject/park → complete. Returns
|
|
201
221
|
// { outcome: 'nothing' | 'done' | 'blocked' | 'stalled' | 'error' }.
|
|
202
|
-
export async function runLiveTask({ mcpUrl, token, cwd, baseRef, isAlive }) {
|
|
222
|
+
export async function runLiveTask({ mcpUrl, token, cwd, baseRef, isAlive, resumeIntentId, onChild }) {
|
|
203
223
|
const claim = await mcpCall(mcpUrl, token, 'claim_next_intent', {}).catch(() => null);
|
|
204
224
|
if (!claim || claim.claimed !== true) return { outcome: 'nothing' };
|
|
205
225
|
const { runId, intentId } = claim;
|
|
206
226
|
const brief = claim.brief ?? {};
|
|
207
227
|
const title = brief.title ?? 'a task';
|
|
228
|
+
// Re-claiming the SAME intent this worker was just working (parked on a blocker,
|
|
229
|
+
// now resuming) — its worktree holds hours of uncommitted work. Do NOT reset.
|
|
230
|
+
const resuming = !!resumeIntentId && intentId === resumeIntentId;
|
|
208
231
|
|
|
209
|
-
// Revision resumes its PR branch;
|
|
232
|
+
// Revision resumes its PR branch; a genuinely fresh task gets a clean base
|
|
233
|
+
// checkout; a resume keeps its dirty worktree untouched.
|
|
210
234
|
if (brief.branch) {
|
|
211
235
|
try {
|
|
212
236
|
git(['fetch', 'origin', '--quiet'], cwd);
|
|
213
237
|
git(['checkout', brief.branch], cwd);
|
|
214
238
|
} catch {
|
|
215
|
-
resetWorktree(cwd, baseRef);
|
|
239
|
+
if (!resuming) resetWorktree(cwd, baseRef);
|
|
216
240
|
}
|
|
217
|
-
} else {
|
|
241
|
+
} else if (!resuming) {
|
|
218
242
|
resetWorktree(cwd, baseRef);
|
|
219
243
|
}
|
|
220
244
|
|
|
@@ -241,11 +265,34 @@ export async function runLiveTask({ mcpUrl, token, cwd, baseRef, isAlive }) {
|
|
|
241
265
|
...(SAFE ? { allowedTools: SAFE_TOOLS } : {}),
|
|
242
266
|
systemPrompt: { type: 'preset', preset: 'claude_code', append: SYSTEM_LIVE },
|
|
243
267
|
mcpServers: {
|
|
244
|
-
flowviant: {
|
|
268
|
+
flowviant: {
|
|
269
|
+
type: 'http',
|
|
270
|
+
url: mcpUrl,
|
|
271
|
+
headers: { Authorization: `Bearer ${token}`, 'User-Agent': USER_AGENT },
|
|
272
|
+
},
|
|
245
273
|
},
|
|
246
274
|
},
|
|
247
275
|
});
|
|
248
276
|
|
|
277
|
+
// Mark this worker BUSY for the daemon's reconcile loop: buildHave keeps the
|
|
278
|
+
// worker's token while a session is live (never rotate a credential out from
|
|
279
|
+
// under it), and teardown/agent-removal can interrupt the SDK session via this
|
|
280
|
+
// marker's kill(). Cleared in finally. Mirrors poll mode's onChild(child).
|
|
281
|
+
onChild?.({
|
|
282
|
+
kill: () => {
|
|
283
|
+
try {
|
|
284
|
+
session.interrupt?.();
|
|
285
|
+
} catch {
|
|
286
|
+
/* already ending */
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
session.return?.();
|
|
290
|
+
} catch {
|
|
291
|
+
/* already closed */
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
249
296
|
let turnId = null;
|
|
250
297
|
let turnText = '';
|
|
251
298
|
let turnAt = null;
|
|
@@ -359,6 +406,7 @@ export async function runLiveTask({ mcpUrl, token, cwd, baseRef, isAlive }) {
|
|
|
359
406
|
} catch (e) {
|
|
360
407
|
return { outcome: 'error', error: e?.message ?? String(e), title, intentId };
|
|
361
408
|
} finally {
|
|
409
|
+
onChild?.(null); // no longer busy — token may rotate between tasks
|
|
362
410
|
input.close();
|
|
363
411
|
try {
|
|
364
412
|
await session.interrupt?.();
|
|
@@ -385,7 +433,12 @@ export async function runLiveWorker({
|
|
|
385
433
|
getMcpUrl,
|
|
386
434
|
isAlive,
|
|
387
435
|
onTokenSuspect,
|
|
436
|
+
onChild,
|
|
388
437
|
}) {
|
|
438
|
+
// The intent this worker is holding across iterations. When a task parks on a
|
|
439
|
+
// blocker its worktree keeps uncommitted work; on the resume claim we must NOT
|
|
440
|
+
// reset it. Cleared once the task finishes or the worker goes idle.
|
|
441
|
+
let lastIntentId = null;
|
|
389
442
|
let phase = '';
|
|
390
443
|
const enter = (p, fn, msg) => {
|
|
391
444
|
if (phase !== p) {
|
|
@@ -441,13 +494,28 @@ export async function runLiveWorker({
|
|
|
441
494
|
}
|
|
442
495
|
let res;
|
|
443
496
|
try {
|
|
444
|
-
res = await runLiveTask({
|
|
497
|
+
res = await runLiveTask({
|
|
498
|
+
mcpUrl: getMcpUrl() ?? MCP_URL,
|
|
499
|
+
token,
|
|
500
|
+
cwd,
|
|
501
|
+
baseRef,
|
|
502
|
+
isAlive,
|
|
503
|
+
resumeIntentId: lastIntentId,
|
|
504
|
+
onChild,
|
|
505
|
+
});
|
|
445
506
|
} catch (e) {
|
|
446
507
|
enter('error', warn, `${c.yellow('error')} ${c.dim(`— ${e?.message ?? e}`)}`);
|
|
447
508
|
await sleep(IDLE_SECONDS);
|
|
448
509
|
continue;
|
|
449
510
|
}
|
|
450
511
|
if (!isAlive()) break;
|
|
512
|
+
// Keep the held intent only while a task is genuinely in flight (parked /
|
|
513
|
+
// stalled / errored → same worktree resumes). Finishing or finding no work
|
|
514
|
+
// clears it so the next fresh task starts from a clean base.
|
|
515
|
+
lastIntentId =
|
|
516
|
+
res.outcome === 'parked' || res.outcome === 'stalled' || res.outcome === 'error'
|
|
517
|
+
? res.intentId
|
|
518
|
+
: null;
|
|
451
519
|
if (res.outcome === 'nothing') {
|
|
452
520
|
enter('idle', info, 'idle — no work assigned');
|
|
453
521
|
await sleep(IDLE_SECONDS);
|
package/bin/lib/preview.mjs
CHANGED
|
@@ -137,14 +137,36 @@ export async function startPreview({ worktree, kind, cmd, port, log, timeoutMs =
|
|
|
137
137
|
const cf = await ensureCloudflared(log);
|
|
138
138
|
if (!cf) return null; // fall back to captured evidence
|
|
139
139
|
return new Promise((resolve) => {
|
|
140
|
-
|
|
140
|
+
// detached so each gets its own process group — `bun run dev` via a shell
|
|
141
|
+
// spawns a grandchild dev server that would otherwise SURVIVE a kill of the
|
|
142
|
+
// shell, keep port bound, and get silently re-fronted by the NEXT task's
|
|
143
|
+
// tunnel (reviewer sees the wrong branch). We kill the whole group instead.
|
|
144
|
+
const server = spawn(cmd, {
|
|
145
|
+
cwd: worktree,
|
|
146
|
+
shell: true,
|
|
147
|
+
detached: true,
|
|
148
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
149
|
+
});
|
|
141
150
|
const tunnel = spawn(cf, ['tunnel', '--url', `http://localhost:${port}`], {
|
|
151
|
+
detached: true,
|
|
142
152
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
143
153
|
});
|
|
144
154
|
let settled = false;
|
|
155
|
+
const killGroup = (child) => {
|
|
156
|
+
if (!child.pid) return;
|
|
157
|
+
try {
|
|
158
|
+
process.kill(-child.pid, 'SIGKILL'); // negative pid = the whole group
|
|
159
|
+
} catch {
|
|
160
|
+
try {
|
|
161
|
+
child.kill('SIGKILL');
|
|
162
|
+
} catch {
|
|
163
|
+
/* gone */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
145
167
|
const stop = () => {
|
|
146
|
-
|
|
147
|
-
|
|
168
|
+
killGroup(server);
|
|
169
|
+
killGroup(tunnel);
|
|
148
170
|
};
|
|
149
171
|
const finish = (val) => {
|
|
150
172
|
if (settled) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|