flowviant 0.95.0 → 0.97.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/README.md +2 -2
- package/bin/cli.mjs +31 -16
- package/bin/lib/artifacts.mjs +38 -7
- package/bin/lib/claude.mjs +241 -4
- package/bin/lib/credentials.mjs +39 -0
- package/bin/lib/env.mjs +17 -0
- package/bin/lib/fleet.mjs +26 -2
- package/bin/lib/hooks/readGuard.mjs +138 -0
- package/bin/lib/knowledge.mjs +330 -13
- package/bin/lib/login.mjs +16 -73
- package/bin/lib/machines.mjs +6 -6
- package/bin/lib/prompts.mjs +339 -22
- package/bin/lib/runtimes.mjs +81 -2
- package/bin/lib/work.mjs +193 -16
- package/package.json +1 -1
package/bin/lib/runtimes.mjs
CHANGED
|
@@ -543,7 +543,12 @@ export const RUNTIMES = {
|
|
|
543
543
|
* "consult" and "plan" currently mean. That is a statement about where the
|
|
544
544
|
* contract was written, not a claim that only Claude could ever satisfy it.
|
|
545
545
|
*/
|
|
546
|
-
|
|
546
|
+
// `design` and `research` (0.97.0) are Claude's alone: each is an
|
|
547
|
+
// `--allowedTools` list with a PATH-SCOPED write (claude.mjs), and neither
|
|
548
|
+
// codex's sandbox modes nor agy's flags were measured to express "write
|
|
549
|
+
// only this directory". A runtime that does not declare them is refused
|
|
550
|
+
// such a card before spawn, in words — never handed a build turn instead.
|
|
551
|
+
profiles: ['build', 'wiki', 'consult', 'plan', 'design', 'research'],
|
|
547
552
|
mcp: claudeMcp,
|
|
548
553
|
/**
|
|
549
554
|
* Claude takes the operating contract as a real system prompt, which is the
|
|
@@ -1040,7 +1045,16 @@ export const runtimeById = (id) => RUNTIMES[id] ?? RUNTIMES.claude;
|
|
|
1040
1045
|
* the single drivability test, so a machine-wide MCP config disqualified it from
|
|
1041
1046
|
* two jobs that never open an MCP connection.
|
|
1042
1047
|
*/
|
|
1043
|
-
const PROFILE_NEEDS_MCP = {
|
|
1048
|
+
const PROFILE_NEEDS_MCP = {
|
|
1049
|
+
build: true,
|
|
1050
|
+
wiki: false,
|
|
1051
|
+
consult: false,
|
|
1052
|
+
plan: true,
|
|
1053
|
+
// An agent turn has no MCP at all (SYSTEM_AGENT's header) — everything it
|
|
1054
|
+
// says rides the final JSON — so neither non-code posture needs one.
|
|
1055
|
+
design: false,
|
|
1056
|
+
research: false,
|
|
1057
|
+
};
|
|
1044
1058
|
|
|
1045
1059
|
/**
|
|
1046
1060
|
* A build needs the control plane, but NOT necessarily an MCP config of its own.
|
|
@@ -1209,6 +1223,68 @@ export function knownSkills() {
|
|
|
1209
1223
|
return skillsCache;
|
|
1210
1224
|
}
|
|
1211
1225
|
|
|
1226
|
+
/**
|
|
1227
|
+
* WHICH MCP SERVERS AND CLAUDE.AI CONNECTORS THE CLI MOUNTED, AND HOW EACH
|
|
1228
|
+
* STANDS (2026-09-23, 0.97.0) — learned off the same `system.init` event the
|
|
1229
|
+
* skills are, so it costs nothing and cannot drift from what a turn reaches.
|
|
1230
|
+
*
|
|
1231
|
+
* MEASURED on Claude Code 2.1.281 (`claude -p hi --model haiku --output-format
|
|
1232
|
+
* stream-json --verbose`): the init event carries `mcp_servers: [{ name,
|
|
1233
|
+
* status, source }]` — statuses seen `connected`, `failed`, `needs-auth` and
|
|
1234
|
+
* `pending`; a claude.ai connector arrives named `claude.ai <Name>` with
|
|
1235
|
+
* `source: "claudeai"`, a server a turn mounts with `--mcp-config` with
|
|
1236
|
+
* `source: "dynamic"`. A connector that needs a one-time sign-in AT THIS BOX
|
|
1237
|
+
* reads `needs-auth` here long before a turn fails on it, and the app's
|
|
1238
|
+
* Machines page names it.
|
|
1239
|
+
*
|
|
1240
|
+
* `flowviant` IS EXCLUDED: it is the server THIS daemon mounts on every tab
|
|
1241
|
+
* turn, always present, and not something a person can act on. A status off
|
|
1242
|
+
* the closed list is relayed as `other` rather than dropped — the CLI said
|
|
1243
|
+
* something about the server, and silence would be the daemon deciding it did
|
|
1244
|
+
* not. Bounded (40 entries, names cut at 60) and sorted, the `recordSkills`
|
|
1245
|
+
* discipline: a stable param, so an unchanged report writes nothing.
|
|
1246
|
+
*
|
|
1247
|
+
* ONLY WHAT IS NOT CONNECTED LEAVES THE BOX (2026-09-23). The first cut
|
|
1248
|
+
* relayed every server, connected ones included — so the operator's whole set
|
|
1249
|
+
* of signed-in services (their mail, their brokerage, their calendar) rode the
|
|
1250
|
+
* poll to the server and into every teammate's read of the Machines page,
|
|
1251
|
+
* which renders only the servers that are NOT connected anyway. A connected
|
|
1252
|
+
* connector is good news and good news is not a line; it is also nobody
|
|
1253
|
+
* else's business. So `connected` is dropped HERE, before the cap, and what is
|
|
1254
|
+
* sent is exactly what the page can say something about.
|
|
1255
|
+
*
|
|
1256
|
+
* THREE STATES, the skills split: null = no turn has taught us (the param is
|
|
1257
|
+
* not sent), [] = nothing of the person's own needs anything (every server is
|
|
1258
|
+
* connected, or none is mounted), a list = the ones that do. Last turn wins,
|
|
1259
|
+
* so a connector somebody signs into leaves the list on the next turn's init,
|
|
1260
|
+
* and an all-connected turn sends `[]`, which clears a stale list server-side.
|
|
1261
|
+
*/
|
|
1262
|
+
let mcpServersCache = null;
|
|
1263
|
+
|
|
1264
|
+
export const MCP_STATUSES = new Set(['connected', 'failed', 'needs-auth', 'pending']);
|
|
1265
|
+
export const MAX_MCP_SERVERS = 40;
|
|
1266
|
+
export const MCP_NAME_MAX = 60;
|
|
1267
|
+
|
|
1268
|
+
export function recordMcpServers(list) {
|
|
1269
|
+
if (!Array.isArray(list)) return;
|
|
1270
|
+
const seen = new Map();
|
|
1271
|
+
for (const e of list) {
|
|
1272
|
+
if (!e || typeof e !== 'object' || typeof e.name !== 'string') continue;
|
|
1273
|
+
const n = e.name.trim().slice(0, MCP_NAME_MAX).trim();
|
|
1274
|
+
if (!n || n === 'flowviant' || seen.has(n)) continue;
|
|
1275
|
+
if (e.status === 'connected') continue; // see the header: never relayed
|
|
1276
|
+
seen.set(n, { n, s: MCP_STATUSES.has(e.status) ? e.status : 'other' });
|
|
1277
|
+
}
|
|
1278
|
+
mcpServersCache = [...seen.values()]
|
|
1279
|
+
.sort((a, b) => (a.n < b.n ? -1 : a.n > b.n ? 1 : 0))
|
|
1280
|
+
.slice(0, MAX_MCP_SERVERS);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/** What to send on the roster poll as `mcp` — null until a turn has taught us. */
|
|
1284
|
+
export function knownMcpServers() {
|
|
1285
|
+
return mcpServersCache;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1212
1288
|
/**
|
|
1213
1289
|
* LEARN WHAT `/` CAN OFFER, ON A MACHINE NO TURN HAS TAUGHT.
|
|
1214
1290
|
*
|
|
@@ -1338,6 +1414,8 @@ export function parseInitLine(line) {
|
|
|
1338
1414
|
// record nothing. Conflating the two would keep the probe scanning a whole
|
|
1339
1415
|
// turn's output on a CLI that does not report them.
|
|
1340
1416
|
skills: Array.isArray(ev.skills) ? ev.skills : null,
|
|
1417
|
+
// The CLI's mounted MCP servers (0.97.0) — `recordMcpServers` normalises.
|
|
1418
|
+
mcpServers: Array.isArray(ev.mcp_servers) ? ev.mcp_servers : null,
|
|
1341
1419
|
sessionId: typeof ev.session_id === 'string' ? ev.session_id : null,
|
|
1342
1420
|
};
|
|
1343
1421
|
}
|
|
@@ -1399,6 +1477,7 @@ export function probeSkillsOnce(cwd) {
|
|
|
1399
1477
|
const init = parseInitLine(line);
|
|
1400
1478
|
if (!init) continue;
|
|
1401
1479
|
if (init.skills) recordSkills(init.skills);
|
|
1480
|
+
if (init.mcpServers) recordMcpServers(init.mcpServers);
|
|
1402
1481
|
finish(init.sessionId);
|
|
1403
1482
|
return;
|
|
1404
1483
|
}
|
package/bin/lib/work.mjs
CHANGED
|
@@ -75,7 +75,9 @@ import {
|
|
|
75
75
|
WORK_TURN_KICKOFF_PLAIN,
|
|
76
76
|
SYSTEM_PLAN,
|
|
77
77
|
AGENT_PLAN_KICKOFF,
|
|
78
|
-
|
|
78
|
+
SYSTEM_AGENT_FOR,
|
|
79
|
+
agentTaskKindOf,
|
|
80
|
+
unknownAgentTaskKind,
|
|
79
81
|
SYSTEM_PRECHECK,
|
|
80
82
|
AGENT_TASK_KICKOFF,
|
|
81
83
|
AGENT_TASK_SPEC,
|
|
@@ -84,13 +86,14 @@ import {
|
|
|
84
86
|
withProjectContext,
|
|
85
87
|
} from './prompts.mjs';
|
|
86
88
|
import { knowledgeDirFor, FLOWVIANT_OWN_PATHS } from './knowledge.mjs';
|
|
87
|
-
import { createArtifactReporter, snapshotArtifacts } from './artifacts.mjs';
|
|
88
|
-
import { myPubB64, scrub as envScrub } from './env.mjs';
|
|
89
|
+
import { changedArtifacts, createArtifactReporter, scanArtifacts, snapshotArtifacts } from './artifacts.mjs';
|
|
90
|
+
import { myPubB64, scrub as envScrub, secretIn as envSecretIn } from './env.mjs';
|
|
89
91
|
import {
|
|
90
92
|
detectRuntimes,
|
|
91
93
|
canRun,
|
|
92
94
|
pickRuntimeFor,
|
|
93
95
|
recordSkills,
|
|
96
|
+
recordMcpServers,
|
|
94
97
|
toolEventOf,
|
|
95
98
|
removeProbeTranscript,
|
|
96
99
|
CLAUDE_TOOL_PROSE_KINDS,
|
|
@@ -171,6 +174,22 @@ const WORK_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
|
171
174
|
* the machine's own default — a turn that runs — rather than a flag no CLI
|
|
172
175
|
* understands and a tab that fails every message.
|
|
173
176
|
*/
|
|
177
|
+
/**
|
|
178
|
+
* THE ONE SENTENCE A PLAN TURN'S SYSTEM PROMPT GAINS (0.97.0), composed here
|
|
179
|
+
* rather than in prompts.mjs because it is a MODIFIER on whichever contract
|
|
180
|
+
* the tab runs, not a contract of its own.
|
|
181
|
+
*
|
|
182
|
+
* The enforcement is not this sentence — it is `--permission-mode plan`, under
|
|
183
|
+
* which the CLI itself refuses every write (claude.mjs, PLAN_MODE_PERM). What
|
|
184
|
+
* the sentence buys is the ANSWER: the plain contract above it says "edit
|
|
185
|
+
* freely, commit", and a turn told only that would spend itself discovering
|
|
186
|
+
* refusals. It also says not to reach for `ExitPlanMode` (measured: disabled
|
|
187
|
+
* under `-p`, and the probe turn then asked the person to leave plan mode
|
|
188
|
+
* themselves) — leaving plan mode is the person's switch in the tab.
|
|
189
|
+
*/
|
|
190
|
+
export const PLAN_TURN_SENTENCE =
|
|
191
|
+
'THIS IS A PLANNING TURN: the person switched this tab to plan mode, so read what you need, decide, and answer with the plan itself as your reply — change nothing (the CLI refuses every write this turn, whatever the mechanics above say about editing, committing or artifacts), and do not try to leave plan mode; they switch it off in the tab when they want the plan carried out.';
|
|
192
|
+
|
|
174
193
|
function brainFor(job) {
|
|
175
194
|
const out = {};
|
|
176
195
|
const model = typeof job?.model === 'string' ? job.model.trim() : '';
|
|
@@ -258,6 +277,9 @@ export function createWorkManager({
|
|
|
258
277
|
token: FLEET_TOKEN,
|
|
259
278
|
userAgent: USER_AGENT,
|
|
260
279
|
scrub: envScrub,
|
|
280
|
+
// …and for the binaries it cannot scrub, the same list as a byte check:
|
|
281
|
+
// a hit is withheld, never uploaded rewritten (artifacts.mjs).
|
|
282
|
+
secretIn: envSecretIn,
|
|
261
283
|
log: (line) => warn(line),
|
|
262
284
|
});
|
|
263
285
|
/**
|
|
@@ -3135,6 +3157,22 @@ export function createWorkManager({
|
|
|
3135
3157
|
});
|
|
3136
3158
|
return;
|
|
3137
3159
|
}
|
|
3160
|
+
/**
|
|
3161
|
+
* A PLAN TURN (0.97.0): the tab's switch, off the job. CLAUDE ONLY —
|
|
3162
|
+
* `--permission-mode plan` is Claude Code's, and a codex or agy turn
|
|
3163
|
+
* has no way to spell it, so it is refused HERE in words rather than
|
|
3164
|
+
* run as a build turn wearing the word. The server never sends the
|
|
3165
|
+
* key for a non-Claude tab; this is the machine's own half. Never on
|
|
3166
|
+
* a capture chat, which is read-only by its own profile already.
|
|
3167
|
+
*/
|
|
3168
|
+
const planTurn = job.planMode === true && job.capture !== true;
|
|
3169
|
+
if (planTurn && rt.id !== 'claude') {
|
|
3170
|
+
await settleWorkTurn(job.id, {
|
|
3171
|
+
ok: false,
|
|
3172
|
+
answer: `plan mode runs on Claude Code only, and this tab is ${rt.id} — turn plan off in the tab, then send the message again`,
|
|
3173
|
+
});
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3138
3176
|
// A PLAIN tab (agy) mounts no MCP: no credential to mint, no config
|
|
3139
3177
|
// to write. The trade is stated in SYSTEM_WORK_PLAIN — no cards, no
|
|
3140
3178
|
// streaming — and the honesty survives on the existing rails: the
|
|
@@ -3279,9 +3317,17 @@ export function createWorkManager({
|
|
|
3279
3317
|
const adoptNote = adopting
|
|
3280
3318
|
? '[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.]'
|
|
3281
3319
|
: '';
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3320
|
+
// A PLAN TURN RUNS PLAIN TOO (0.97.0): measured on 2.1.281, plan mode
|
|
3321
|
+
// refuses every `mcp__flowviant` call ("Cannot call … while in plan
|
|
3322
|
+
// mode") unless the tool is annotated read-only, and none of the
|
|
3323
|
+
// session tools are — so mounting the control plane would hand the
|
|
3324
|
+
// turn a list of tools it can only fail at. The credential is still
|
|
3325
|
+
// minted above: its lease answers (held elsewhere, gone) are about
|
|
3326
|
+
// the SESSION, and a plan turn needs them as much as any other.
|
|
3327
|
+
const mcp =
|
|
3328
|
+
plainTab || planTurn
|
|
3329
|
+
? { args: [], env: null, dir: null }
|
|
3330
|
+
: mcpFor(rt.id, mint.token, getMcpUrl());
|
|
3285
3331
|
// The tab's model/effort, if it named any. Spread into turnArgs so
|
|
3286
3332
|
// BOTH runTurn calls below carry it — the retry is the same turn on
|
|
3287
3333
|
// the same brain, not a quieter second opinion.
|
|
@@ -3428,7 +3474,7 @@ export function createWorkManager({
|
|
|
3428
3474
|
const turnArgs = {
|
|
3429
3475
|
// A plain tab has no tools to name and no session id to pass —
|
|
3430
3476
|
// its kickoff asks for one complete report instead of a stream.
|
|
3431
|
-
prompt: plainTab
|
|
3477
|
+
prompt: plainTab || planTurn
|
|
3432
3478
|
? WORK_TURN_KICKOFF_PLAIN({
|
|
3433
3479
|
sessionName: job.sessionName,
|
|
3434
3480
|
message,
|
|
@@ -3448,6 +3494,10 @@ export function createWorkManager({
|
|
|
3448
3494
|
askedByName: job.askedByName,
|
|
3449
3495
|
}),
|
|
3450
3496
|
planPerm: captureTab,
|
|
3497
|
+
// `--permission-mode plan` INSTEAD of the build posture — never
|
|
3498
|
+
// beside `--dangerously-skip-permissions`, which silently wins
|
|
3499
|
+
// (claude.mjs, PLAN_MODE_PERM). Present only on a plan turn.
|
|
3500
|
+
...(planTurn ? { planMode: true } : {}),
|
|
3451
3501
|
// The adopt turn resumes the TERMINAL conversation by forking it
|
|
3452
3502
|
// into this cwd (claude: --resume <id> --fork-session). After it
|
|
3453
3503
|
// speaks once, the fork lives natively here and turn 2+ is the
|
|
@@ -3460,12 +3510,18 @@ export function createWorkManager({
|
|
|
3460
3510
|
...(() => {
|
|
3461
3511
|
const knowledgeDir = knowledgeDirFor(repoRoot);
|
|
3462
3512
|
return {
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3513
|
+
// A PLAN TURN (0.97.0) runs the PLAIN contract — it has no
|
|
3514
|
+
// Flowviant tools, see `mcp` above — with no artifacts
|
|
3515
|
+
// paragraph (plan mode cannot write one) and the planning
|
|
3516
|
+
// sentence appended. Every other tab is untouched.
|
|
3517
|
+
system: planTurn
|
|
3518
|
+
? `${withProjectContext(SYSTEM_WORK_PLAIN, { knowledgeDir, artifacts: false })}\n\n${PLAN_TURN_SENTENCE}`
|
|
3519
|
+
: withProjectContext(
|
|
3520
|
+
plainTab ? SYSTEM_WORK_PLAIN : captureTab ? SYSTEM_CAPTURE : SYSTEM_WORK,
|
|
3521
|
+
// ARTIFACTS (0.94.0): every tab but the read-only capture
|
|
3522
|
+
// chat, and only while the server can show one.
|
|
3523
|
+
{ knowledgeDir, artifacts: !captureTab && getArtifactsAccepted() }
|
|
3524
|
+
),
|
|
3469
3525
|
// The directory is OUTSIDE a worktree's cwd; Claude Code is
|
|
3470
3526
|
// told it may read there (`--add-dir`) rather than left to
|
|
3471
3527
|
// refuse a read-only capture turn a path it was just handed.
|
|
@@ -3494,6 +3550,9 @@ export function createWorkManager({
|
|
|
3494
3550
|
// learned from a turn rather than looked up.
|
|
3495
3551
|
onInit: (i) => {
|
|
3496
3552
|
recordSkills(i.skills);
|
|
3553
|
+
// The CLI's mounted MCP servers and connectors (0.97.0) — the
|
|
3554
|
+
// same free fact off the same event; see recordMcpServers.
|
|
3555
|
+
recordMcpServers(i.mcpServers);
|
|
3497
3556
|
// The conversation this turn is actually speaking under. Held
|
|
3498
3557
|
// and persisted after the turn ends, so the NEXT one resumes
|
|
3499
3558
|
// this exact thread rather than whatever the directory saw
|
|
@@ -4868,11 +4927,62 @@ export function createWorkManager({
|
|
|
4868
4927
|
const branch = readGit(['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
4869
4928
|
|
|
4870
4929
|
const rt = job.runtime || 'claude';
|
|
4871
|
-
|
|
4930
|
+
/**
|
|
4931
|
+
* WHAT THIS CARD HANDS BACK, AND THE POSTURE THAT FOLLOWS (0.97.0).
|
|
4932
|
+
*
|
|
4933
|
+
* `code` (the default — absent on the job IS code) is the build turn,
|
|
4934
|
+
* byte-for-byte what every agent turn was. `design` and `research` run
|
|
4935
|
+
* their own contract (SYSTEM_AGENT_DESIGN / _RESEARCH) under their own
|
|
4936
|
+
* posture, which can write ONLY `.flowviant/artifacts/` (claude.mjs).
|
|
4937
|
+
*
|
|
4938
|
+
* A RUNTIME THAT CANNOT EXPRESS THE POSTURE IS REFUSED, NOT IMPROVISED.
|
|
4939
|
+
* Codex and antigravity declare neither (runtimes.mjs), and the only
|
|
4940
|
+
* alternative to refusing is running the card as a BUILD turn with
|
|
4941
|
+
* permissions skipped — an agent writing code for an ask that was a
|
|
4942
|
+
* mockup, reporting success. `nothing` puts the agent in Stuck saying so.
|
|
4943
|
+
*
|
|
4944
|
+
* THE JOB'S OWN KIND FIRST (2026-09-23). A `human` turn — a send-back
|
|
4945
|
+
* from the review deck, an answer — usually names NO card: the queue has
|
|
4946
|
+
* emptied, or the card it is about was just re-queued behind it. Read off
|
|
4947
|
+
* `job.task` alone, a send-back on a design agent ran as a BUILD turn
|
|
4948
|
+
* under the code contract with permissions skipped — the one turn in the
|
|
4949
|
+
* owner's loop ("iterate in the review column, the agent opened") that
|
|
4950
|
+
* could edit code on an ask that was a mockup. So the server projects the
|
|
4951
|
+
* kind of the card the turn is ABOUT onto the job itself (`taskKind`,
|
|
4952
|
+
* sent only for design and research; the same `DAEMON_TASK_KIND_MIN`
|
|
4953
|
+
* floor as the card's own key, no new one), and the card's key stays the
|
|
4954
|
+
* fallback for a server that sends only that. Absent on both is code.
|
|
4955
|
+
*/
|
|
4956
|
+
/**
|
|
4957
|
+
* A KIND THIS DAEMON DOES NOT KNOW IS REFUSED, NEVER BUILT (2026-09-23).
|
|
4958
|
+
* `agentTaskKindOf` reads anything unrecognised as code — right for a
|
|
4959
|
+
* printed label, wrong here, where code means the build posture with
|
|
4960
|
+
* permissions skipped. A server that learned a fourth kind ahead of
|
|
4961
|
+
* this daemon is exactly the case the version floor cannot cover (the
|
|
4962
|
+
* handout does not re-check it), and the answer is an update, said in
|
|
4963
|
+
* the word the server used. `nothing` puts the agent in Stuck with it.
|
|
4964
|
+
*/
|
|
4965
|
+
const strangeKind = unknownAgentTaskKind(job.taskKind ?? job.task?.taskKind);
|
|
4966
|
+
if (strangeKind !== null) {
|
|
4967
|
+
await postAgentTurn({
|
|
4968
|
+
turnId,
|
|
4969
|
+
outcome: 'nothing',
|
|
4970
|
+
answer: `this daemon does not know the card kind '${strangeKind}' — update it`,
|
|
4971
|
+
branch,
|
|
4972
|
+
worktree: wt,
|
|
4973
|
+
});
|
|
4974
|
+
return;
|
|
4975
|
+
}
|
|
4976
|
+
const taskKind = agentTaskKindOf(job.taskKind ?? job.task?.taskKind);
|
|
4977
|
+
const posture = taskKind === 'code' ? 'build' : taskKind;
|
|
4978
|
+
if (!canRun(RUNTIMES[rt], posture)) {
|
|
4872
4979
|
await postAgentTurn({
|
|
4873
4980
|
turnId,
|
|
4874
4981
|
outcome: 'nothing',
|
|
4875
|
-
answer:
|
|
4982
|
+
answer:
|
|
4983
|
+
posture === 'build'
|
|
4984
|
+
? `this machine cannot run ${rt}`
|
|
4985
|
+
: 'design and research cards run on Claude on this machine',
|
|
4876
4986
|
branch,
|
|
4877
4987
|
worktree: wt,
|
|
4878
4988
|
});
|
|
@@ -5003,13 +5113,16 @@ export function createWorkManager({
|
|
|
5003
5113
|
// The knowledge paragraph, when this box holds a library — the same
|
|
5004
5114
|
// composer the tabs use, so an agent and a tab can never be told two
|
|
5005
5115
|
// different things about the same directory.
|
|
5006
|
-
system: withProjectContext(
|
|
5116
|
+
system: withProjectContext(SYSTEM_AGENT_FOR(taskKind), {
|
|
5007
5117
|
knowledgeDir: knowledgeDirFor(repoRoot),
|
|
5008
5118
|
// ARTIFACTS (0.94.0), while the server can show one — an agent's
|
|
5009
5119
|
// land on its page, under the facts row.
|
|
5010
5120
|
artifacts: getArtifactsAccepted(),
|
|
5011
5121
|
}),
|
|
5012
5122
|
knowledgeDir: knowledgeDirFor(repoRoot),
|
|
5123
|
+
// Named only for the two non-code kinds; a code turn passes nothing
|
|
5124
|
+
// and keeps the build branch it always had.
|
|
5125
|
+
...(posture !== 'build' ? { posture } : {}),
|
|
5013
5126
|
cwd: wt,
|
|
5014
5127
|
runtime: rt,
|
|
5015
5128
|
resume,
|
|
@@ -5057,6 +5170,16 @@ export function createWorkManager({
|
|
|
5057
5170
|
onUsage: (u) => {
|
|
5058
5171
|
usage = u;
|
|
5059
5172
|
},
|
|
5173
|
+
// WHAT THE CLI SAYS IT CAN BE ASKED FOR, AND WHICH CONNECTORS NEED A
|
|
5174
|
+
// LOGIN — the same free fact off the same init event the tab lane
|
|
5175
|
+
// records (2026-09-23). Without it a box that only ever runs agents
|
|
5176
|
+
// taught the app its skills and connectors ONCE, from the startup
|
|
5177
|
+
// probe, and never again: a connector signed into at the box kept
|
|
5178
|
+
// reading "needs authorization" for the life of the process.
|
|
5179
|
+
onInit: (i) => {
|
|
5180
|
+
recordSkills(i.skills);
|
|
5181
|
+
recordMcpServers(i.mcpServers);
|
|
5182
|
+
},
|
|
5060
5183
|
onSpawn: (ch) => {
|
|
5061
5184
|
child = ch;
|
|
5062
5185
|
// The AGENT it serves — what the machine snapshot charges this
|
|
@@ -5161,6 +5284,60 @@ export function createWorkManager({
|
|
|
5161
5284
|
});
|
|
5162
5285
|
return;
|
|
5163
5286
|
}
|
|
5287
|
+
/**
|
|
5288
|
+
* A DESIGN OR RESEARCH CARD IS NOT DELIVERED UNTIL ITS ARTIFACT EXISTS
|
|
5289
|
+
* (0.97.0) — measured, never taken on the agent's word.
|
|
5290
|
+
*
|
|
5291
|
+
* Its whole product is one file under `.flowviant/artifacts/`: a `.html`
|
|
5292
|
+
* mockup for design, a `.md` write-up for research. A turn that says
|
|
5293
|
+
* "delivered" without having written one would land the agent in Review
|
|
5294
|
+
* with nothing to look at and a summary describing a page that does not
|
|
5295
|
+
* exist — so it settles `nothing` with the measured sentence, and the
|
|
5296
|
+
* agent lands in Stuck with a true reason instead.
|
|
5297
|
+
*
|
|
5298
|
+
* WHAT COUNTS: on a TASK turn, only what the scan found NEW OR CHANGED
|
|
5299
|
+
* this turn (`changedArtifacts` against the snapshot taken before the
|
|
5300
|
+
* spawn) — a second design card in the same agent must not pass on the
|
|
5301
|
+
* first card's page. On a HUMAN turn (an answer, a send-back) a matching
|
|
5302
|
+
* file already standing in the directory also counts: "keep it as it
|
|
5303
|
+
* is" is a legitimate answer to a question the agent asked after
|
|
5304
|
+
* drawing, and the turn did not have to rewrite the page to deliver it.
|
|
5305
|
+
*
|
|
5306
|
+
* …AND ON A REDO (2026-09-23): a TASK turn re-running a card this agent
|
|
5307
|
+
* already delivered, which is what a send-back's "Needs work" queues —
|
|
5308
|
+
* the human turn in front of it usually rewrote the mockup already, so
|
|
5309
|
+
* the task turn that follows truthfully says "revised last turn" with
|
|
5310
|
+
* nothing new to write, and the task rule sent it to Stuck with a false
|
|
5311
|
+
* sentence. The server marks such a job `redo: true` (a link delivered
|
|
5312
|
+
* before, or carrying a `needs_work` verdict), and then a standing match
|
|
5313
|
+
* counts, as it does for a human turn. No floor: an older server never
|
|
5314
|
+
* sends the key, and absent keeps the stricter task rule.
|
|
5315
|
+
*
|
|
5316
|
+
* Commits are still reported if any exist — the posture prevents them,
|
|
5317
|
+
* and a report never lies by omission about what is on the branch.
|
|
5318
|
+
*/
|
|
5319
|
+
if (res.outcome === 'delivered' && taskKind !== 'code') {
|
|
5320
|
+
const want = taskKind === 'design' ? /\.html?$/i : /\.md$/i;
|
|
5321
|
+
const standing = scanArtifacts(wt);
|
|
5322
|
+
const wrote = changedArtifacts(artifactsBefore, standing).some((e) => want.test(e.name));
|
|
5323
|
+
const present =
|
|
5324
|
+
wrote || ((job.kind !== 'task' || job.redo === true) && standing.some((e) => want.test(e.name)));
|
|
5325
|
+
if (!present) {
|
|
5326
|
+
await postAgentTurn({
|
|
5327
|
+
turnId,
|
|
5328
|
+
outcome: 'nothing',
|
|
5329
|
+
answer:
|
|
5330
|
+
taskKind === 'design'
|
|
5331
|
+
? 'the turn ended without writing a mockup under .flowviant/artifacts/'
|
|
5332
|
+
: 'the turn ended without writing a write-up under .flowviant/artifacts/',
|
|
5333
|
+
...(commits.length ? { commits } : {}),
|
|
5334
|
+
...(usage ? { usage } : {}),
|
|
5335
|
+
branch,
|
|
5336
|
+
worktree: wt,
|
|
5337
|
+
});
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5164
5341
|
const reply = await postAgentTurn({
|
|
5165
5342
|
turnId,
|
|
5166
5343
|
outcome: res.outcome,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.97.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": {
|