flowviant 0.47.0 → 0.47.2
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 +25 -0
- package/bin/lib/fleet.mjs +6 -0
- package/bin/lib/localSessions.mjs +90 -5
- package/bin/lib/prompts.mjs +35 -6
- package/bin/lib/work.mjs +53 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,31 @@ Flowviant only stores the tunnel URL; your browser talks to it directly.
|
|
|
68
68
|
| `FLOWVIANT_TOKENS=a,b,c` | a static fleet, one worktree each |
|
|
69
69
|
| `FLOWVIANT_SAFE=1` | restrict the toolset instead of running unattended |
|
|
70
70
|
|
|
71
|
+
## Security posture
|
|
72
|
+
|
|
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.
|
|
77
|
+
|
|
78
|
+
Two knobs bound the blast radius, and both are worth setting on a shared box:
|
|
79
|
+
|
|
80
|
+
- **Run the daemon under a dedicated OS user** that owns only the repository
|
|
81
|
+
checkout and `~/.flowviant`. This is the single biggest hardening available
|
|
82
|
+
— a session can then only touch that account's files, not your keys, your
|
|
83
|
+
home directory, or the rest of the machine. A plain separate account works;
|
|
84
|
+
a systemd unit with `ProtectHome=read-only` and `ReadWritePaths=` works
|
|
85
|
+
better.
|
|
86
|
+
- **`FLOWVIANT_SAFE=1`** narrows the toolset: Claude to an allowlist
|
|
87
|
+
(edit/read/search plus `git`/`gh`/`npm`/`bun` — no arbitrary shell), Codex
|
|
88
|
+
to a workspace-write sandbox. Antigravity has no per-invocation narrowing —
|
|
89
|
+
its permission engine is machine-wide — which is surfaced in the app rather
|
|
90
|
+
than papered over.
|
|
91
|
+
|
|
92
|
+
The posture is reported on every poll and shown in the project's
|
|
93
|
+
Settings → Machine section, so the team can see whether the box runs the
|
|
94
|
+
guarded toolset or full permissions.
|
|
95
|
+
|
|
71
96
|
## License
|
|
72
97
|
|
|
73
98
|
MIT — see [LICENSE](./LICENSE).
|
package/bin/lib/fleet.mjs
CHANGED
|
@@ -101,6 +101,12 @@ async function fetchRoster(haveIds) {
|
|
|
101
101
|
// our own package.json). Older servers ignore unknown params, so sending it
|
|
102
102
|
// unconditionally is always safe.
|
|
103
103
|
url.searchParams.set('dv', VERSION);
|
|
104
|
+
// The permission posture this machine runs turns under — '1' when
|
|
105
|
+
// FLOWVIANT_SAFE narrows the toolset, '0' when everything is granted. A
|
|
106
|
+
// statement of configuration, not a request: the app SHOWS it in Settings
|
|
107
|
+
// so a team can see whether the shared box runs wide open, and enforces
|
|
108
|
+
// nothing (membership is the consent boundary). Older servers ignore it.
|
|
109
|
+
url.searchParams.set('safe', SAFE ? '1' : '0');
|
|
104
110
|
// WHICH CLIs this machine actually has, so the app can stop guessing.
|
|
105
111
|
//
|
|
106
112
|
// Until now every surface that listed Gemini or Codex said "not wired up yet"
|
|
@@ -24,8 +24,66 @@ import {
|
|
|
24
24
|
import { homedir } from 'node:os';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
|
|
27
|
-
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
28
27
|
const REPORT_CAP = 30;
|
|
28
|
+
// ENDED sessions are the adoptable inventory, and the useful ones are FRESH:
|
|
29
|
+
// "closed my laptop terminal, picking it up here". Claude Code prunes its own
|
|
30
|
+
// history anyway, so a week-old row was a soon-to-be-dead offer — 48 hours,
|
|
31
|
+
// newest per directory, few. (The first ship reported 7 days of everything
|
|
32
|
+
// and the strip read as session history instead of presence.)
|
|
33
|
+
const ENDED_WINDOW_MS = 48 * 60 * 60 * 1000;
|
|
34
|
+
const ENDED_CAP = 5;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The conversation's own title, off the transcript's `ai-title` records
|
|
38
|
+
* (the LAST one wins — titles get rewritten as a session evolves), falling
|
|
39
|
+
* back to the first real user message. Those records sit anywhere in the
|
|
40
|
+
* file (measured: line 81 to line 4457), so this reads the WHOLE transcript
|
|
41
|
+
* — behind an mtime cache, because the scan runs every minute and a title
|
|
42
|
+
* only changes when the file does: steady state is a stat, not a read.
|
|
43
|
+
*/
|
|
44
|
+
const titleCache = new Map(); // file → { mtimeMs, title }
|
|
45
|
+
function transcriptTitle(file, mtimeMs) {
|
|
46
|
+
const hit = titleCache.get(file);
|
|
47
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.title;
|
|
48
|
+
let title = null;
|
|
49
|
+
try {
|
|
50
|
+
const stat = statSync(file);
|
|
51
|
+
// A transcript past this is not worth a read per minute of drift.
|
|
52
|
+
if (stat.size <= 64 * 1024 * 1024) {
|
|
53
|
+
let firstUser = null;
|
|
54
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
55
|
+
if (line.includes('"type":"ai-title"')) {
|
|
56
|
+
try {
|
|
57
|
+
const t = JSON.parse(line)?.aiTitle;
|
|
58
|
+
if (typeof t === 'string' && t.trim()) title = t.trim(); // last wins
|
|
59
|
+
} catch {
|
|
60
|
+
/* torn line */
|
|
61
|
+
}
|
|
62
|
+
} else if (!firstUser && !title && line.includes('"type":"user"') && !line.includes('"isMeta":true')) {
|
|
63
|
+
try {
|
|
64
|
+
const content = JSON.parse(line)?.message?.content;
|
|
65
|
+
const text =
|
|
66
|
+
typeof content === 'string'
|
|
67
|
+
? content
|
|
68
|
+
: Array.isArray(content)
|
|
69
|
+
? (content.find((b) => typeof b?.text === 'string')?.text ?? '')
|
|
70
|
+
: '';
|
|
71
|
+
if (text.trim() && !text.startsWith('<')) firstUser = text.trim();
|
|
72
|
+
} catch {
|
|
73
|
+
/* torn line */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!title && firstUser) title = firstUser;
|
|
78
|
+
if (title) title = title.replace(/\s+/g, ' ').slice(0, 120);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
title = null;
|
|
82
|
+
}
|
|
83
|
+
if (titleCache.size > 400) titleCache.clear(); // a bound, not an LRU — refills in one scan
|
|
84
|
+
titleCache.set(file, { mtimeMs, title });
|
|
85
|
+
return title;
|
|
86
|
+
}
|
|
29
87
|
|
|
30
88
|
/** Path-prefix containment on already-realpath'd absolute paths. */
|
|
31
89
|
const inside = (p, root) => p === root || p.startsWith(root.endsWith('/') ? root : `${root}/`);
|
|
@@ -182,12 +240,29 @@ export function scanLocalSessions({ repoRoot, excludeDirs = [] }) {
|
|
|
182
240
|
}
|
|
183
241
|
if (!ours(cwd)) continue;
|
|
184
242
|
liveIds.add(rec.sessionId);
|
|
243
|
+
// A live session's title, off its own transcript (the registry `name`
|
|
244
|
+
// is a machine-y fallback like "flowviant-35").
|
|
245
|
+
let liveTitle = null;
|
|
246
|
+
try {
|
|
247
|
+
const liveFile = join(
|
|
248
|
+
homedir(),
|
|
249
|
+
'.claude',
|
|
250
|
+
'projects',
|
|
251
|
+
cwd.replace(/[/.]/g, '-'),
|
|
252
|
+
`${rec.sessionId}.jsonl`
|
|
253
|
+
);
|
|
254
|
+
liveTitle = transcriptTitle(liveFile, statSync(liveFile).mtimeMs);
|
|
255
|
+
} catch {
|
|
256
|
+
/* no transcript yet */
|
|
257
|
+
}
|
|
258
|
+
if (!liveTitle && typeof rec.name === 'string' && rec.name.trim()) liveTitle = rec.name.trim();
|
|
185
259
|
live.push({
|
|
186
260
|
id: rec.sessionId,
|
|
187
261
|
cwd,
|
|
188
262
|
live: true,
|
|
189
263
|
lastActiveAt: nowIso,
|
|
190
264
|
...(typeof rec.gitBranch === 'string' && rec.gitBranch ? { branch: rec.gitBranch } : {}),
|
|
265
|
+
...(liveTitle ? { title: liveTitle } : {}),
|
|
191
266
|
});
|
|
192
267
|
}
|
|
193
268
|
live.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
@@ -207,7 +282,7 @@ export function scanLocalSessions({ repoRoot, excludeDirs = [] }) {
|
|
|
207
282
|
} catch {
|
|
208
283
|
/* no transcript store — live sessions still report */
|
|
209
284
|
}
|
|
210
|
-
const cutoff = Date.now() -
|
|
285
|
+
const cutoff = Date.now() - ENDED_WINDOW_MS;
|
|
211
286
|
const candidates = [];
|
|
212
287
|
for (const dirName of projDirs) {
|
|
213
288
|
if (dirName !== munged && !dirName.startsWith(`${munged}-`)) continue;
|
|
@@ -228,7 +303,7 @@ export function scanLocalSessions({ repoRoot, excludeDirs = [] }) {
|
|
|
228
303
|
} catch {
|
|
229
304
|
continue;
|
|
230
305
|
}
|
|
231
|
-
if (mtimeMs < cutoff) continue; //
|
|
306
|
+
if (mtimeMs < cutoff) continue; // an aged session is history, not presence
|
|
232
307
|
candidates.push({ id, file, mtimeMs });
|
|
233
308
|
}
|
|
234
309
|
}
|
|
@@ -236,8 +311,14 @@ export function scanLocalSessions({ repoRoot, excludeDirs = [] }) {
|
|
|
236
311
|
// the verification read is the expensive step, so it is not spent on
|
|
237
312
|
// sessions the report would drop anyway.
|
|
238
313
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.id < b.id ? -1 : 1));
|
|
239
|
-
|
|
314
|
+
// ONE row per DIRECTORY, newest first, few: twenty sessions in the repo
|
|
315
|
+
// root are one offer — the newest is the one `--resume`'s picker would
|
|
316
|
+
// reach for and the only one worth importing 95% of the time. The rest
|
|
317
|
+
// are scrollback, and the product's own law says scrollback doesn't
|
|
318
|
+
// matter.
|
|
319
|
+
const room = Math.min(ENDED_CAP, Math.max(0, REPORT_CAP - Math.min(live.length, REPORT_CAP)));
|
|
240
320
|
const endedIds = new Set();
|
|
321
|
+
const seenCwds = new Set(live.map((s) => s.cwd));
|
|
241
322
|
for (const cand of candidates) {
|
|
242
323
|
if (ended.length >= room) break;
|
|
243
324
|
if (endedIds.has(cand.id)) continue; // one row per session, whatever dir names it
|
|
@@ -251,12 +332,16 @@ export function scanLocalSessions({ repoRoot, excludeDirs = [] }) {
|
|
|
251
332
|
continue;
|
|
252
333
|
}
|
|
253
334
|
if (!ours(cwd)) continue;
|
|
335
|
+
if (seenCwds.has(cwd)) continue; // newest per directory; a live one owns its cwd
|
|
336
|
+
seenCwds.add(cwd);
|
|
337
|
+
const title = transcriptTitle(cand.file, cand.mtimeMs);
|
|
254
338
|
ended.push({
|
|
255
339
|
id: cand.id,
|
|
256
340
|
cwd,
|
|
257
341
|
live: false,
|
|
258
342
|
lastActiveAt: new Date(cand.mtimeMs).toISOString(),
|
|
259
343
|
...(typeof rec.gitBranch === 'string' && rec.gitBranch ? { branch: rec.gitBranch } : {}),
|
|
344
|
+
...(title ? { title } : {}),
|
|
260
345
|
});
|
|
261
346
|
}
|
|
262
347
|
} catch {
|
|
@@ -365,7 +450,7 @@ function scanAgyConversations({ repoRoot, excludeDirs = [] }) {
|
|
|
365
450
|
const raw = readFileSync(join(AGY_DIR(), 'cache', 'last_conversations.json'), 'utf8');
|
|
366
451
|
const map = JSON.parse(raw);
|
|
367
452
|
if (!map || typeof map !== 'object') return out;
|
|
368
|
-
const cutoff = Date.now() -
|
|
453
|
+
const cutoff = Date.now() - ENDED_WINDOW_MS;
|
|
369
454
|
const processUp = agyProcessAlive();
|
|
370
455
|
for (const [cwd, id] of Object.entries(map)) {
|
|
371
456
|
if (typeof id !== 'string' || !AGY_UUID_RE.test(id)) continue;
|
package/bin/lib/prompts.mjs
CHANGED
|
@@ -458,26 +458,40 @@ MECHANICS OF THIS TAB:
|
|
|
458
458
|
4. NEVER merge to main, deploy, or force-push unless the human explicitly says
|
|
459
459
|
so in this conversation. Branch pushes and PRs are fine when asked. Shipping
|
|
460
460
|
is their word to say, not yours to infer.
|
|
461
|
+
5. WHEN THEY HAVE TO CHOOSE, HAND THEM THE CHOICES. A real pick between known
|
|
462
|
+
options — not an open question — ends your reply with a fenced block the app
|
|
463
|
+
renders as buttons; their click composes their answer as the next message:
|
|
464
|
+
|
|
465
|
+
\`\`\`flowviant-ask
|
|
466
|
+
{"question": "Which auth flow?", "options": ["Magic link", "Password", "Both"], "multiSelect": false}
|
|
467
|
+
\`\`\`
|
|
468
|
+
|
|
469
|
+
ONE block per reply, and always the LAST thing in it. Two to eight options,
|
|
470
|
+
each label short enough to sit on a button. multiSelect true only for a
|
|
471
|
+
genuine check-several-of-these case. NEVER for an open question — ask those
|
|
472
|
+
in prose, like anyone would. And ask the question in prose above the block
|
|
473
|
+
as well: a client that doesn't render the fence shows it as plain text, so
|
|
474
|
+
the reply has to read as a question with its options either way.
|
|
461
475
|
|
|
462
476
|
THE LEDGER. This session's work is logged as CARDS as it happens, by you,
|
|
463
477
|
through tools — so a four-hour churn doesn't evaporate into scrollback. The
|
|
464
478
|
rules:
|
|
465
479
|
|
|
466
|
-
|
|
480
|
+
6. CLAIM WHAT YOU WORK. When they say "take the auth card" or "next", call
|
|
467
481
|
list_cards, then claim_card the one they mean. The card you hold is the
|
|
468
482
|
tab's "Now" — it is how they and their team see what this session is doing.
|
|
469
|
-
|
|
483
|
+
7. LOG DRIFT, don't ask permission for it. "Also fix that redirect" mid-flow:
|
|
470
484
|
do the work, and file_card it — check list_cards FIRST; if a planned card
|
|
471
485
|
already covers it, claim that one instead of filing a twin. One card per
|
|
472
486
|
shippable unit. Never card-ify chatter, questions, or exploration.
|
|
473
|
-
|
|
487
|
+
8. DELIVER WITH RECEIPTS. When a card's work is committed, deliver_card with a
|
|
474
488
|
one-paragraph summary and the commit shas. Delivered is ASSERTED; done is
|
|
475
489
|
OBSERVED (the merge, on their word). Never claim done, and never deliver
|
|
476
490
|
work that isn't committed.
|
|
477
|
-
|
|
491
|
+
9. RAISE WHAT YOU SPOT. A design flaw, a follow-up they named for later —
|
|
478
492
|
raise_card, queued, unheld. You do not start raised work.
|
|
479
|
-
|
|
480
|
-
|
|
493
|
+
10. BE PROPORTIONAL. A one-line typo fix inside the card you already hold is
|
|
494
|
+
that card's work, not a new card. When in doubt, fewer cards.
|
|
481
495
|
|
|
482
496
|
POSTURE: terminal, not ticket. Don't ask permission to look at things. Don't
|
|
483
497
|
narrate ceremony. Ground claims in files you opened. When they ask a question,
|
|
@@ -514,6 +528,21 @@ MECHANICS OF THIS TAB:
|
|
|
514
528
|
4. NEVER merge to main, deploy, or force-push unless the human explicitly says
|
|
515
529
|
so in this conversation. Branch pushes are fine when asked. Shipping is
|
|
516
530
|
their word to say, not yours to infer.
|
|
531
|
+
5. WHEN THEY HAVE TO CHOOSE, HAND THEM THE CHOICES. You have no tools here, but
|
|
532
|
+
this one costs none — it is text. A real pick between known options (not an
|
|
533
|
+
open question) ends your reply with a fenced block the app renders as
|
|
534
|
+
buttons; their click composes their answer as the next message:
|
|
535
|
+
|
|
536
|
+
\`\`\`flowviant-ask
|
|
537
|
+
{"question": "Which auth flow?", "options": ["Magic link", "Password", "Both"], "multiSelect": false}
|
|
538
|
+
\`\`\`
|
|
539
|
+
|
|
540
|
+
ONE block per reply, and always the LAST thing in it. Two to eight options,
|
|
541
|
+
each label short enough to sit on a button. multiSelect true only for a
|
|
542
|
+
genuine check-several-of-these case. NEVER for an open question — ask those
|
|
543
|
+
in prose, like anyone would. And ask the question in prose above the block
|
|
544
|
+
as well: a client that doesn't render the fence shows it as plain text, so
|
|
545
|
+
the reply has to read as a question with its options either way.
|
|
517
546
|
|
|
518
547
|
POSTURE: terminal, not ticket. Don't ask permission to look at things. Ground
|
|
519
548
|
claims in files you opened. When they ask a question, answer it; when they ask
|
package/bin/lib/work.mjs
CHANGED
|
@@ -45,6 +45,45 @@ import { detectRuntimes, canRun, RUNTIMES } from './runtimes.mjs';
|
|
|
45
45
|
import { isTerminalSessionLive, isAgyConversationLive } from './localSessions.mjs';
|
|
46
46
|
import { homedir } from 'node:os';
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The shape a per-tab model name must have before it rides argv as
|
|
50
|
+
* `--model <name>`. Conservative for the same reason the codex thread id is
|
|
51
|
+
* (below): it comes off the wire and lands in a child process's arguments —
|
|
52
|
+
* alphanumerics plus dot/dash/underscore, at most 40 characters, and NEVER a
|
|
53
|
+
* leading dash, which is an argv that parses as a flag.
|
|
54
|
+
*/
|
|
55
|
+
const WORK_MODEL_RE = /^[a-zA-Z0-9._][a-zA-Z0-9._-]{0,39}$/;
|
|
56
|
+
|
|
57
|
+
/** The five efforts the CLIs actually accept. A literal set rather than a
|
|
58
|
+
* pattern: there is no such thing as an effort we haven't heard of, and the
|
|
59
|
+
* server's own union is exactly this list. */
|
|
60
|
+
const WORK_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* WHICH BRAIN, AT WHICH EFFORT — the tab's own pick, off the roster.
|
|
64
|
+
*
|
|
65
|
+
* Absent is the resting state and it must stay genuinely absent: every tab ran
|
|
66
|
+
* with no `--model` and no `--effort` until now, so a job that names neither
|
|
67
|
+
* has to produce the byte-identical argv it produced yesterday — Claude falling
|
|
68
|
+
* back to the machine's MODEL pin, codex and agy to their own defaults. Hence
|
|
69
|
+
* an object with the key MISSING rather than one holding null: a null would
|
|
70
|
+
* reach the builders as a value and Claude's `model || MODEL` is the only one
|
|
71
|
+
* that would survive it.
|
|
72
|
+
*
|
|
73
|
+
* A value that fails its guard is DROPPED, not passed through and not an error.
|
|
74
|
+
* The honest outcome of "the server named a model this machine can't spell" is
|
|
75
|
+
* the machine's own default — a turn that runs — rather than a flag no CLI
|
|
76
|
+
* understands and a tab that fails every message.
|
|
77
|
+
*/
|
|
78
|
+
function brainFor(job) {
|
|
79
|
+
const out = {};
|
|
80
|
+
const model = typeof job?.model === 'string' ? job.model.trim() : '';
|
|
81
|
+
if (model && WORK_MODEL_RE.test(model)) out.model = model;
|
|
82
|
+
const effort = typeof job?.effort === 'string' ? job.effort.trim() : '';
|
|
83
|
+
if (effort && WORK_EFFORTS.has(effort)) out.effort = effort;
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
48
87
|
export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLeaseTtl }) {
|
|
49
88
|
const WORK_TOKEN_URL = FLEET_URL.replace(/\/agents\/?$/, '/work-token');
|
|
50
89
|
const WORK_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/work-turn-done');
|
|
@@ -869,9 +908,20 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
869
908
|
// in the prompt, so the AGENT tells the user what stayed behind.
|
|
870
909
|
let carryNote = '';
|
|
871
910
|
if (adopting && dir.fresh && adoptSrc) carryNote = carryDirtyState(adoptSrc, dir.wt);
|
|
911
|
+
// The tab's transcript starts EMPTY on adoption (scrollback is
|
|
912
|
+
// disposable, the held context is the brain — never import an
|
|
913
|
+
// archive), so the first reply opens with a recap: the human sees
|
|
914
|
+
// the thread they are picking up without asking for it.
|
|
915
|
+
const adoptNote = adopting
|
|
916
|
+
? '[ADOPTED SESSION — this conversation was brought in from a terminal. Begin your reply with a 2-3 sentence recap of where it left off and what state carried over, then answer the message.]'
|
|
917
|
+
: '';
|
|
872
918
|
const mcp = plainTab
|
|
873
919
|
? { args: [], env: null, dir: null }
|
|
874
920
|
: mcpFor(rt.id, mint.token, getMcpUrl());
|
|
921
|
+
// The tab's model/effort, if it named any. Spread into turnArgs so
|
|
922
|
+
// BOTH runTurn calls below carry it — the retry is the same turn on
|
|
923
|
+
// the same brain, not a quieter second opinion.
|
|
924
|
+
const brain = brainFor(job);
|
|
875
925
|
// Attempts count RUNS: the infra refusals above consumed nothing and
|
|
876
926
|
// settled on their own terms.
|
|
877
927
|
workAttempts.set(job.id, tries + 1);
|
|
@@ -879,7 +929,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
879
929
|
let seenThreadId = null; // codex's conversation id, off thread.started
|
|
880
930
|
const spawned = []; // this turn's children, for the teardown registry
|
|
881
931
|
try {
|
|
882
|
-
const message =
|
|
932
|
+
const message = [job.body, adoptNote, carryNote].filter(Boolean).join('\n\n');
|
|
883
933
|
const turnArgs = {
|
|
884
934
|
// A plain tab has no tools to name and no session id to pass —
|
|
885
935
|
// its kickoff asks for one complete report instead of a stream.
|
|
@@ -901,6 +951,8 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
901
951
|
// ordinary --continue resume path, unchanged.
|
|
902
952
|
...(adopting ? { adoptResumeId: job.adopt.id } : {}),
|
|
903
953
|
system: plainTab ? SYSTEM_WORK_PLAIN : SYSTEM_WORK,
|
|
954
|
+
// Present only when the tab named one — see brainFor.
|
|
955
|
+
...brain,
|
|
904
956
|
cwd: dir.wt,
|
|
905
957
|
mcpArgs: mcp.args,
|
|
906
958
|
mcpEnv: mcp.env,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.2",
|
|
4
4
|
"description": "Run your own coding CLIs as headless build agents for Flowviant — Claude Code or Codex, 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": {
|