flowviant 0.114.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/artifactContracts.mjs +26 -0
- package/bin/lib/credentials.mjs +9 -9
- package/bin/lib/fleetRoster.mjs +1 -1
- package/bin/lib/git.mjs +5 -5
- 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 +3 -1
- package/bin/lib/resumeLost.mjs +6 -0
- package/bin/lib/runTurn.mjs +26 -6
- package/bin/lib/runtimeCodex.mjs +4 -3
- package/bin/lib/startProject.mjs +6 -6
- 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 +35 -2
- package/bin/lib/workSessionTurns.mjs +5 -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.
|
|
@@ -59,6 +59,25 @@ or, if you are stopping to ask:
|
|
|
59
59
|
"progress" is REQUIRED on both shapes and REPLACES the previous one whole —
|
|
60
60
|
past tense, plain sentences, no card ids.`;
|
|
61
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
|
+
|
|
62
81
|
/**
|
|
63
82
|
* THE 3D-MODEL CARD (rewritten 0.107.0, the owner, 2026-09-27: "yeah just
|
|
64
83
|
* glb"). The product is ONE .glb a person downloads and imports; the card
|
|
@@ -133,6 +152,8 @@ WHAT TO DO:
|
|
|
133
152
|
|
|
134
153
|
Keep the page under 2 MB.
|
|
135
154
|
|
|
155
|
+
${LOOK_HEADLESS}
|
|
156
|
+
|
|
136
157
|
${ARTIFACT_CONTRACT_TAIL({
|
|
137
158
|
ask: `If you cannot model it without a DECISION only a person can make — which of
|
|
138
159
|
two directions, what size or style, which asset it stands beside — STOP AND
|
|
@@ -171,6 +192,8 @@ WHAT TO DO:
|
|
|
171
192
|
one slide on each page (break-after: page), so the browser prints it to a
|
|
172
193
|
PDF. Keep it under 2 MB.
|
|
173
194
|
|
|
195
|
+
${LOOK_HEADLESS}
|
|
196
|
+
|
|
174
197
|
${ARTIFACT_CONTRACT_TAIL({
|
|
175
198
|
ask: `If you cannot write it without a DECISION only a person can make — who the
|
|
176
199
|
audience is, how long it runs, what it must argue — STOP AND ASK. Do not
|
|
@@ -234,6 +257,9 @@ WHAT TO DO:
|
|
|
234
257
|
no notes, no scripts, no discarded variants. Keep each file under
|
|
235
258
|
${IMAGE_CAP_WORDS}; if one is larger, save it as WebP or at a smaller size.
|
|
236
259
|
|
|
260
|
+
${NO_WINDOW} Look at a picture through your own tools,
|
|
261
|
+
never an image viewer.
|
|
262
|
+
|
|
237
263
|
${ARTIFACT_CONTRACT_TAIL({
|
|
238
264
|
ask: `If you cannot make it without a DECISION only a person can make — what it
|
|
239
265
|
shows, its style, its size, where it goes — STOP AND ASK. Do not guess. And
|
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) => {
|
|
@@ -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
|
+
}
|
|
@@ -15,24 +15,51 @@
|
|
|
15
15
|
* projectTools.mjs. projectTools.test.mjs pins every transport word to here.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { join, resolve } from 'node:path';
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
|
|
22
22
|
export const toml = (value) => JSON.stringify(String(value));
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
/**
|
|
25
|
+
* ONE LINK PER PERSONAL ENTRY, MADE OR MENDED IN PLACE (2026-09-29). A home
|
|
26
|
+
* that outlives its turn is refreshed rather than rebuilt, so it must end up
|
|
27
|
+
* exactly what a fresh build would link: a link already right stays, a link
|
|
28
|
+
* pointing anywhere else is re-pointed, and a real file or directory standing
|
|
29
|
+
* where a personal entry belongs — Codex rewrote a link by rename, or made its
|
|
30
|
+
* own before the person's existed — gives way to the link, because the
|
|
31
|
+
* personal copy is the one login and the one store (one login per CLI). A
|
|
32
|
+
* racing refresh that made the same link first is not an error.
|
|
33
|
+
*/
|
|
34
|
+
function linkPersonalEntry(target, link) {
|
|
35
|
+
let st = null;
|
|
36
|
+
try { st = lstatSync(link); } catch { /* absent: link it */ }
|
|
37
|
+
if (st?.isSymbolicLink() && readlinkSync(link) === target) return;
|
|
38
|
+
if (st) rmSync(link, { recursive: true, force: true }); // never follows a link
|
|
39
|
+
try {
|
|
40
|
+
symlinkSync(target, link);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error?.code !== 'EEXIST' || readlinkSync(link) !== target) throw error;
|
|
35
43
|
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Keep personal Codex files, but do not copy its project trust grants. An
|
|
48
|
+
* untrusted worktree's .codex/config.toml is ignored by Codex 0.156.1.
|
|
49
|
+
*
|
|
50
|
+
* `dest` IS REFRESHED IN PLACE, NEVER ASSUMED EMPTY (2026-09-29). An agent's
|
|
51
|
+
* home is kept between its turns (prepareAgentTools' `codexHome`), because
|
|
52
|
+
* Codex's shared thread index (`state_5.sqlite`, reached through the link)
|
|
53
|
+
* records each rollout under the CODEX_HOME in force, unresolved: a home that
|
|
54
|
+
* died with its turn left the agent's pinned thread pointing into a deleted
|
|
55
|
+
* directory, and the next card's `resume <id>` failed "no rollout found".
|
|
56
|
+
* So every personal entry is (re)linked, every link whose entry left the
|
|
57
|
+
* personal home is dropped, config.toml is rewritten whole each turn (the
|
|
58
|
+
* worktree's skills can change between turns), and what Codex made here
|
|
59
|
+
* itself under a name the person has not got is kept.
|
|
60
|
+
*/
|
|
61
|
+
function isolatedCodexHome(dest, env, worktree) {
|
|
62
|
+
const source = resolve(env.CODEX_HOME || join(homedir(), '.codex'));
|
|
36
63
|
const configPath = join(source, 'config.toml');
|
|
37
64
|
const sourceConfig = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
|
|
38
65
|
const topLevel = sourceConfig.split(/^\s*\[/m, 1)[0];
|
|
@@ -65,7 +92,33 @@ function isolatedCodexHome(dir, env, worktree) {
|
|
|
65
92
|
throw new Error('Cannot isolate Codex project skills with inline skills.config');
|
|
66
93
|
for (const skill of worktreeSkills) config += `\n[[skills.config]]\npath = ${toml(skill)}\nenabled = false\n`;
|
|
67
94
|
}
|
|
68
|
-
|
|
95
|
+
|
|
96
|
+
// The home itself, only once the config is decided: a refused turn touches
|
|
97
|
+
// nothing here. A home that is not a real directory is not ours to write
|
|
98
|
+
// through — a link standing here would carry what follows somewhere else.
|
|
99
|
+
try { if (!lstatSync(dest).isDirectory()) rmSync(dest, { force: true }); } catch { /* absent */ }
|
|
100
|
+
mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
101
|
+
chmodSync(dest, 0o700);
|
|
102
|
+
const personal = existsSync(source) ? readdirSync(source).filter((name) => name !== 'config.toml') : [];
|
|
103
|
+
for (const name of personal) linkPersonalEntry(join(source, name), join(dest, name));
|
|
104
|
+
const linked = new Set(personal);
|
|
105
|
+
for (const name of readdirSync(dest)) {
|
|
106
|
+
if (linked.has(name) || name === 'config.toml') continue;
|
|
107
|
+
// Only a LINK is ours to drop: a real entry under a name the person has
|
|
108
|
+
// not got is Codex's own (its sessions, on a box whose personal home had
|
|
109
|
+
// none yet), and this agent's threads live in it.
|
|
110
|
+
try { if (lstatSync(join(dest, name)).isSymbolicLink()) rmSync(join(dest, name), { force: true }); } catch { /* gone */ }
|
|
111
|
+
}
|
|
112
|
+
// Written beside and RENAMED over, so a config.toml that is somehow a link
|
|
113
|
+
// is replaced as an entry and never written through: the personal
|
|
114
|
+
// config.toml is the one file this home must never reach.
|
|
115
|
+
const staged = join(dest, `.config.toml.${process.pid}.${Date.now()}`);
|
|
116
|
+
try {
|
|
117
|
+
writeFileSync(staged, config, { mode: 0o600, flag: 'wx' });
|
|
118
|
+
renameSync(staged, join(dest, 'config.toml'));
|
|
119
|
+
} finally {
|
|
120
|
+
rmSync(staged, { force: true });
|
|
121
|
+
}
|
|
69
122
|
return { path: dest, personalDeveloperInstructions };
|
|
70
123
|
}
|
|
71
124
|
|
|
@@ -85,8 +138,9 @@ function reviewedClaudePlugin(dir, snapshot) {
|
|
|
85
138
|
|
|
86
139
|
/**
|
|
87
140
|
* The table. `refuseMcp(config)` names why this runtime cannot take one MCP
|
|
88
|
-
* configuration it otherwise could; `prepare(dir, snapshot, env, worktree
|
|
89
|
-
* returns the runner-specific part of prepareAgentTools' result
|
|
141
|
+
* configuration it otherwise could; `prepare(dir, snapshot, env, worktree,
|
|
142
|
+
* kept)` returns the runner-specific part of prepareAgentTools' result — under
|
|
143
|
+
* `dir`, which dies with the turn, except what `kept` names a lasting place for.
|
|
90
144
|
*/
|
|
91
145
|
const TOOL_RUNTIMES = {
|
|
92
146
|
claude: {
|
|
@@ -99,8 +153,11 @@ const TOOL_RUNTIMES = {
|
|
|
99
153
|
mcp: true,
|
|
100
154
|
skills: true,
|
|
101
155
|
refuseMcp: (config) => (config.type === 'sse' ? 'Codex cannot take SSE MCP' : null),
|
|
102
|
-
|
|
103
|
-
|
|
156
|
+
// The agent's own lasting home when the caller has one (2026-09-29: its
|
|
157
|
+
// threads are indexed under it — see isolatedCodexHome), else one that
|
|
158
|
+
// dies with the turn, as every home did before.
|
|
159
|
+
prepare: (dir, _snapshot, env, worktree, kept) => {
|
|
160
|
+
const isolated = isolatedCodexHome(kept?.codexHome ?? join(dir, 'codex-home'), env, worktree);
|
|
104
161
|
return { codexHome: isolated.path, personalDeveloperInstructions: isolated.personalDeveloperInstructions };
|
|
105
162
|
},
|
|
106
163
|
},
|
|
@@ -132,7 +189,8 @@ export function skillsRefusal(runner) {
|
|
|
132
189
|
return runner === 'none' ? 'no installed CLI can take instructions' : `${runner} cannot take instructions`;
|
|
133
190
|
}
|
|
134
191
|
|
|
135
|
-
/** The runner-specific files a turn needs, written under `dir
|
|
136
|
-
|
|
137
|
-
|
|
192
|
+
/** The runner-specific files a turn needs, written under `dir` — or, for what
|
|
193
|
+
* `kept` names, where the caller keeps it between turns. */
|
|
194
|
+
export function prepareForRunner(runner, dir, snapshot, env, worktree, kept = {}) {
|
|
195
|
+
return { pluginDir: null, codexHome: null, personalDeveloperInstructions: false, ...(rowFor(runner).prepare?.(dir, snapshot, env, worktree, kept) ?? {}) };
|
|
138
196
|
}
|
package/bin/lib/projectTools.mjs
CHANGED
|
@@ -99,8 +99,16 @@ export function readBaseTools(root, ref, env = process.env) {
|
|
|
99
99
|
|
|
100
100
|
const expand = (value, env) => String(value).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, key) => env[key] ?? '');
|
|
101
101
|
|
|
102
|
-
/**
|
|
103
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Temporary per-turn files are removed after the last retry. No personal CLI config is written.
|
|
104
|
+
*
|
|
105
|
+
* `codexHome` is the AGENT'S OWN CODEX_HOME, kept between its turns
|
|
106
|
+
* (2026-09-29): a place the caller owns, refreshed here each turn and never
|
|
107
|
+
* removed by `cleanup`, because Codex indexes a thread under the home it ran
|
|
108
|
+
* in and the next card resumes that thread. Absent, a Codex turn gets a home
|
|
109
|
+
* that dies with it.
|
|
110
|
+
*/
|
|
111
|
+
export function prepareAgentTools(snapshot, runner, env = process.env, worktree = null, { codexHome: keptCodexHome = null } = {}) {
|
|
104
112
|
const dir = mkdtempSync(join(tmpdir(), 'flowviant-agent-tools-'));
|
|
105
113
|
const usable = snapshot.tools.filter((t) => t.state === 'ready' && mcpRefusal(runner, t.config) == null);
|
|
106
114
|
const mcpServers = {};
|
|
@@ -142,7 +150,7 @@ export function prepareAgentTools(snapshot, runner, env = process.env, worktree
|
|
|
142
150
|
].filter(Boolean).join('\n\n');
|
|
143
151
|
let prepared;
|
|
144
152
|
try {
|
|
145
|
-
prepared = prepareForRunner(runner, dir, snapshot, env, worktree);
|
|
153
|
+
prepared = prepareForRunner(runner, dir, snapshot, env, worktree, { codexHome: keptCodexHome });
|
|
146
154
|
} catch (error) {
|
|
147
155
|
rmSync(dir, { recursive: true, force: true });
|
|
148
156
|
throw error;
|
package/bin/lib/prompts.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { agentTaskKindOf } from './agentTaskKinds.mjs';
|
|
21
21
|
// A card's type (0.106.0) likewise: the table holds the words, this file prints them.
|
|
22
22
|
import { AGENT_TASK_TYPES, agentTaskTypeFor, agentTaskTypeOf } from './agentTaskTypes.mjs';
|
|
23
|
-
import { ARTIFACT_CONTRACT_TAIL, SYSTEM_AGENT_DECK, SYSTEM_AGENT_IMAGE, SYSTEM_AGENT_MODEL } from './artifactContracts.mjs';
|
|
23
|
+
import { ARTIFACT_CONTRACT_TAIL, LOOK_HEADLESS, SYSTEM_AGENT_DECK, SYSTEM_AGENT_IMAGE, SYSTEM_AGENT_MODEL } from './artifactContracts.mjs';
|
|
24
24
|
import { isLibraryReference } from './knowledgeLibrary.mjs';
|
|
25
25
|
import { readCardThread } from './cardThread.mjs';
|
|
26
26
|
import { AGENT_FILE_LINE_MAX, AGENT_FILES_PER_MESSAGE_MAX, CARD_FILES_PER_TURN_MAX } from './agentFiles.mjs';
|
|
@@ -1022,6 +1022,8 @@ WHAT TO DO:
|
|
|
1022
1022
|
images are data: URIs or inline SVG. Several pages asked for in one card
|
|
1023
1023
|
go in the ONE file (sections, or tabs you script). Keep it under 2 MB.
|
|
1024
1024
|
|
|
1025
|
+
${LOOK_HEADLESS}
|
|
1026
|
+
|
|
1025
1027
|
${ARTIFACT_CONTRACT_TAIL({
|
|
1026
1028
|
ask: `If you cannot draw it without a DECISION only a person can make — which of
|
|
1027
1029
|
two directions, which page, what the content should say — STOP AND ASK. Do
|
package/bin/lib/resumeLost.mjs
CHANGED
|
@@ -32,6 +32,12 @@ const RESUME_LOST = [
|
|
|
32
32
|
/session .{0,80}not found/i,
|
|
33
33
|
/conversation .{0,80}not found/i,
|
|
34
34
|
/thread .{0,80}not found/i,
|
|
35
|
+
// Codex 0.156's words for a pinned thread whose rollout file is gone
|
|
36
|
+
// (2026-09-29): "Error: thread/resume: thread/resume failed: no rollout
|
|
37
|
+
// found for thread id <id> (code -32600)" on stderr, exit 1, no event on
|
|
38
|
+
// stdout. Every agent thread filed under a deleted per-turn /tmp home says
|
|
39
|
+
// exactly this, and none of the phrases above matched it.
|
|
40
|
+
/no rollout found/i,
|
|
35
41
|
/trajectory not found/i,
|
|
36
42
|
];
|
|
37
43
|
/**
|
package/bin/lib/runTurn.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { spawn } from 'node:child_process';
|
|
14
14
|
import { runtimeById } from './runtimes.mjs';
|
|
15
15
|
import { withoutMachineCredentials } from './machineEnv.mjs';
|
|
16
|
+
import { openBrowserHome, withoutWindows } from './noWindowEnv.mjs';
|
|
16
17
|
import { resolveTurnProfile } from './turnProfile.mjs';
|
|
17
18
|
import { claudePermFor } from './claudePosture.mjs';
|
|
18
19
|
import { handleStreamLine } from './claudeStream.mjs';
|
|
@@ -38,9 +39,18 @@ const CODEX_MODEL_READ_MS = 1_500;
|
|
|
38
39
|
* the MCP config file. So those two names, and only those, are removed —
|
|
39
40
|
* by the one rule in machineEnv.mjs, which the project check obeys too.
|
|
40
41
|
*/
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* …AND, UNLESS A PERSON IS AT THE KEYBOARD, NO WAY TO A WINDOW (2026-09-29).
|
|
44
|
+
* `display: true` is the Terminal tab's alone: a person typing there may ask
|
|
45
|
+
* for a window and gets their own environment. Every other turn — an agent's,
|
|
46
|
+
* its pre-review, the planner, the wiki, intake, the capture chat, and any
|
|
47
|
+
* lane added later that does not say otherwise — runs under
|
|
48
|
+
* `withoutWindows` (noWindowEnv.mjs), its browsers in `browserHome`.
|
|
49
|
+
*/
|
|
50
|
+
export function cliEnv(mcpEnv, { display = false, browserHome = null } = {}) {
|
|
42
51
|
// After the merge, so an mcpEnv can never smuggle one back in.
|
|
43
|
-
|
|
52
|
+
const env = withoutMachineCredentials({ ...process.env, ...(mcpEnv ?? {}) });
|
|
53
|
+
return display === true ? env : withoutWindows(env, browserHome);
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
// One Claude Code turn. Output is captured (for sentinel detection) and streamed
|
|
@@ -53,7 +63,7 @@ export function cliEnv(mcpEnv) {
|
|
|
53
63
|
// `onActivity` so the caller can forward progress. Build-agent turns leave it
|
|
54
64
|
// off and keep the raw text passthrough + line sentinels.
|
|
55
65
|
export function runTurn(opts) {
|
|
56
|
-
const { prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, agentTools, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onToolEvent, onInit, onUsage, onRateLimit, onModel, onThreadId, onAnswer, vaultDir, knowledgeDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId } = opts;
|
|
66
|
+
const { prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, agentTools, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onToolEvent, onInit, onUsage, onRateLimit, onModel, onThreadId, onAnswer, vaultDir, knowledgeDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId, display, browserHome } = opts;
|
|
57
67
|
/**
|
|
58
68
|
* AFTER THE TEARDOWN, NO TURN STARTS AND NO TURN ENDS (standDownGate.mjs).
|
|
59
69
|
* A stand-down that drains a deploy keeps this process alive; without this
|
|
@@ -195,6 +205,11 @@ export function runTurn(opts) {
|
|
|
195
205
|
// Which credential is correct, and whether an account may be shared, is
|
|
196
206
|
// between the operator and the vendor. Flowviant does not detect it and does
|
|
197
207
|
// not enforce it; it runs the CLI the ordinary way and relays what happens.
|
|
208
|
+
//
|
|
209
|
+
// A turn nobody is sitting at keeps its browsers in a home of its own —
|
|
210
|
+
// the lane's kept one (an agent's), else this spawn's, removed when the
|
|
211
|
+
// CLI closes. See `cliEnv`.
|
|
212
|
+
const browser = display === true ? null : openBrowserHome(browserHome);
|
|
198
213
|
const child = spawn(rt.bin, args, {
|
|
199
214
|
cwd,
|
|
200
215
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -219,10 +234,15 @@ export function runTurn(opts) {
|
|
|
219
234
|
detached: true,
|
|
220
235
|
// Only ADDS to the environment (the worker token, for runtimes that read
|
|
221
236
|
// it from there). Never replaces it: the CLI's own credentials live in
|
|
222
|
-
// this environment, and handing it a curated one signs it out.
|
|
223
|
-
//
|
|
224
|
-
|
|
237
|
+
// this environment, and handing it a curated one signs it out. What it
|
|
238
|
+
// REMOVES is this daemon's machine credential and, for a turn nobody is
|
|
239
|
+
// sitting at, every way to a window — see `cliEnv`.
|
|
240
|
+
env: cliEnv(mcpEnv, { display: display === true, browserHome: browser?.dir }),
|
|
225
241
|
});
|
|
242
|
+
if (browser) {
|
|
243
|
+
child.once('close', browser.cleanup);
|
|
244
|
+
child.once('error', browser.cleanup);
|
|
245
|
+
}
|
|
226
246
|
onSpawn?.(child);
|
|
227
247
|
// When this turn began, for the Codex rollout read: a `turn_context` older
|
|
228
248
|
// than this is a previous turn's (turnModel.mjs).
|
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -243,9 +243,10 @@ export const CODEX_RUNTIME = {
|
|
|
243
243
|
// one under `$CODEX_HOME/generated_images/…` and says the path (the
|
|
244
244
|
// binary's own words: "Generated images are saved to … by default. If
|
|
245
245
|
// you need to use a generated image at another path, copy it"). An agent
|
|
246
|
-
// turn's CODEX_HOME is
|
|
247
|
-
// `:root` reads admit it, so the agent
|
|
248
|
-
// writable directory, where the
|
|
246
|
+
// turn's CODEX_HOME is the agent's own isolated home (projectToolRuntimes,
|
|
247
|
+
// kept per agent since 2026-09-29); `:root` reads admit it, so the agent
|
|
248
|
+
// copies what it keeps into the one writable directory, where the
|
|
249
|
+
// artifact scan finds it.
|
|
249
250
|
//
|
|
250
251
|
// IMAGE GENERATION IS SWITCHED ON HERE AND ONLY HERE — a decision. This
|
|
251
252
|
// is the one turn whose card asks for pictures, so it must not depend
|
package/bin/lib/startProject.mjs
CHANGED
|
@@ -33,7 +33,7 @@ async function reexecAfterLogin() {
|
|
|
33
33
|
|
|
34
34
|
// TWO ROWS THAT READ ALIKE ARE NOT A CHOICE. `projectRowLabel` adds the id and
|
|
35
35
|
// the connected date to the COLLIDING rows only — production held two projects
|
|
36
|
-
//
|
|
36
|
+
// with one name, both bound to the same checkout, so this picker
|
|
37
37
|
// offered two identical lines and the only way to answer was to guess. See
|
|
38
38
|
// credentials.mjs for why the suffix is not on every row. (That exact case is
|
|
39
39
|
// a refusal now rather than a picker — a directory serves one project — but
|
|
@@ -62,7 +62,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
62
62
|
// binds it, and a headless start with no unambiguous answer refuses in words
|
|
63
63
|
// that name every stored project and every way out. The one thing this block
|
|
64
64
|
// must never do is serve a project the resolution did not name — "it said
|
|
65
|
-
//
|
|
65
|
+
// contoso in my calendar repo" is the confusion this exists to end.
|
|
66
66
|
// A RESTART IS NOT A PERSON. `reexec` (update.mjs) inherits stdio, so an
|
|
67
67
|
// auto-updated daemon's child sees two TTYs; without this it would stop on the
|
|
68
68
|
// binding confirm below and the machine would stay dark until somebody typed a
|
|
@@ -122,7 +122,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
122
122
|
|
|
123
123
|
const loginLabel = `connect ${repoRoot ? 'this repo' : 'a repo'} to a different project (${terminalCommand('login')})`;
|
|
124
124
|
// WHICH ONE LOOKS RIGHT — a pre-selection, never an auto-serve. The resolver
|
|
125
|
-
// refuses to serve a project the repo PATH did not name (the
|
|
125
|
+
// refuses to serve a project the repo PATH did not name (the contoso law);
|
|
126
126
|
// this only decides which row the cursor starts on, using the repo's folder
|
|
127
127
|
// name and its github repo-name against the stored project names. A unique
|
|
128
128
|
// match becomes ONE keypress; a wrong guess costs nothing, because the human
|
|
@@ -136,7 +136,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
136
136
|
|
|
137
137
|
// Bounded like the confirm below, and for the same reason — but silence
|
|
138
138
|
// means something DIFFERENT here and the difference is load-bearing. There
|
|
139
|
-
// is a real ambiguity to resolve; serving a guess is the
|
|
139
|
+
// is a real ambiguity to resolve; serving a guess is the contoso bug.
|
|
140
140
|
// So no answer REFUSES, which is exactly what this branch already does
|
|
141
141
|
// headless, and the message says how to answer without being present.
|
|
142
142
|
let chosen = null; // 0-based into [...choices, login]
|
|
@@ -220,7 +220,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
220
220
|
} else if (!externalToken && CREDENTIAL.needsConfirm && interactive) {
|
|
221
221
|
// ONE stored project, never tied to a repo — the pre-0.55.0 world. Ask once;
|
|
222
222
|
// yes binds and every later start is silent. This is the exact question
|
|
223
|
-
// whose absence had a calendar checkout serving
|
|
223
|
+
// whose absence had a calendar checkout serving contoso.
|
|
224
224
|
//
|
|
225
225
|
// AND IT TIMES OUT, because a prompt on a start path is a way for a machine
|
|
226
226
|
// to go dark. `FLOWVIANT_REEXEC` above covers the restart THIS version
|
|
@@ -240,7 +240,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
240
240
|
// not, which is the whole reason this product has exactly one refusal.
|
|
241
241
|
// · DO NOT BIND, because binding is the thing the question was FOR. Nobody
|
|
242
242
|
// answered, so nothing is cemented; the next human start asks again. That
|
|
243
|
-
// keeps the
|
|
243
|
+
// keeps the contoso case fixed for the person who is actually looking,
|
|
244
244
|
// which is the only person it could ever have been fixed for.
|
|
245
245
|
const creds = await import('./credentials.mjs');
|
|
246
246
|
const label = creds.projectLabel(CREDENTIAL.entry);
|
package/bin/lib/work.mjs
CHANGED
|
@@ -330,6 +330,8 @@ export function createWorkManager({
|
|
|
330
330
|
postBestEffort,
|
|
331
331
|
workChildren,
|
|
332
332
|
groupKillChildren,
|
|
333
|
+
// Only to find the agent's browser profile (noWindowEnv.mjs).
|
|
334
|
+
sessionMetaPath,
|
|
333
335
|
});
|
|
334
336
|
const { runPrecheck } = createWorkAgentPrecheck({
|
|
335
337
|
baseRef,
|
|
@@ -17,6 +17,7 @@ import { warn } from './ui.mjs';
|
|
|
17
17
|
import { ARTIFACT_DIR, artifactTypeFor } from './artifacts.mjs';
|
|
18
18
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
19
19
|
import { withoutMachineCredentials } from './machineEnv.mjs';
|
|
20
|
+
import { agentBrowserHome, openBrowserHome, withoutWindows } from './noWindowEnv.mjs';
|
|
20
21
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
21
22
|
import { workLanesStopped } from './standDownGate.mjs';
|
|
22
23
|
|
|
@@ -85,11 +86,14 @@ export function clearNonArtifacts(wt) {
|
|
|
85
86
|
* authenticates with; everything else is what the agent's own turn saw when it
|
|
86
87
|
* ran the same tests.
|
|
87
88
|
*/
|
|
88
|
-
export function checkEnv(env = process.env) {
|
|
89
|
-
|
|
89
|
+
export function checkEnv(env = process.env, browserHome = null) {
|
|
90
|
+
// …and, like that turn since 2026-09-29, no way to a window: a test run
|
|
91
|
+
// nobody is watching that opens a headed browser or an HTML report opens it
|
|
92
|
+
// on nobody's behalf (noWindowEnv.mjs). The agent's own browser profile.
|
|
93
|
+
return withoutWindows(withoutMachineCredentials(env), browserHome);
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, groupKillChildren }) {
|
|
96
|
+
export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, groupKillChildren, sessionMetaPath }) {
|
|
93
97
|
const AGENT_CHECK_DONE_URL = fleetEndpoint('agent-check-done', FLEET_URL);
|
|
94
98
|
|
|
95
99
|
// ── THE PROJECT'S OWN CHECK, and the MERGE ─────────────────────────────────
|
|
@@ -142,6 +146,7 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
142
146
|
warn(
|
|
143
147
|
`agent ${agentId}: removed ${cleared.length} non-artifact file(s) from ${ARTIFACT_DIR} before the check — ${cleared.slice(0, 5).join(', ')}`
|
|
144
148
|
);
|
|
149
|
+
const browser = openBrowserHome(agentBrowserHome(sessionMetaPath, wt, agentId));
|
|
145
150
|
const out = await new Promise((resolve) => {
|
|
146
151
|
let text = '';
|
|
147
152
|
let done = false;
|
|
@@ -178,7 +183,7 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
178
183
|
shell: true,
|
|
179
184
|
detached: true,
|
|
180
185
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
181
|
-
env: checkEnv(),
|
|
186
|
+
env: checkEnv(process.env, browser.dir),
|
|
182
187
|
});
|
|
183
188
|
/**
|
|
184
189
|
* …AND IT IS TRACKED, so a stop or a takeover takes it with them.
|
|
@@ -266,6 +271,7 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
266
271
|
finish(code === 0 ? 'passed' : 'failed');
|
|
267
272
|
});
|
|
268
273
|
});
|
|
274
|
+
browser.cleanup();
|
|
269
275
|
await postCheck({
|
|
270
276
|
agentId,
|
|
271
277
|
status: out.status,
|
|
@@ -20,6 +20,7 @@ import { removeProbeTranscript } from './runtimeCapabilities.mjs';
|
|
|
20
20
|
import { taskIdsFromMessage } from './worktreeDiff.mjs';
|
|
21
21
|
import { limitLine } from './cliLimit.mjs';
|
|
22
22
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
23
|
+
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
23
24
|
|
|
24
25
|
export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workChildren }) {
|
|
25
26
|
const AGENT_PRECHECK_URL = fleetEndpoint('agent-precheck', FLEET_URL);
|
|
@@ -257,6 +258,9 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
257
258
|
profile: 'consult',
|
|
258
259
|
cwd: wt,
|
|
259
260
|
runtime: rt,
|
|
261
|
+
// The agent's own browser profile, in the worktree this reader stands
|
|
262
|
+
// in; no display either way (noWindowEnv.mjs, 2026-09-29).
|
|
263
|
+
browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
|
|
260
264
|
// NO `resume`, AND THAT IS THE FEATURE. A resumed turn would be the
|
|
261
265
|
// agent grading its own homework out of its own context; this is a
|
|
262
266
|
// stranger reading a diff.
|
|
@@ -38,6 +38,7 @@ import { brainFor } from './workBrain.mjs';
|
|
|
38
38
|
import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
|
|
39
39
|
import { agentTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
40
40
|
import { ensureArtifactDir } from './artifacts.mjs';
|
|
41
|
+
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
41
42
|
|
|
42
43
|
export function createAgentTurnExecution({
|
|
43
44
|
baseRef,
|
|
@@ -286,6 +287,23 @@ export function createAgentTurnExecution({
|
|
|
286
287
|
rt === 'codex'
|
|
287
288
|
? Boolean(codexResumeId)
|
|
288
289
|
: rt === 'claude' && Boolean(ranMarker && existsSync(ranMarker));
|
|
290
|
+
/**
|
|
291
|
+
* …AND THE HOME THAT THREAD IS FILED UNDER, KEPT PER AGENT (2026-09-29).
|
|
292
|
+
*
|
|
293
|
+
* Codex's thread index records each rollout under the CODEX_HOME the turn
|
|
294
|
+
* ran with, as that path — not the personal store the link leads to — so
|
|
295
|
+
* the per-turn home in /tmp every turn used to get died with its turn and
|
|
296
|
+
* took the pinned thread with it: the next card's `resume` failed "no
|
|
297
|
+
* rollout found" and the agent went to Stuck. Beside the thread marker,
|
|
298
|
+
* in the worktree's private git dir: outside the working tree (never
|
|
299
|
+
* committed, never seen by `git add -A`), refreshed by every turn and
|
|
300
|
+
* removed only with the worktree, when the agent retires and its threads
|
|
301
|
+
* stop mattering. One agent runs one turn at a time (its place's writer
|
|
302
|
+
* lock), so no two turns refresh it at once. Null (no git dir to hold it)
|
|
303
|
+
* is the per-turn home again, and a lost resume then runs once fresh.
|
|
304
|
+
*/
|
|
305
|
+
const codexAgentHome =
|
|
306
|
+
rt === 'codex' ? sessionMetaPath(wt, 'flowviant-agent-codex-home', agentId) : null;
|
|
289
307
|
|
|
290
308
|
/**
|
|
291
309
|
* THE TURN'S FILES, ON DISK BEFORE THE PROMPT NAMES THEM (0.112.0) — the
|
|
@@ -376,6 +394,15 @@ export function createAgentTurnExecution({
|
|
|
376
394
|
|
|
377
395
|
let out = '';
|
|
378
396
|
let child = null;
|
|
397
|
+
/**
|
|
398
|
+
* EVERY CHILD THIS TURN SPAWNED, not only the last (2026-09-29). A lost
|
|
399
|
+
* resume runs once more fresh (runTurnResumingOnce), so a turn can own two
|
|
400
|
+
* CLIs, and `child` names only the second: the first stayed in
|
|
401
|
+
* `workChildren` for the life of the daemon — one phantom live turn per
|
|
402
|
+
* retry, held against the machine's ceiling, and `workBusy()` true for
|
|
403
|
+
* ever, which holds the self-update and the tray's install for ever.
|
|
404
|
+
*/
|
|
405
|
+
const spawnedChildren = new Set();
|
|
379
406
|
/**
|
|
380
407
|
* WHAT THIS TURN SPENT, AS THE CLI COUNTED IT (2026-09-19).
|
|
381
408
|
*
|
|
@@ -409,7 +436,7 @@ export function createAgentTurnExecution({
|
|
|
409
436
|
try {
|
|
410
437
|
// A turn reads the base ref afresh. Changes the agent has made to its
|
|
411
438
|
// own worktree cannot silently change the next turn's tools.
|
|
412
|
-
projectTools = prepareAgentTools(readBaseTools(repoRoot, baseRef()), rt, process.env, wt);
|
|
439
|
+
projectTools = prepareAgentTools(readBaseTools(repoRoot, baseRef()), rt, process.env, wt, { codexHome: codexAgentHome });
|
|
413
440
|
const agentTurnArgs = {
|
|
414
441
|
prompt:
|
|
415
442
|
job.kind === 'task' && job.task
|
|
@@ -450,6 +477,11 @@ export function createAgentTurnExecution({
|
|
|
450
477
|
}) + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
|
|
451
478
|
agentTools: projectTools,
|
|
452
479
|
...(projectTools.codexHome ? { mcpEnv: { CODEX_HOME: projectTools.codexHome } } : {}),
|
|
480
|
+
// NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
|
|
481
|
+
// a browser profile of the agent's own, kept beside its Codex home —
|
|
482
|
+
// so a Chrome it starts to look at its page never reaches the
|
|
483
|
+
// person's (noWindowEnv.mjs).
|
|
484
|
+
browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
|
|
453
485
|
...(posture === 'build' && rt === 'claude'
|
|
454
486
|
? { mcpArgs: ['--strict-mcp-config', '--mcp-config', projectTools.mcpPath] }
|
|
455
487
|
: posture === 'build' && rt === 'codex'
|
|
@@ -531,6 +563,7 @@ export function createAgentTurnExecution({
|
|
|
531
563
|
},
|
|
532
564
|
onSpawn: (ch) => {
|
|
533
565
|
child = ch;
|
|
566
|
+
spawnedChildren.add(ch);
|
|
534
567
|
// The AGENT it serves — what the machine snapshot charges this
|
|
535
568
|
// child's memory to.
|
|
536
569
|
workChildren.set(ch, agentId);
|
|
@@ -585,7 +618,7 @@ export function createAgentTurnExecution({
|
|
|
585
618
|
*/
|
|
586
619
|
trace.stop();
|
|
587
620
|
await trace.flush(TRACE_FINAL_FLUSH_MS);
|
|
588
|
-
|
|
621
|
+
for (const ch of spawnedChildren) workChildren.delete(ch);
|
|
589
622
|
if (agentChildren.get(place) === child) agentChildren.delete(place);
|
|
590
623
|
if (ranMarker && rt === 'claude') {
|
|
591
624
|
try {
|
|
@@ -694,6 +694,11 @@ export function createWorkSessionTurns({
|
|
|
694
694
|
cwd: dir.wt,
|
|
695
695
|
mcpArgs: mcp.args,
|
|
696
696
|
mcpEnv: mcp.env,
|
|
697
|
+
// A PERSON AT THE KEYBOARD MAY ASK FOR A WINDOW (2026-09-29): a
|
|
698
|
+
// Terminal tab keeps its environment's display. The capture chat
|
|
699
|
+
// does not — it only reads and stages cards, and the person
|
|
700
|
+
// talking to it may be on a phone (noWindowEnv.mjs).
|
|
701
|
+
display: !captureTab,
|
|
697
702
|
runtime: rt.id,
|
|
698
703
|
label: c.cyan('[tab]'),
|
|
699
704
|
// Only codex announces one (thread.started); held here so the id
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.114.
|
|
3
|
+
"version": "0.114.1",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant — 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": {
|