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 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 skadooble?"), not documentation.
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 skadooble?", and "which of these two
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). Every reader of a kind goes
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
- /** Every non-code kind runs on Claude: the one CLI that declares their postures. */
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 PRESENTATION CARDS' CONTRACTS (2026-09-27, 0.105.0).
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 imports, no environment, no I/O.
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
 
@@ -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) {
@@ -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,
@@ -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 called "BRIF AI", both bound to the same checkout on one box, so
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 "BRIF AI" twice, and he read it as two
189
- * daemons or two "machine profiles" on one box for one project: *"when I go to
190
- * BRIF AI to disconnect the duplicate, I only see one."* It was two PROJECTS
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 BRIF AI,
260
- // so BRIF AI was not connected" is the puzzle it exists to end.
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 skadooble in my
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-skadooble surprise this file exists to
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 skadooble in my calendar repo" into a
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 };
@@ -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 "skadooble" instead of an id — a
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 — the owner's "Death Note - Roblox",
264
- * 2026-09-25) has no `origin/*` refs at all, and cutting a worktree from one
265
- * fails; every agent turn then ended "nothing" in a second. So: the ref itself
266
- * when it resolves to a commit, else the LOCAL branch of the same name, else
267
- * the ref unchanged (and the worktree step says why in git's words).
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 FOUR. The safe-name rule for a knowledge file
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 four
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 four directories are OURS the way
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 swept.
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 four subdirectories a kept item may live in, by kind — read off
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
- * both named "BRIF AI" bound to the same checkout — which is why `npx
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
- * ("BRIF AI appears twice meaning I likely have 2 daemon or 'machine
46
- * profiles' on my machine for the same BRIF AI project"). `renderCollisions`
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 "BRIF AI" twice, and concluded he had two
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
+ }