flowviant 0.113.0 → 0.114.1
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/cli.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +29 -3
- package/bin/lib/agentTaskTypes.mjs +10 -1
- package/bin/lib/artifactContracts.mjs +97 -2
- package/bin/lib/artifactPolicy.mjs +1 -1
- package/bin/lib/artifacts.mjs +34 -1
- package/bin/lib/claudePosture.mjs +7 -0
- package/bin/lib/claudeStream.mjs +14 -1
- package/bin/lib/credentials.mjs +9 -9
- package/bin/lib/fleetRoster.mjs +1 -1
- package/bin/lib/git.mjs +5 -5
- package/bin/lib/knowledgeLibrary.mjs +14 -8
- package/bin/lib/machineListing.mjs +4 -4
- package/bin/lib/noWindowEnv.mjs +104 -0
- package/bin/lib/projectToolRuntimes.mjs +78 -20
- package/bin/lib/projectTools.mjs +11 -3
- package/bin/lib/prompts.mjs +23 -12
- package/bin/lib/resumeLost.mjs +6 -0
- package/bin/lib/runTurn.mjs +78 -11
- package/bin/lib/runtimeCodex.mjs +91 -8
- package/bin/lib/runtimeLimits.mjs +22 -7
- package/bin/lib/runtimes.mjs +3 -0
- package/bin/lib/startProject.mjs +6 -6
- package/bin/lib/turnModel.mjs +120 -0
- package/bin/lib/turnProfile.mjs +24 -5
- package/bin/lib/work.mjs +2 -0
- package/bin/lib/workAgentCheck.mjs +10 -4
- package/bin/lib/workAgentPrecheck.mjs +4 -0
- package/bin/lib/workAgentTurnExecution.mjs +64 -2
- package/bin/lib/workAgentTurnOutcome.mjs +7 -4
- package/bin/lib/workAgentTurns.mjs +2 -1
- package/bin/lib/workSessionTurns.mjs +15 -0
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -325,7 +325,7 @@ async function uninstallCommand() {
|
|
|
325
325
|
// repo each is bound to, and which the legacy mirror points at. Needs no
|
|
326
326
|
// network: it reads the store, which is the exact thing a confused person is
|
|
327
327
|
// trying to see. The remedies are named because this listing IS the moment of
|
|
328
|
-
// confusion ("why did it say
|
|
328
|
+
// confusion ("why did it say contoso?"), not documentation.
|
|
329
329
|
async function projectsCommand() {
|
|
330
330
|
const { listStoredProjects, projectLabel } = await import('./lib/credentials.mjs');
|
|
331
331
|
const { connectedOn } = await import('./lib/credentialDate.mjs');
|
|
@@ -336,7 +336,7 @@ async function projectsCommand() {
|
|
|
336
336
|
}
|
|
337
337
|
for (const e of entries) {
|
|
338
338
|
// THE DATE RIDES THE ID, always — this listing exists for the moment
|
|
339
|
-
// somebody is asking "why did it say
|
|
339
|
+
// somebody is asking "why did it say contoso?", and "which of these two
|
|
340
340
|
// did I set up last month" is the same question one step on. Absent when
|
|
341
341
|
// nothing recorded one (a credential stored before the field existed);
|
|
342
342
|
// nothing is invented.
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* A CARD'S KIND — the daemon's one table of what each kind IS (0.97.0):
|
|
3
3
|
* `code` (the default), `design` and `research`; since 0.105.0 also `model`
|
|
4
4
|
* (the 3D-model kind — not an AI-model pin, which is `job.model`) and `deck`
|
|
5
|
-
* (the presentation kind — not the review deck)
|
|
5
|
+
* (the presentation kind — not the review deck); since 0.114.0 `image` (one or
|
|
6
|
+
* more generated PNG/WebP pictures — the first non-code kind that runs on
|
|
7
|
+
* Codex rather than Claude). Every reader of a kind goes
|
|
6
8
|
* through this module: the prompts that print it and pick a contract
|
|
7
9
|
* (prompts.mjs, which keeps the strings, with the 3D-model and presentation
|
|
8
10
|
* contracts and the shared tail in artifactContracts.mjs), the agent lane
|
|
@@ -31,8 +33,12 @@
|
|
|
31
33
|
* sentence a turn settles `nothing` with when the proof was not written;
|
|
32
34
|
* - `offRuntime(rt)`: the sentence when the chosen CLI cannot run the posture.
|
|
33
35
|
*/
|
|
34
|
-
/**
|
|
36
|
+
/** The mockup, 3D-model, presentation and write-up kinds run on Claude: the one
|
|
37
|
+
* CLI that declares their postures (`design`, `research`). */
|
|
35
38
|
const CLAUDE_ONLY = 'mockup, 3D model, presentation and write-up cards run on Claude on this machine';
|
|
39
|
+
/** The image kind runs on Codex: the one CLI that declares its posture, because
|
|
40
|
+
* the pictures come from Codex's own image tool on its own login. */
|
|
41
|
+
const CODEX_ONLY = 'image cards run on Codex on this machine';
|
|
36
42
|
|
|
37
43
|
export const AGENT_TASK_KINDS = Object.freeze({
|
|
38
44
|
code: Object.freeze({
|
|
@@ -65,6 +71,25 @@ export const AGENT_TASK_KINDS = Object.freeze({
|
|
|
65
71
|
}),
|
|
66
72
|
offRuntime: () => CLAUDE_ONLY,
|
|
67
73
|
}),
|
|
74
|
+
/**
|
|
75
|
+
* THE IMAGE KIND (0.114.0): one or more raster pictures, generated by
|
|
76
|
+
* Codex's own image tool (`image_generation`, the machine's Codex login —
|
|
77
|
+
* no API key) and copied under `.flowviant/artifacts/`. Its own posture,
|
|
78
|
+
* `image`, which ONLY Codex declares (runtimeCodex.mjs): read the repo,
|
|
79
|
+
* write only the artifacts directory, image generation on, no web. Claude
|
|
80
|
+
* Code draws no raster image, so a Claude container is refused before spawn
|
|
81
|
+
* in these words. Proven by one PNG or WebP — the formats the card is told
|
|
82
|
+
* to hand back; a picture drawn some other way (SVG, an HTML page) is not
|
|
83
|
+
* the delivery.
|
|
84
|
+
*/
|
|
85
|
+
image: Object.freeze({
|
|
86
|
+
posture: 'image',
|
|
87
|
+
artifact: Object.freeze({
|
|
88
|
+
match: /\.(png|webp)$/i,
|
|
89
|
+
missing: 'the turn ended without writing a PNG or WebP image under .flowviant/artifacts/',
|
|
90
|
+
}),
|
|
91
|
+
offRuntime: () => CODEX_ONLY,
|
|
92
|
+
}),
|
|
68
93
|
/** THE PRESENTATION KIND (0.105.0): one HTML slide deck, under the design fence. */
|
|
69
94
|
deck: Object.freeze({
|
|
70
95
|
posture: 'design',
|
|
@@ -119,7 +144,8 @@ export const unknownAgentTaskKind = (v) =>
|
|
|
119
144
|
export const agentTaskKind = (v) => AGENT_TASK_KINDS[agentTaskKindOf(v)];
|
|
120
145
|
|
|
121
146
|
/** The postures only a card's kind selects (every kind's but code's), each
|
|
122
|
-
* once: the 3D-model and presentation kinds reuse the design posture
|
|
147
|
+
* once: the 3D-model and presentation kinds reuse the design posture; the
|
|
148
|
+
* image kind has its own. */
|
|
123
149
|
export const KIND_POSTURES = Object.freeze([
|
|
124
150
|
...new Set(
|
|
125
151
|
Object.values(AGENT_TASK_KINDS)
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* the kind line (`AGENT_TASK_SPEC`), so the spec stashed for the pre-review
|
|
17
17
|
* carries it too. Each sentence is true to what its kind's posture can do: a
|
|
18
18
|
* code type may build and test; a write-up type reads and writes one Markdown
|
|
19
|
-
* file; a mockup type writes under .flowviant/artifacts/ and nothing else
|
|
19
|
+
* file; a mockup type writes under .flowviant/artifacts/ and nothing else; the
|
|
20
|
+
* Image type (0.114.0) generates pictures with Codex's own tool and copies
|
|
21
|
+
* them there.
|
|
20
22
|
*
|
|
21
23
|
* `label` is the app's `TASK_TYPE_LABEL`, the word a person picked. The ids
|
|
22
24
|
* (in the app's picker order), their kinds and their labels are held to the
|
|
@@ -101,6 +103,13 @@ export const AGENT_TASK_TYPES = Object.freeze({
|
|
|
101
103
|
'write each icon as its own .svg file directly in .flowviant/artifacts/, and make the mockup page ' +
|
|
102
104
|
'the sheet that shows them all, inline, at the sizes they are used.',
|
|
103
105
|
}),
|
|
106
|
+
image: Object.freeze({
|
|
107
|
+
label: 'Image',
|
|
108
|
+
kind: 'image',
|
|
109
|
+
contract:
|
|
110
|
+
'generate the picture the card describes with your image tool, at the size and in the style it ' +
|
|
111
|
+
'names, and hand back each image it asks for as its own PNG or WebP file.',
|
|
112
|
+
}),
|
|
104
113
|
research: Object.freeze({
|
|
105
114
|
label: 'Research',
|
|
106
115
|
kind: 'research',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* THE 3D-MODEL AND
|
|
2
|
+
* THE 3D-MODEL, PRESENTATION AND IMAGE CARDS' CONTRACTS (2026-09-27, 0.105.0;
|
|
3
|
+
* the image card 2026-09-29, 0.114.0).
|
|
3
4
|
*
|
|
4
5
|
* Two more kinds that hand back a file rather than a change, beside the design
|
|
5
6
|
* and research contracts in prompts.mjs (which is past its size and keeps
|
|
@@ -19,8 +20,11 @@
|
|
|
19
20
|
* "model" here is the 3D-model KIND (not an AI-model pin, `job.model`), and
|
|
20
21
|
* "deck" the presentation kind (not the review deck).
|
|
21
22
|
*
|
|
22
|
-
* Pure strings: no
|
|
23
|
+
* Pure strings: no environment, no I/O. The one import is the generated
|
|
24
|
+
* artifact policy's caps (pure data), so the image contract's size limit is
|
|
25
|
+
* the one the uploader enforces, never a second spelling of it.
|
|
23
26
|
*/
|
|
27
|
+
import { ARTIFACT_BINARY_MODEL_EXTS, ARTIFACT_BINARY_MODEL_MAX_BYTES, ARTIFACT_MAX_BYTES } from './artifactPolicy.mjs';
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
30
|
* Rule 3, rule 4 and the final JSON a file-handing contract ends with.
|
|
@@ -55,6 +59,25 @@ or, if you are stopping to ask:
|
|
|
55
59
|
"progress" is REQUIRED on both shapes and REPLACES the previous one whole —
|
|
56
60
|
past tense, plain sentences, no card ids.`;
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* NOBODY IS AT THIS SCREEN (2026-09-29, the owner: Chrome "kept opening on my
|
|
64
|
+
* desktop … but me as the user didnt click anything"). A board agent looking
|
|
65
|
+
* at the page it made started Chrome itself, and a Chrome started on the
|
|
66
|
+
* default profile hands its window to the person's own running Chrome. The
|
|
67
|
+
* turn's environment now closes those roads (noWindowEnv.mjs); these words
|
|
68
|
+
* are for what an environment cannot close — an agent reaching for a window
|
|
69
|
+
* on purpose — and say how to look without one: headless, a profile of its
|
|
70
|
+
* own, a screenshot it reads back. `flowviant shot` is not named because
|
|
71
|
+
* nothing puts `flowviant` on a turn's PATH (an npx or tray-bundled daemon
|
|
72
|
+
* has none there). Every file-handing kind that draws a page renders
|
|
73
|
+
* `LOOK_HEADLESS` in its rule 2; the image card renders `NO_WINDOW`.
|
|
74
|
+
*/
|
|
75
|
+
export const NO_WINDOW = `Never open a browser window or any desktop application: this is somebody's
|
|
76
|
+
own screen, and nobody is at it.`;
|
|
77
|
+
export const LOOK_HEADLESS = `${NO_WINDOW} To look at your page, screenshot it
|
|
78
|
+
headless with a profile of its own and read the PNG:
|
|
79
|
+
google-chrome --headless=new --user-data-dir="$(mktemp -d)" --virtual-time-budget=5000 --screenshot=/tmp/<name>.png file://<the page's absolute path>`;
|
|
80
|
+
|
|
58
81
|
/**
|
|
59
82
|
* THE 3D-MODEL CARD (rewritten 0.107.0, the owner, 2026-09-27: "yeah just
|
|
60
83
|
* glb"). The product is ONE .glb a person downloads and imports; the card
|
|
@@ -129,6 +152,8 @@ WHAT TO DO:
|
|
|
129
152
|
|
|
130
153
|
Keep the page under 2 MB.
|
|
131
154
|
|
|
155
|
+
${LOOK_HEADLESS}
|
|
156
|
+
|
|
132
157
|
${ARTIFACT_CONTRACT_TAIL({
|
|
133
158
|
ask: `If you cannot model it without a DECISION only a person can make — which of
|
|
134
159
|
two directions, what size or style, which asset it stands beside — STOP AND
|
|
@@ -167,9 +192,79 @@ WHAT TO DO:
|
|
|
167
192
|
one slide on each page (break-after: page), so the browser prints it to a
|
|
168
193
|
PDF. Keep it under 2 MB.
|
|
169
194
|
|
|
195
|
+
${LOOK_HEADLESS}
|
|
196
|
+
|
|
170
197
|
${ARTIFACT_CONTRACT_TAIL({
|
|
171
198
|
ask: `If you cannot write it without a DECISION only a person can make — who the
|
|
172
199
|
audience is, how long it runs, what it must argue — STOP AND ASK. Do not
|
|
173
200
|
guess. A question costs one reply; a guessed deck costs a review.`,
|
|
174
201
|
summary: 'one or two sentences on what the deck covers, and the file it is in',
|
|
175
202
|
})}`;
|
|
203
|
+
|
|
204
|
+
/** The upload cap a PNG or WebP artifact meets (the generated policy's), as
|
|
205
|
+
* the contract says it: "20 MB" since the app gave generated pictures the
|
|
206
|
+
* binary ceiling (2026-09-29). The larger of the two, should they differ. */
|
|
207
|
+
const imageCapBytes = Math.max(
|
|
208
|
+
...['png', 'webp'].map((ext) =>
|
|
209
|
+
ARTIFACT_BINARY_MODEL_EXTS.includes(ext) ? ARTIFACT_BINARY_MODEL_MAX_BYTES : ARTIFACT_MAX_BYTES
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
export const IMAGE_CAP_WORDS = `${Math.floor(imageCapBytes / (1024 * 1024))} MB`;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* THE IMAGE CARD (0.114.0, the owner 2026-09-29). The product is one or more
|
|
216
|
+
* RASTER pictures, and the one CLI on the machine that makes them is Codex,
|
|
217
|
+
* through its own image tool on its own login (`image_generation` — no API
|
|
218
|
+
* key; Flowviant generates nothing). So this is the first contract spoken to
|
|
219
|
+
* Codex rather than Claude, and it runs under Codex's `image` fence (read the
|
|
220
|
+
* repo, write only `.flowviant/artifacts/**`, no web — runtimeCodex.mjs).
|
|
221
|
+
*
|
|
222
|
+
* WHERE THE BYTES ARE. Codex's tool saves each image under
|
|
223
|
+
* `$CODEX_HOME/generated_images/…` and names the path (its own instructions:
|
|
224
|
+
* "copy it and leave the original in place"); the fence reads everywhere and
|
|
225
|
+
* writes only the artifacts directory, so the contract's one mechanical step
|
|
226
|
+
* is the COPY. The artifact scan finds what was copied; the delivery is proven
|
|
227
|
+
* by one .png or .webp there (agentTaskKinds.mjs).
|
|
228
|
+
*
|
|
229
|
+
* NEVER DRAWN IN CODE: an agent without the tool could paint an SVG or a
|
|
230
|
+
* canvas and call it the picture — a stand-in that reports success. The card
|
|
231
|
+
* asked for a generated picture, so a missing or refusing tool is a question
|
|
232
|
+
* to the person, quoting the tool's words, never a substitute.
|
|
233
|
+
*/
|
|
234
|
+
export const SYSTEM_AGENT_IMAGE = `You are the human's own Codex, working one IMAGE card in a git worktree of
|
|
235
|
+
their repository. Nobody is watching this run. You have the repo and your image
|
|
236
|
+
generation tool, and nothing else — no project tools, no board, no chat, no web.
|
|
237
|
+
|
|
238
|
+
AN IMAGE CARD HANDS BACK PICTURES, NOT A CHANGE. Nothing you do here edits the
|
|
239
|
+
product: the person looks at the images, downloads the ones they want, and
|
|
240
|
+
decides.
|
|
241
|
+
|
|
242
|
+
WHAT TO DO:
|
|
243
|
+
|
|
244
|
+
1. Read the card and the repository first — the brand, its colours and type,
|
|
245
|
+
and where the picture will be used: its size and shape (a hero banner, an
|
|
246
|
+
app icon, a social card) and what stands beside it. The picture must belong
|
|
247
|
+
to THIS product. A size, aspect ratio or style the card names wins;
|
|
248
|
+
otherwise say in your summary what you assumed.
|
|
249
|
+
|
|
250
|
+
2. Make each picture the card asks for WITH YOUR IMAGE GENERATION TOOL. Never
|
|
251
|
+
draw one in code — no SVG, no canvas, no HTML page, no script that paints
|
|
252
|
+
pixels: the card asks for a generated picture. The tool saves each image
|
|
253
|
+
under $CODEX_HOME/generated_images/ and tells you where. COPY each image you
|
|
254
|
+
hand back into .flowviant/artifacts/ under a short kebab-case name for what
|
|
255
|
+
it shows (for example .flowviant/artifacts/hero-banner.png), as a PNG or a
|
|
256
|
+
WebP file — one file per image, and nothing else in that directory: no page,
|
|
257
|
+
no notes, no scripts, no discarded variants. Keep each file under
|
|
258
|
+
${IMAGE_CAP_WORDS}; if one is larger, save it as WebP or at a smaller size.
|
|
259
|
+
|
|
260
|
+
${NO_WINDOW} Look at a picture through your own tools,
|
|
261
|
+
never an image viewer.
|
|
262
|
+
|
|
263
|
+
${ARTIFACT_CONTRACT_TAIL({
|
|
264
|
+
ask: `If you cannot make it without a DECISION only a person can make — what it
|
|
265
|
+
shows, its style, its size, where it goes — STOP AND ASK. Do not guess. And
|
|
266
|
+
if your image tool is not available here, or refuses or fails, STOP and say
|
|
267
|
+
so in your question, quoting its words — never draw a stand-in some other
|
|
268
|
+
way. A question costs one reply; a guessed picture costs a review.`,
|
|
269
|
+
summary: 'one or two sentences on what the images show, the files they are in, and the size and style you assumed',
|
|
270
|
+
})}`;
|
|
@@ -15,7 +15,7 @@ export const ARTIFACT_POLICY_SOURCE = 'packages/shared/src/schemas/artifact.sche
|
|
|
15
15
|
export const ARTIFACT_MAX_BYTES = 2097152;
|
|
16
16
|
/** Binary model buffers (the extensions below). */
|
|
17
17
|
export const ARTIFACT_BINARY_MODEL_MAX_BYTES = 20971520;
|
|
18
|
-
export const ARTIFACT_BINARY_MODEL_EXTS = ['glb', 'bin'];
|
|
18
|
+
export const ARTIFACT_BINARY_MODEL_EXTS = ['glb', 'bin', 'png', 'webp'];
|
|
19
19
|
/** Per session and per agent — the server keeps as many per owner. */
|
|
20
20
|
export const ARTIFACT_MAX_PER_OWNER = 40;
|
|
21
21
|
|
package/bin/lib/artifacts.mjs
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
* that cannot show an artifact must not have the CLI told it will.
|
|
75
75
|
*/
|
|
76
76
|
|
|
77
|
-
import { constants, closeSync, fstatSync, lstatSync, openSync, readdirSync, readSync } from 'node:fs';
|
|
77
|
+
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync } from 'node:fs';
|
|
78
78
|
import { createHash } from 'node:crypto';
|
|
79
79
|
import { join } from 'node:path';
|
|
80
80
|
import { cutKeepingExtension } from './safeFileName.mjs';
|
|
@@ -105,6 +105,39 @@ export const artifactMaxBytesFor = (name) => {
|
|
|
105
105
|
/** Listed per scan, newest first — the server keeps as many per owner. */
|
|
106
106
|
export const ARTIFACT_MAX_FILES = ARTIFACT_MAX_PER_OWNER;
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* THE ARTIFACTS DIRECTORY, STANDING BEFORE A FENCED KIND TURN SPAWNS (0.114.0).
|
|
110
|
+
*
|
|
111
|
+
* Codex's image profile makes `<place>/.flowviant/artifacts` its ONE writable
|
|
112
|
+
* root, and a root that does not exist cannot be made from inside the fence
|
|
113
|
+
* (measured on 0.156.1: `mkdir` under it failed "Read-only file system"). So
|
|
114
|
+
* the lane makes it first — and only as a REAL directory under a REAL
|
|
115
|
+
* `.flowviant`, the scan's own rule (`scanArtifacts`) on the write side: a
|
|
116
|
+
* repository can commit `.flowviant -> ~/.ssh`, and a fence granted on a path
|
|
117
|
+
* through that link is a write grant on its target. Either component being a
|
|
118
|
+
* symlink or a non-directory answers false and creates nothing; the caller
|
|
119
|
+
* refuses the turn in words.
|
|
120
|
+
*/
|
|
121
|
+
export function ensureArtifactDir(placeDir) {
|
|
122
|
+
const parent = join(placeDir, '.flowviant');
|
|
123
|
+
const dir = join(placeDir, ARTIFACT_DIR);
|
|
124
|
+
const real = (p) => {
|
|
125
|
+
try {
|
|
126
|
+
const st = lstatSync(p);
|
|
127
|
+
return st.isDirectory() && !st.isSymbolicLink() ? true : false;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return e?.code === 'ENOENT' ? null : false;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
if (real(parent) === false || real(dir) === false) return false;
|
|
133
|
+
try {
|
|
134
|
+
mkdirSync(dir, { recursive: true });
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return real(parent) === true && real(dir) === true;
|
|
139
|
+
}
|
|
140
|
+
|
|
108
141
|
export function artifactTypeFor(name) {
|
|
109
142
|
const dot = String(name).lastIndexOf('.');
|
|
110
143
|
if (dot <= 0) return null;
|
|
@@ -361,6 +361,13 @@ const CLAUDE_PERM = {
|
|
|
361
361
|
'plan-mode': () => PLAN_MODE_PERM,
|
|
362
362
|
design: (kd) => designPermFor(kd),
|
|
363
363
|
research: (kd) => researchPerm(kd),
|
|
364
|
+
// THE IMAGE POSTURE IS CODEX'S (0.114.0) and Claude does not declare it
|
|
365
|
+
// (runtimeClaude.mjs `profiles`), so turnProfile.mjs refuses a Claude turn
|
|
366
|
+
// under it before argv is built. The entry exists because `runTurn` looks
|
|
367
|
+
// Claude's list up BY NAME for every runtime (the adapter that is not Claude
|
|
368
|
+
// ignores it), and a missing name throws. Were it ever reached on Claude it
|
|
369
|
+
// is the consult list: read-only, no write at all — failing closed.
|
|
370
|
+
image: (kd) => consultPermFor(kd),
|
|
364
371
|
};
|
|
365
372
|
export const CLAUDE_PERM_PROFILES = Object.freeze(Object.keys(CLAUDE_PERM));
|
|
366
373
|
export function claudePermFor(profileName, knowledgeDir) {
|
package/bin/lib/claudeStream.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { humanizeClaudeTool, THINK_MARKER } from './runtimeEvents.mjs';
|
|
13
|
+
import { turnModelOf } from './turnModel.mjs';
|
|
13
14
|
|
|
14
15
|
// Turn one Claude tool_use into a compact activity {kind, label}, or null for
|
|
15
16
|
// tools not worth surfacing. `kind:'read'` is what the file counter counts; a
|
|
@@ -82,7 +83,7 @@ export function usageFromResult(ev) {
|
|
|
82
83
|
// container that reports no spend, which is indistinguishable from an older
|
|
83
84
|
// daemon by design. A callback that is silent when it breaks has to be called
|
|
84
85
|
// directly by something.
|
|
85
|
-
export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit, onUsage, onRateLimit }) {
|
|
86
|
+
export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit, onUsage, onRateLimit, onModel }) {
|
|
86
87
|
let ev;
|
|
87
88
|
try {
|
|
88
89
|
ev = JSON.parse(line);
|
|
@@ -97,6 +98,13 @@ export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, app
|
|
|
97
98
|
onActivity?.(a);
|
|
98
99
|
};
|
|
99
100
|
if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
|
|
101
|
+
// THE MODEL THAT ANSWERED (2026-09-29, turnModel.mjs) — the fallback for an
|
|
102
|
+
// init that named none. The turn's own messages only: a subagent's carry
|
|
103
|
+
// `parent_tool_use_id`, and its model is not the turn's.
|
|
104
|
+
if (ev.parent_tool_use_id == null) {
|
|
105
|
+
const answered = turnModelOf(ev.message.model);
|
|
106
|
+
if (answered) onModel?.(answered);
|
|
107
|
+
}
|
|
100
108
|
for (const b of ev.message.content) {
|
|
101
109
|
if (b.type === 'thinking' || b.type === 'redacted_thinking') {
|
|
102
110
|
/**
|
|
@@ -163,6 +171,11 @@ export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, app
|
|
|
163
171
|
// status (`needs-auth` above all — a sign-in that has to happen at this
|
|
164
172
|
// box). Relayed raw; `recordMcpServers` (runtimeCapabilities.mjs) normalises it and
|
|
165
173
|
// drops this daemon's own `flowviant` server.
|
|
174
|
+
//
|
|
175
|
+
// `model` (2026-09-29) is the model this turn RESOLVED — `opus` is an
|
|
176
|
+
// alias, and this is the CLI naming what it chose. See turnModel.mjs.
|
|
177
|
+
const resolved = turnModelOf(ev.model);
|
|
178
|
+
if (resolved) onModel?.(resolved);
|
|
166
179
|
if (Array.isArray(ev.skills) || typeof ev.session_id === 'string' || Array.isArray(ev.mcp_servers)) {
|
|
167
180
|
onInit?.({
|
|
168
181
|
skills: Array.isArray(ev.skills) ? ev.skills.map(String) : undefined,
|
package/bin/lib/credentials.mjs
CHANGED
|
@@ -153,7 +153,7 @@ export function safeName(name) {
|
|
|
153
153
|
* (2026-09-19).
|
|
154
154
|
*
|
|
155
155
|
* Two projects genuinely named the same thing is not a hypothetical: production
|
|
156
|
-
* held two
|
|
156
|
+
* held two projects with one name, both bound to the same checkout on one box, so
|
|
157
157
|
* `npx flowviant` there offered two IDENTICAL picker rows and the only way to
|
|
158
158
|
* choose between them was to guess. A picker whose options cannot be told apart
|
|
159
159
|
* is not a picker.
|
|
@@ -185,9 +185,9 @@ export function projectRowLabel(e, entries) {
|
|
|
185
185
|
* WHICH STORED PROJECTS SHARE ONE REPO — the duplicate the owner met and could
|
|
186
186
|
* not name (2026-09-23).
|
|
187
187
|
*
|
|
188
|
-
* `npx flowviant machines` printed
|
|
189
|
-
* daemons or two "machine profiles" on one box for one project: *"when I go
|
|
190
|
-
*
|
|
188
|
+
* `npx flowviant machines` printed one project name twice, and he read it as
|
|
189
|
+
* two daemons or two "machine profiles" on one box for one project: *"when I go
|
|
190
|
+
* to <the project> to disconnect the duplicate, I only see one."* It was two PROJECTS
|
|
191
191
|
* with one name, both bound to one checkout — and the id-on-every-row rule
|
|
192
192
|
* (2026-09-19) had not made that legible, because two hex prefixes say
|
|
193
193
|
* "different" without saying "same repo". A project's Machines page shows its
|
|
@@ -256,8 +256,8 @@ export function boundElsewhere(entries, repoRoot, projectId) {
|
|
|
256
256
|
*/
|
|
257
257
|
export function directoryTakenRefusal(bound, repoRoot, { incoming } = {}) {
|
|
258
258
|
// Every project in the sentence is labelled against the WHOLE set, the
|
|
259
|
-
// incoming one included: a refusal reading "already connected to
|
|
260
|
-
// so
|
|
259
|
+
// incoming one included: a refusal reading "already connected to Acme App,
|
|
260
|
+
// so Acme App was not connected" is the puzzle it exists to end.
|
|
261
261
|
const all = [...bound, ...(incoming ? [incoming] : [])];
|
|
262
262
|
const names = bound.map((e) => projectRowLabel(e, all));
|
|
263
263
|
const list = names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}` : names[0];
|
|
@@ -284,7 +284,7 @@ function normalizeName(s) {
|
|
|
284
284
|
* WHICH stored project most likely goes with this repo — a HINT for the
|
|
285
285
|
* picker's default, and DELIBERATELY not a licence to serve it. `resolveStored
|
|
286
286
|
* Credential` matches by repo PATH and refuses to guess past that, because
|
|
287
|
-
* serving a project the path did not name is the "it said
|
|
287
|
+
* serving a project the path did not name is the "it said contoso in my
|
|
288
288
|
* calendar repo" surprise this whole file exists to prevent. A name that
|
|
289
289
|
* matches the repo's folder or its github repo-name is softer evidence — good
|
|
290
290
|
* enough to put the cursor on that row so the answer is one keypress, never
|
|
@@ -450,7 +450,7 @@ export function matchStoredProject(ref) {
|
|
|
450
450
|
*
|
|
451
451
|
* Deliberately NO mirror fallback once more than one project is stored: the
|
|
452
452
|
* mirror is whichever login happened last, and serving it because it was
|
|
453
|
-
* recent is exactly the calendar-says-
|
|
453
|
+
* recent is exactly the calendar-says-contoso surprise this file exists to
|
|
454
454
|
* end. Two projects means the answer is a QUESTION (or --project, or the
|
|
455
455
|
* repo binding), never recency.
|
|
456
456
|
*/
|
|
@@ -473,7 +473,7 @@ export function resolveStoredCredential(argv = process.argv, cwd = process.cwd()
|
|
|
473
473
|
|
|
474
474
|
// ONE stored project with no binding: the pre-0.55.0 world. Serve it — a
|
|
475
475
|
// headless restart must not hang — but flag it, so a TTY start asks ONCE and
|
|
476
|
-
// binds, which is what turns "it said
|
|
476
|
+
// binds, which is what turns "it said contoso in my calendar repo" into a
|
|
477
477
|
// question instead of a surprise.
|
|
478
478
|
if (entries.length === 1 && !entries[0].repoRoot) {
|
|
479
479
|
return { entry: entries[0], source: 'only', needsConfirm: Boolean(repoRoot), repoRoot };
|
package/bin/lib/fleetRoster.mjs
CHANGED
|
@@ -467,7 +467,7 @@ export function announceFirstPoll(roster) {
|
|
|
467
467
|
);
|
|
468
468
|
note(c.dim(' wiki + agents stream to THIS project — view its Code canvas in Flowviant.'));
|
|
469
469
|
// Remember the NAME beside the stored credential, so the picker and
|
|
470
|
-
// `flowviant projects` can say "
|
|
470
|
+
// `flowviant projects` can say "contoso" instead of an id — a
|
|
471
471
|
// credential saved before the server sent names backfills here. No-op
|
|
472
472
|
// for a --fleet/env token (nothing stored to annotate).
|
|
473
473
|
setStoredProjectName(roster.project.id, roster.project.name);
|
package/bin/lib/git.mjs
CHANGED
|
@@ -260,11 +260,11 @@ export function detectBaseRef(repoRoot) {
|
|
|
260
260
|
* A base ref an agent's worktree can actually be cut from.
|
|
261
261
|
*
|
|
262
262
|
* `origin/<branch>` is the right base when the repo HAS an origin. A repo with
|
|
263
|
-
* no remote (a local-only project
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
263
|
+
* no remote (a local-only project, 2026-09-25) has no `origin/*` refs at
|
|
264
|
+
* all, and cutting a worktree from one fails; every agent turn then ended
|
|
265
|
+
* "nothing" in a second. So: the ref itself when it resolves to a commit,
|
|
266
|
+
* else the LOCAL branch of the same name, else the ref unchanged (and the
|
|
267
|
+
* worktree step says why in git's words).
|
|
268
268
|
*/
|
|
269
269
|
export function usableBaseRef(repoRoot, ref) {
|
|
270
270
|
const resolves = (r) => {
|
|
@@ -18,21 +18,24 @@
|
|
|
18
18
|
* including its original relative sidecar paths and measured preview. At
|
|
19
19
|
* 0.105.0 the end-product kinds add `models/<slug>-v<N>/` (a 3D model, a
|
|
20
20
|
* folder like a design: its viewer, its model files, its textures) and
|
|
21
|
-
* `decks/<slug>-v<N>.html` (a presentation, one file).
|
|
21
|
+
* `decks/<slug>-v<N>.html` (a presentation, one file). At 0.114.0 the image
|
|
22
|
+
* kind adds `images/<slug>-v<N>.png` or `.webp` — one BINARY file per kept
|
|
23
|
+
* picture, fetched and written as bytes like every entry.
|
|
22
24
|
*
|
|
23
|
-
* SUBDIRECTORIES, AND EXACTLY
|
|
25
|
+
* SUBDIRECTORIES, AND EXACTLY FIVE. The safe-name rule for a knowledge file
|
|
24
26
|
* forbids every separator; a library item is the one thing allowed ONE (or,
|
|
25
|
-
* for a bundle kind, a versioned folder), and only after one of the
|
|
27
|
+
* for a bundle kind, a versioned folder), and only after one of the five
|
|
26
28
|
* literal prefixes, with a stem that is already safe and the extension its
|
|
27
29
|
* kind hands back. `..`, another prefix, a leading dot, a folder for a
|
|
28
30
|
* single-file kind — refused, never rewritten, because a rewritten path could
|
|
29
|
-
* collide with another item's real one. The
|
|
31
|
+
* collide with another item's real one. The five directories are OURS the way
|
|
30
32
|
* the library directory is: a symlink or a file planted there is removed (the
|
|
31
33
|
* link, never its target) and a real directory made.
|
|
32
34
|
*
|
|
33
35
|
* ABSENT MEANS "LEAVE IT ALONE", the same rule the whole key keeps: a manifest
|
|
34
36
|
* with no `library` key is an older server, and `designs/`, `models/`,
|
|
35
|
-
* `decks/`, `research/` and `LIBRARY.md` are neither synced nor
|
|
37
|
+
* `images/`, `decks/`, `research/` and `LIBRARY.md` are neither synced nor
|
|
38
|
+
* swept.
|
|
36
39
|
* `library: { items: [] }` is the emptied catalog, SAID — every directory and
|
|
37
40
|
* the catalog go.
|
|
38
41
|
*/
|
|
@@ -91,15 +94,18 @@ const DESIGN_EXT = Object.freeze(['.html', '.htm', '.gltf', '.obj', '.glb', '.bi
|
|
|
91
94
|
* fact below — `LIBRARY_DIRS`, the path rule, the reference rule, the
|
|
92
95
|
* previews, the catalog — reads this, so a kind is one row here. It was five
|
|
93
96
|
* parallel tables keyed by kind, and a kind missed in one of them threw.
|
|
94
|
-
* `model` is the 3D-model kind, `deck` the presentation kind
|
|
97
|
+
* `model` is the 3D-model kind, `deck` the presentation kind, `image` the
|
|
98
|
+
* picture kind (0.114.0: one PNG or WebP, which is its own preview — no page
|
|
99
|
+
* for the server to measure).
|
|
95
100
|
*/
|
|
96
101
|
export const LIBRARY_KINDS = Object.freeze({
|
|
97
102
|
design: Object.freeze({ dir: 'designs', bundle: true, ext: DESIGN_EXT, preview: true, word: 'design' }),
|
|
98
103
|
model: Object.freeze({ dir: 'models', bundle: true, ext: Object.freeze([...DESIGN_EXT, '.mtl']), preview: true, word: '3D model' }),
|
|
104
|
+
image: Object.freeze({ dir: 'images', bundle: false, ext: Object.freeze(['.png', '.webp']), preview: false, word: 'image' }),
|
|
99
105
|
deck: Object.freeze({ dir: 'decks', bundle: false, ext: Object.freeze(['.html', '.htm']), preview: true, word: 'deck' }),
|
|
100
106
|
research: Object.freeze({ dir: 'research', bundle: false, ext: Object.freeze(['.md']), preview: false, word: 'research' }),
|
|
101
107
|
});
|
|
102
|
-
/** The
|
|
108
|
+
/** The five subdirectories a kept item may live in, by kind — read off
|
|
103
109
|
* `LIBRARY_KINDS`. */
|
|
104
110
|
export const LIBRARY_DIRS = Object.freeze(
|
|
105
111
|
Object.fromEntries(Object.entries(LIBRARY_KINDS).map(([kind, k]) => [kind, k.dir]))
|
|
@@ -279,7 +285,7 @@ export function renderLibraryCatalog(items) {
|
|
|
279
285
|
});
|
|
280
286
|
return (
|
|
281
287
|
'# Library\n\n' +
|
|
282
|
-
'The mockups, 3D models, decks and research this project kept. Paths are relative to this directory;\n' +
|
|
288
|
+
'The mockups, 3D models, images, decks and research this project kept. Paths are relative to this directory;\n' +
|
|
283
289
|
'a newer version supersedes an older one of the same name.\n\n' +
|
|
284
290
|
`${lines.join('\n')}\n`
|
|
285
291
|
);
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
* the connected flowviants? because im not sure if i have any duplicate or
|
|
37
37
|
* redundant daemons running". Measured the day it was asked: four live daemons
|
|
38
38
|
* across two boxes, two credentials last heard three days ago, and two projects
|
|
39
|
-
*
|
|
39
|
+
* with one name bound to the same checkout — which is why `npx
|
|
40
40
|
* flowviant` there offered two identical picker rows. THE PROJECT ID IS
|
|
41
41
|
* THEREFORE ALWAYS PRINTED: it is the only thing that tells those two apart,
|
|
42
42
|
* and the whole listing is worth less without it. **And since 0.95.0 the
|
|
43
43
|
* collision is SAID, not left to be noticed**: the id told him the rows were
|
|
44
44
|
* different projects, and he still read them as one project connected twice
|
|
45
|
-
* ("
|
|
46
|
-
* profiles' on my machine for the same
|
|
45
|
+
* ("<the project> appears twice meaning I likely have 2 daemon or 'machine
|
|
46
|
+
* profiles' on my machine for the same <the project> project"). `renderCollisions`
|
|
47
47
|
* prints one line per repo that two projects are bound to, naming both.
|
|
48
48
|
*
|
|
49
49
|
* ── IT IS A RELAY ──
|
|
@@ -266,7 +266,7 @@ export function projectPhrase(e) {
|
|
|
266
266
|
/**
|
|
267
267
|
* TWO PROJECTS ON ONE REPO, SAID OUT LOUD (2026-09-23).
|
|
268
268
|
*
|
|
269
|
-
* The owner ran this command, saw
|
|
269
|
+
* The owner ran this command, saw one project name twice, and concluded he had two
|
|
270
270
|
* daemons for one project. The id on each row said they were different
|
|
271
271
|
* projects; nothing said they were bound to the SAME checkout, which is the
|
|
272
272
|
* fact that makes it a duplicate rather than two rows. So the listing ends
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TURN NOBODY IS SITTING AT PUTS NO WINDOW ON ANYBODY'S SCREEN (2026-09-29).
|
|
3
|
+
*
|
|
4
|
+
* The owner, on WSL: Chrome "kept opening on my desktop … but me as the user
|
|
5
|
+
* didnt click anything". Board agents checking their three.js pages started
|
|
6
|
+
* Chrome themselves, and an inherited environment gives such a process three
|
|
7
|
+
* roads to the person's screen:
|
|
8
|
+
* · a Chrome started without --user-data-dir takes the DEFAULT profile, and
|
|
9
|
+
* while the person's own Chrome runs on it the new process hands its
|
|
10
|
+
* command line to that one through the profile's SingletonSocket — taken
|
|
11
|
+
* before any display is opened (measured) — so the PERSON'S Chrome opens
|
|
12
|
+
* the window, and no display variable of ours is involved;
|
|
13
|
+
* · a headed browser, Playwright's `headless: false`, any GUI program draws
|
|
14
|
+
* wherever DISPLAY / WAYLAND_DISPLAY point, and WSLg points them at the
|
|
15
|
+
* Windows desktop;
|
|
16
|
+
* · xdg-open, Python's webbrowser, a dev server's --open honour BROWSER, which
|
|
17
|
+
* on WSL is usually the Windows browser.
|
|
18
|
+
*
|
|
19
|
+
* So every turn but a Terminal tab's — the one lane where a person at the
|
|
20
|
+
* keyboard may ask for a window — runs with:
|
|
21
|
+
* · DISPLAY and WAYLAND_SOCKET removed, and WAYLAND_DISPLAY set EMPTY rather
|
|
22
|
+
* than removed: unset, libwayland falls back to `$XDG_RUNTIME_DIR/wayland-0`
|
|
23
|
+
* (measured: a client with it unset connected there), and that is exactly
|
|
24
|
+
* the socket WSLg keeps; empty names no socket, a Chrome that picks Wayland
|
|
25
|
+
* from it fails "Failed to connect to Wayland display" and exits (measured),
|
|
26
|
+
* and xdg-open reads empty as no display at all;
|
|
27
|
+
* · BROWSER=none, childEnv.mjs's word for the same thing;
|
|
28
|
+
* · a browser home of its own: CHROME_CONFIG_HOME, the root Chrome and
|
|
29
|
+
* Chromium put their default profile under (chrome_paths_linux.cc), and
|
|
30
|
+
* CHROME_USER_DATA_DIR, the profile itself (chrome_main_delegate.cc) — the
|
|
31
|
+
* one a fork like Brave still honours where it ignores the first
|
|
32
|
+
* (measured) — so a browser the agent starts bare opens a profile nobody
|
|
33
|
+
* else is running and can never reach the person's.
|
|
34
|
+
*
|
|
35
|
+
* Nothing that runs here needs what is removed: the CLIs' own headless modes
|
|
36
|
+
* (`claude -p`, `codex exec`) and headless browsers run without a display
|
|
37
|
+
* (measured). XDG_CONFIG_HOME is deliberately NOT moved: it is where Claude,
|
|
38
|
+
* Codex and git keep their own configuration.
|
|
39
|
+
*
|
|
40
|
+
* What an environment cannot stop is an agent that runs a Windows program on
|
|
41
|
+
* purpose through WSL interop (`explorer.exe`, `cmd.exe /c start`); the kind
|
|
42
|
+
* contracts ask it not to (artifactContracts.mjs, `NO_WINDOW`).
|
|
43
|
+
*/
|
|
44
|
+
import { lstatSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
45
|
+
import { tmpdir } from 'node:os';
|
|
46
|
+
import { join } from 'node:path';
|
|
47
|
+
|
|
48
|
+
/** Removed outright; WAYLAND_DISPLAY is kept but emptied (see above). */
|
|
49
|
+
export const WINDOW_ENV_REMOVED = Object.freeze(['DISPLAY', 'WAYLAND_SOCKET']);
|
|
50
|
+
|
|
51
|
+
/** A copy of `env` a turn nobody is sitting at runs with. Never mutates its
|
|
52
|
+
* input. `browserHome` is the directory its browsers keep their profile in. */
|
|
53
|
+
export function withoutWindows(env, browserHome = null) {
|
|
54
|
+
const out = { ...(env ?? {}) };
|
|
55
|
+
for (const k of WINDOW_ENV_REMOVED) delete out[k];
|
|
56
|
+
out.WAYLAND_DISPLAY = '';
|
|
57
|
+
out.BROWSER = 'none';
|
|
58
|
+
if (browserHome) {
|
|
59
|
+
out.CHROME_CONFIG_HOME = browserHome;
|
|
60
|
+
out.CHROME_USER_DATA_DIR = join(browserHome, 'user-data');
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* AN AGENT'S BROWSER HOME is kept beside its Codex home, in its worktree's
|
|
67
|
+
* private git dir: never committed, the same profile turn after turn, and
|
|
68
|
+
* removed with the worktree when the agent retires. The pre-review and the
|
|
69
|
+
* project check stand in the same worktree for the same agent, and take it
|
|
70
|
+
* too. Null when there is no git dir to hold it.
|
|
71
|
+
*/
|
|
72
|
+
export const agentBrowserHome = (sessionMetaPath, wt, agentId) =>
|
|
73
|
+
sessionMetaPath?.(wt, 'flowviant-agent-browser', agentId) ?? null;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The directory, made: `kept` when a lane has one (0700, never removed here),
|
|
77
|
+
* else one of this turn's own that `cleanup` removes. A kept path that is not
|
|
78
|
+
* a real directory is replaced, never followed; one that cannot be made falls
|
|
79
|
+
* back to a turn's own. Never throws — the turn runs without a browser home
|
|
80
|
+
* sooner than not at all, and its display variables are gone either way.
|
|
81
|
+
*/
|
|
82
|
+
export function openBrowserHome(kept) {
|
|
83
|
+
if (kept) {
|
|
84
|
+
try {
|
|
85
|
+
try { if (!lstatSync(kept).isDirectory()) rmSync(kept, { force: true }); } catch { /* absent */ }
|
|
86
|
+
mkdirSync(kept, { recursive: true, mode: 0o700 });
|
|
87
|
+
return { dir: kept, cleanup: () => {} };
|
|
88
|
+
} catch { /* fall through to a turn's own */ }
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const dir = mkdtempSync(join(tmpdir(), 'flowviant-browser-'));
|
|
92
|
+
let removed = false;
|
|
93
|
+
return {
|
|
94
|
+
dir,
|
|
95
|
+
cleanup: () => {
|
|
96
|
+
if (removed) return;
|
|
97
|
+
removed = true;
|
|
98
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
} catch {
|
|
102
|
+
return { dir: null, cleanup: () => {} };
|
|
103
|
+
}
|
|
104
|
+
}
|