flowviant 0.51.2 → 0.52.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 +16 -3
- package/bin/lib/fleet.mjs +16 -1
- package/bin/lib/prompts.mjs +62 -13
- package/bin/lib/runtimes.mjs +47 -0
- package/bin/lib/work.mjs +7 -1
- package/package.json +1 -1
package/bin/lib/claude.mjs
CHANGED
|
@@ -197,7 +197,7 @@ const oneLine = (s, n = 160) => String(s).replace(/\s+/g, ' ').trim().slice(0, n
|
|
|
197
197
|
// every intermediate text block still NARRATES, but only the final `result`
|
|
198
198
|
// event contributes text — otherwise the same sentences arrive twice, once as
|
|
199
199
|
// they stream and once in the result, and the tab posts the duplicate.
|
|
200
|
-
function handleStreamLine(line, { cwd, emit, onActivity, appendText, answerFromResult }) {
|
|
200
|
+
function handleStreamLine(line, { cwd, emit, onActivity, appendText, answerFromResult, onInit }) {
|
|
201
201
|
let ev;
|
|
202
202
|
try {
|
|
203
203
|
ev = JSON.parse(line);
|
|
@@ -224,6 +224,19 @@ function handleStreamLine(line, { cwd, emit, onActivity, appendText, answerFromR
|
|
|
224
224
|
push(humanizeToolUse(b.name, b.input || {}, cwd));
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
|
+
} else if (ev.type === 'system' && ev.subtype === 'init') {
|
|
228
|
+
// WHAT THIS MACHINE'S CLI CAN BE ASKED FOR BY NAME. The init event is the
|
|
229
|
+
// CLI's OWN answer — it has already resolved personal skills, this repo's
|
|
230
|
+
// skills, plugins and whatever the project settings enable or disable — so
|
|
231
|
+
// reading it costs nothing and cannot drift the way a `~/.claude/skills`
|
|
232
|
+
// scan of our own would. `skills` (rather than `slash_commands`) is the
|
|
233
|
+
// deliberate narrowing: the 50-odd commands beside it are the CLI's own
|
|
234
|
+
// interactive furniture (/clear, /model, /compact), and offering those in a
|
|
235
|
+
// relayed tab would be an offer wired to nothing.
|
|
236
|
+
//
|
|
237
|
+
// Only ever REPORTED, never enforced. Flowviant does not decide what your
|
|
238
|
+
// Claude can do; it relays what your Claude said it has.
|
|
239
|
+
if (Array.isArray(ev.skills)) onInit?.({ skills: ev.skills.map(String) });
|
|
227
240
|
} else if (ev.type === 'result') {
|
|
228
241
|
// The final assistant text (carries WIKI_DONE / REGROUND_DONE).
|
|
229
242
|
if (typeof ev.result === 'string') appendText(ev.result + '\n');
|
|
@@ -247,7 +260,7 @@ function handleStreamLine(line, { cwd, emit, onActivity, appendText, answerFromR
|
|
|
247
260
|
// returned string for sentinel detection, and each activity is handed to
|
|
248
261
|
// `onActivity` so the caller can forward progress. Build-agent turns leave it
|
|
249
262
|
// off and keep the raw text passthrough + line sentinels.
|
|
250
|
-
export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onThreadId, wikiPerm, readOnly, planPerm, vaultDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId }) {
|
|
263
|
+
export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onInit, onThreadId, wikiPerm, readOnly, planPerm, vaultDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId }) {
|
|
251
264
|
return new Promise((resolve) => {
|
|
252
265
|
const rt = runtimeById(runtime);
|
|
253
266
|
if (!rt.args) {
|
|
@@ -359,7 +372,7 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
|
|
|
359
372
|
/** One line of the child's stdout, in whichever dialect it speaks. */
|
|
360
373
|
const onLine = (line) => {
|
|
361
374
|
if (!rt.parse)
|
|
362
|
-
return handleStreamLine(line, { cwd, emit, onActivity, appendText, answerFromResult });
|
|
375
|
+
return handleStreamLine(line, { cwd, emit, onActivity, appendText, answerFromResult, onInit });
|
|
363
376
|
const ev = rt.parse(line, cwd);
|
|
364
377
|
if (!ev) return;
|
|
365
378
|
// The conversation id, when the runtime announces one (codex's
|
package/bin/lib/fleet.mjs
CHANGED
|
@@ -73,7 +73,7 @@ import {
|
|
|
73
73
|
} from './env.mjs';
|
|
74
74
|
import { processDeployJobs, reportDeployConfig } from './deploy.mjs';
|
|
75
75
|
import { machineSnapshot } from './resources.mjs';
|
|
76
|
-
import { detectRuntimes, pickRuntimeFor, RUNTIMES } from './runtimes.mjs';
|
|
76
|
+
import { detectRuntimes, knownSkills, pickRuntimeFor, RUNTIMES } from './runtimes.mjs';
|
|
77
77
|
import { createWorkManager } from './work.mjs';
|
|
78
78
|
import { scanLocalSessions } from './localSessions.mjs';
|
|
79
79
|
|
|
@@ -118,6 +118,21 @@ async function fetchRoster(haveIds) {
|
|
|
118
118
|
} catch {
|
|
119
119
|
/* detection is best-effort — a probe must never fail the poll */
|
|
120
120
|
}
|
|
121
|
+
// WHAT THE CLI CAN BE ASKED FOR BY NAME, so the composer can autocomplete a
|
|
122
|
+
// `/` the way the terminal does. Learned from the init event of a turn we
|
|
123
|
+
// already ran (runtimes.mjs) — never probed, because spawning a CLI to fill a
|
|
124
|
+
// dropdown would spend the operator's quota on an affordance.
|
|
125
|
+
//
|
|
126
|
+
// NOT SENT until a turn has taught us: absent means "no turn has run here
|
|
127
|
+
// yet", and the app renders no menu rather than asserting this machine has no
|
|
128
|
+
// skills. An empty report, though, IS a fact and is sent as such — hence the
|
|
129
|
+
// null check rather than a truthiness check on the array.
|
|
130
|
+
try {
|
|
131
|
+
const skills = knownSkills();
|
|
132
|
+
if (skills !== null) url.searchParams.set('skills', skills.join(','));
|
|
133
|
+
} catch {
|
|
134
|
+
/* best-effort — the poll must never fail on a readout */
|
|
135
|
+
}
|
|
121
136
|
// Env-sync identity + materialized version (the Settings "env vN" chip).
|
|
122
137
|
try {
|
|
123
138
|
for (const [k, v] of Object.entries(await envQueryParams())) {
|
package/bin/lib/prompts.mjs
CHANGED
|
@@ -397,24 +397,73 @@ for work, do it.
|
|
|
397
397
|
|
|
398
398
|
Write plain Markdown for a person reading your reply in a chat tab.`;
|
|
399
399
|
|
|
400
|
+
/**
|
|
401
|
+
* A LEADING SLASH COMMAND, which the CLI will only expand at position 0.
|
|
402
|
+
*
|
|
403
|
+
* Claude Code parses `/name …` as a command ONLY when it opens the prompt. Every
|
|
404
|
+
* turn here wraps the human's words in the scaffolding below, so a `/code-review`
|
|
405
|
+
* typed into a tab used to arrive on line 6 of a fenced block — inert text that
|
|
406
|
+
* looked like it should have worked. That is the product telling you no for
|
|
407
|
+
* bookkeeping reasons, which is the one thing it never does.
|
|
408
|
+
*
|
|
409
|
+
* SHAPE, NOT MEMBERSHIP. We do not check the name against the machine's skill
|
|
410
|
+
* list: that list is only learned after a turn has run (runtimes.mjs), so
|
|
411
|
+
* gating on it would make the first `/foo` of a machine's life behave
|
|
412
|
+
* differently from the second. Instead this matches what a command can LOOK
|
|
413
|
+
* like — one segment, no second slash — which leaves `/home/user/x.ts is
|
|
414
|
+
* broken` fenced as the prose it is. Measured on 2.1.238: an unknown command
|
|
415
|
+
* is treated as ordinary text, so a false positive costs nothing anyway.
|
|
416
|
+
*/
|
|
417
|
+
const LEADING_SLASH_COMMAND = /^\/[A-Za-z0-9][A-Za-z0-9_:-]*(?=\s|$)/;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The kickoff, in the two orders it can be written.
|
|
421
|
+
*
|
|
422
|
+
* ORDINARY: scaffolding first, the human's words fenced inside it. The speaker
|
|
423
|
+
* is the tab's OWNER — the same person who owns this machine — so this is the
|
|
424
|
+
* one prompt whose author is fully trusted. The fence stays anyway: it costs
|
|
425
|
+
* nothing and keeps the shape identical everywhere, and repo content this turn
|
|
426
|
+
* READS is as untrusted as ever.
|
|
427
|
+
*
|
|
428
|
+
* SLASH: the human's words go FIRST, verbatim and unfenced, because that is the
|
|
429
|
+
* only position the CLI expands a command from — and the scaffolding follows,
|
|
430
|
+
* LABELLED as ours so the trailing lines cannot read as more of what the person
|
|
431
|
+
* typed. The fence is what is traded away, and only for the one author already
|
|
432
|
+
* trusted above; nothing else about the turn changes.
|
|
433
|
+
*/
|
|
434
|
+
const kickoff = ({ message, askedByName, head, tail }) => {
|
|
435
|
+
const scaffold =
|
|
436
|
+
`${head}\n\n` +
|
|
437
|
+
`${fence('WHO IS TALKING', askedByName || 'the tab owner')}\n\n`;
|
|
438
|
+
if (LEADING_SLASH_COMMAND.test(message.trim()))
|
|
439
|
+
return (
|
|
440
|
+
`${message.trim()}\n\n` +
|
|
441
|
+
`---\n` +
|
|
442
|
+
`[FLOWVIANT SESSION CONTEXT — written by Flowviant, not typed by the person above]\n` +
|
|
443
|
+
`${scaffold}${tail}`
|
|
444
|
+
);
|
|
445
|
+
return `${scaffold}${fence('WHAT THEY SAID', message)}\n\n${tail}`;
|
|
446
|
+
};
|
|
447
|
+
|
|
400
448
|
export const WORK_TURN_KICKOFF = ({ sessionId, sessionName, message, askedByName }) =>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
`Stream your reply with stream_session_turn as you work.`;
|
|
449
|
+
kickoff({
|
|
450
|
+
message,
|
|
451
|
+
askedByName,
|
|
452
|
+
head:
|
|
453
|
+
`Continue the session${sessionName ? ` "${sessionName}"` : ''}.\n\n` +
|
|
454
|
+
`SESSION ID (pass this to stream_session_turn / update_session): ${sessionId}`,
|
|
455
|
+
tail: `Stream your reply with stream_session_turn as you work.`,
|
|
456
|
+
});
|
|
410
457
|
|
|
411
458
|
/** The plain tab's kickoff: no session id (there is no tool to pass it to)
|
|
412
459
|
* and no streaming instruction — the final message is the reply. */
|
|
413
460
|
export const WORK_TURN_KICKOFF_PLAIN = ({ sessionName, message, askedByName }) =>
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
461
|
+
kickoff({
|
|
462
|
+
message,
|
|
463
|
+
askedByName,
|
|
464
|
+
head: `Continue the session${sessionName ? ` "${sessionName}"` : ''}.`,
|
|
465
|
+
tail: `Reply with your complete report when the work is done.`,
|
|
466
|
+
});
|
|
418
467
|
|
|
419
468
|
|
|
420
469
|
export const REGROUND_KICKOFF = ({ sha, title, files, vaultDir, predictedPages = [] }) =>
|
package/bin/lib/runtimes.mjs
CHANGED
|
@@ -903,3 +903,50 @@ export function detectRuntimes({ refresh = false } = {}) {
|
|
|
903
903
|
});
|
|
904
904
|
return detectedCache;
|
|
905
905
|
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* WHAT THE CLI SAID IT CAN BE ASKED FOR BY NAME — the machine's skills.
|
|
909
|
+
*
|
|
910
|
+
* Learned, never scanned. Claude Code's `system.init` event names its own
|
|
911
|
+
* resolved skill set on every stream-json turn, and the daemon already parses
|
|
912
|
+
* that stream (claude.mjs), so this costs nothing and is authoritative: it has
|
|
913
|
+
* plugins, this repo's `.claude/skills`, and whatever project settings enabled
|
|
914
|
+
* or disabled already folded in. A `~/.claude/skills` scan of our own would be
|
|
915
|
+
* a second implementation of the CLI's resolution rules, and would drift.
|
|
916
|
+
*
|
|
917
|
+
* THE PRICE OF LEARNING RATHER THAN PROBING is that a machine which has not run
|
|
918
|
+
* a turn yet knows nothing, and says nothing. That is the honest answer: the
|
|
919
|
+
* app renders no menu rather than an empty one, and a slash typed into a tab
|
|
920
|
+
* still reaches the CLI either way — the menu is an autocomplete, never a gate.
|
|
921
|
+
* We do NOT probe for it: a `claude -p` run purely to populate a dropdown would
|
|
922
|
+
* spend the operator's quota on a UI affordance.
|
|
923
|
+
*
|
|
924
|
+
* PER MACHINE, not per session. Every session worktree is a checkout of the one
|
|
925
|
+
* repo this daemon serves, so project skills are identical across tabs and
|
|
926
|
+
* personal skills are machine-wide. Last turn wins, which is what makes a skill
|
|
927
|
+
* added mid-run show up on the next poll.
|
|
928
|
+
*/
|
|
929
|
+
let skillsCache = null;
|
|
930
|
+
|
|
931
|
+
/** Claude Code's own names: letters, digits, dash, underscore, and the colon a
|
|
932
|
+
* plugin skill wears (`plugin:skill`). Anything else is not a name we could
|
|
933
|
+
* put after a `/` anyway, so it is dropped rather than relayed as garbage. */
|
|
934
|
+
const SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9_:-]{0,63}$/;
|
|
935
|
+
|
|
936
|
+
/** Record what a turn's init event reported. Bounded and sorted so the poll's
|
|
937
|
+
* query param has a stable length and a stable order — an unstable order would
|
|
938
|
+
* make the server write a "change" on every single poll. */
|
|
939
|
+
export function recordSkills(names) {
|
|
940
|
+
if (!Array.isArray(names)) return;
|
|
941
|
+
const clean = [...new Set(names.map((n) => String(n).trim()).filter((n) => SKILL_NAME.test(n)))]
|
|
942
|
+
.sort()
|
|
943
|
+
.slice(0, 100);
|
|
944
|
+
// An empty report is a FACT (a machine with no skills installed), so it is
|
|
945
|
+
// recorded as []. Never conflated with null, which stays "no turn has run".
|
|
946
|
+
skillsCache = clean;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** What to send on the roster poll — null until a turn has taught us. */
|
|
950
|
+
export function knownSkills() {
|
|
951
|
+
return skillsCache;
|
|
952
|
+
}
|
package/bin/lib/work.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
WORK_TURN_KICKOFF_PLAIN,
|
|
42
42
|
} from './prompts.mjs';
|
|
43
43
|
import { materializeInto, excludeInWorktree, scrub as envScrub } from './env.mjs';
|
|
44
|
-
import { detectRuntimes, canRun, RUNTIMES } from './runtimes.mjs';
|
|
44
|
+
import { detectRuntimes, canRun, recordSkills, RUNTIMES } from './runtimes.mjs';
|
|
45
45
|
import { isTerminalSessionLive, isAgyConversationLive } from './localSessions.mjs';
|
|
46
46
|
import { worktreeDiff } from './worktreeDiff.mjs';
|
|
47
47
|
import { homedir } from 'node:os';
|
|
@@ -1353,6 +1353,12 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
1353
1353
|
streamJson: true,
|
|
1354
1354
|
answerFromResult: true,
|
|
1355
1355
|
onActivity: (a) => narrator.line(a?.label),
|
|
1356
|
+
// What this CLI says it can be asked for by name. Harvested off
|
|
1357
|
+
// the init event the stream already carries — no probe, no scan,
|
|
1358
|
+
// no extra spawn — and reported on the next roster poll so the
|
|
1359
|
+
// composer can autocomplete a `/`. See runtimes.mjs for why it is
|
|
1360
|
+
// learned from a turn rather than looked up.
|
|
1361
|
+
onInit: (i) => recordSkills(i.skills),
|
|
1356
1362
|
cwd: dir.wt,
|
|
1357
1363
|
mcpArgs: mcp.args,
|
|
1358
1364
|
mcpEnv: mcp.env,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|