flowviant 0.114.0 → 0.114.2

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.
@@ -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';
@@ -466,6 +466,23 @@ const kickoff = ({ message, askedByName, head, tail }) => {
466
466
  * · Rule 9 is the `assumed` param on the staging verbs: a guess the chat did
467
467
  * not ask about is SAID, one line under the staged row, never folded into
468
468
  * the brief where it reads as something the person asked for.
469
+ *
470
+ * STAGE, THEN ASK — A QUESTION NEVER STANDS IN FOR A CARD (2026-09-29). The
471
+ * owner: "it ignored a subsequent ask … but it worked after i tried again."
472
+ * The follow-ups were not lost; rule 5 said "ask … then wait for the answer",
473
+ * so a new ask with one open detail came back as a question and no card, and
474
+ * to a person watching the Staged rail a reply that stages nothing reads as
475
+ * ignored. Re-sending the same words got a card only because the chat judged
476
+ * the question already asked. So the order flips: the best guess is STAGED
477
+ * first, said through `assumed` (the rail shows it under the row), and the
478
+ * one question follows in chips; the answer moves that row in place
479
+ * (`edit_staged` — restaging loses what the person typed into it). Rule 7's
480
+ * two-product case is the same order. Rule 11 says the chat stays open: a
481
+ * later message is a new ask, never an occasion to recite the staged batch.
482
+ * Rule 1 lost "what you need to know" as a reply in place of a card, and now
483
+ * says the final reply is always words — a turn that ends with none settles
484
+ * as failed on the tab. Rule 4 lost "what earlier chats left": list_staged
485
+ * holds this chat's own rows too, and its tool description says so.
469
486
  */
470
487
  export const SYSTEM_CAPTURE = `You are the human's chosen CLI, in their repository, with ONE job: turn what
471
488
  they say into well-cut task cards on their Flowviant board. You are READ-ONLY
@@ -480,21 +497,29 @@ with the matching name your CLI exposes.
480
497
 
481
498
  1. NARRATE WHILE YOU WORK. Call stream_session_turn with short progress lines
482
499
  as you read and stage. Your FINAL reply is delivered automatically when the
483
- turn ends — do not repeat it through the tool. Keep replies short: what you
484
- staged, or what you need to know — one line each.
500
+ turn ends — always end with it, in words, even when it only says what you
501
+ staged; do not also send it through the tool. Keep it short: what you
502
+ staged, what you guessed, and your one question if you have one — a line
503
+ each.
485
504
  2. STAGE, NEVER FILE. stage_card proposes a new card; stage_card_edit proposes
486
505
  a change to an existing one (read_card first — never replace fields you
487
- have not seen). Everything you stage waits in an area the person reviews
488
- and lands themselves; nothing you do reaches the board directly.
506
+ have not seen); edit_staged changes a row already staged, in place.
507
+ Everything you stage waits in an area the person reviews and lands
508
+ themselves; nothing you do reaches the board directly.
489
509
  3. ONE CARD PER SHIPPABLE UNIT. Break a big ask into the units that will build
490
510
  it, in landing order. Never card-ify chatter, questions, or one unit split
491
511
  thin.
492
512
  4. DEDUPE FIRST, EVERY TIME. Before staging, call list_cards (the OPEN QUEUE —
493
513
  capped, and cards agents already hold are absent from it) and list_staged
494
- (what earlier chats left). If the work exists, say so and point at it;
495
- stage_card_edit it if the ask adds something. Never stage a twin.
496
- 5. ASK ONLY FOR WHAT SHAPES THE CARD. Before staging, ask when — and only
497
- when — the answer changes the card itself:
514
+ (everything waiting to be landed — what you staged earlier in THIS chat
515
+ included). If the work exists, say so and point at it; when the ask adds
516
+ something to it, stage_card_edit a card or edit_staged a staged row.
517
+ Never stage a twin — and never take new work for old: an ask for
518
+ something different gets its own card (rule 11).
519
+ 5. STAGE FIRST, THEN ASK ONLY FOR WHAT SHAPES THE CARD. A question never
520
+ stands in for a card: a message that asks for work ends this turn with
521
+ that work staged. A question is worth asking when — and only when — the
522
+ answer changes the card itself:
498
523
  - its KIND — what it hands back — when the words could mean more than one
499
524
  product (rule 7);
500
525
  - its SCOPE — which pages, screens or parts, when the ask names a family
@@ -503,9 +528,15 @@ with the matching name your CLI exposes.
503
528
  acceptance criteria;
504
529
  - a CONSTRAINT the repository cannot answer — a vendor, a deadline, a
505
530
  browser to support, what must not change.
531
+ Even then, stage your best guess FIRST, the guessed part named in
532
+ \`assumed\` (rule 9); say in one line what you guessed; THEN ask your
533
+ question, as rule 8 says. When they answer, change that staged row in
534
+ place with edit_staged — only the fields the answer moves, and \`assumed\`
535
+ as "" once it is settled — never unstage_card and a fresh stage_card.
506
536
  Never ask what reading the repo answers: read it. One question per
507
- message, asked as rule 8 says, then wait for the answer. A clear ask
508
- stages without ceremony — no question for the sake of one.
537
+ message; any other guess stays said in \`assumed\`, where the person can
538
+ correct it on the row. A clear ask stages without ceremony — no question
539
+ for the sake of one.
509
540
  6. A GOOD CARD: a title naming the outcome, a brief a stranger could start
510
541
  from, acceptance criteria only when the person stated (or the code shows)
511
542
  what done means. No sizes, no owners, no statuses — none of those are
@@ -526,11 +557,13 @@ with the matching name your CLI exposes.
526
557
  "a deck", "a presentation", "a pitch".
527
558
  - "research": a write-up — read the repo and the web; no code changed.
528
559
  "Find out", "research", "compare", "how does X do it", "write up".
529
- When the words fit two products, ASK before staging — rule 5 — in chips, one
530
- option per product that fits, its description its consequence. "Redesign the
560
+ When the words fit two products, STAGE the likelier one, the guess named
561
+ in \`assumed\` ("assumed a code change, not a mockup first"), then ask —
562
+ rule 5 — in chips, one option per product that fits, its description its
563
+ consequence; the answer re-files the row with edit_staged. "Redesign the
531
564
  landing page" fits code and design; "make a chair for the scene" fits code
532
565
  (commit the asset) and model (a model to look at first). Never stage a
533
- guessed kind.
566
+ guessed kind without saying it is a guess.
534
567
  8. HAND THEM THE CHOICES. A question with answers you can name ends your
535
568
  reply with a fenced block the sheet renders as options; picking one sends
536
569
  its label as their next message, so every label must read as an answer a
@@ -558,18 +591,23 @@ with the matching name your CLI exposes.
558
591
  above the block as well: a client that doesn't render the fence shows it
559
592
  as plain text, so the reply has to read as a question with its options
560
593
  either way.
561
- 9. SAY WHAT YOU ASSUMED. When you stage a card on a GUESS you did not ask
562
- about, pass \`assumed\` on stage_card (or stage_card_edit): one short
563
- sentence naming the guess — "assumed the mobile layout too", "assumed
564
- English only". It shows under the staged row, where the person can
565
- correct it before the card lands. Never bury a guess in the brief, where
566
- it reads as something they asked for, and never pass \`assumed\` for
567
- something they said.
594
+ 9. SAY WHAT YOU ASSUMED. When you stage a card on a GUESS — the one your
595
+ question is about (rule 5), or one not worth a question —
596
+ pass \`assumed\` on stage_card (or stage_card_edit): one short sentence
597
+ naming the guess — "assumed the mobile layout too", "assumed English
598
+ only". It shows under the staged row, where the person can correct it
599
+ before the card lands. Never bury a guess in the brief, where it reads as
600
+ something they asked for, and never pass \`assumed\` for something they
601
+ said.
568
602
  10. REFERENCE WHAT THE PROJECT KEPT. When the ask names a kept mockup, 3D model,
569
603
  image, deck or write-up ("implement design A"), call list_library and pass its id as
570
604
  \`references\` on the card you stage, so the agent reads it. When the person's
571
605
  message includes attached library items, pass those ids as \`references\` on
572
- the cards you stage from that message.`;
606
+ the cards you stage from that message.
607
+ 11. THE CHAT STAYS OPEN. Every message is a new ask, however many turns came
608
+ before it (an answer to your question moves its row instead — rule 5).
609
+ When they ask for more after a staged batch, stage the new cards; never
610
+ answer a new ask by restating what is already staged.`;
573
611
 
574
612
  export const WORK_TURN_KICKOFF = ({ sessionId, sessionName, message, askedByName }) =>
575
613
  kickoff({
@@ -1022,6 +1060,8 @@ WHAT TO DO:
1022
1060
  images are data: URIs or inline SVG. Several pages asked for in one card
1023
1061
  go in the ONE file (sections, or tabs you script). Keep it under 2 MB.
1024
1062
 
1063
+ ${LOOK_HEADLESS}
1064
+
1025
1065
  ${ARTIFACT_CONTRACT_TAIL({
1026
1066
  ask: `If you cannot draw it without a DECISION only a person can make — which of
1027
1067
  two directions, which page, what the content should say — STOP AND ASK. Do
@@ -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
  /**
@@ -72,9 +78,23 @@ export const resumeConversationLost = (text, { runtime, sawInit } = {}) => {
72
78
  * is gone. `sawInit` is whether THIS spawn's CLI emitted `system.init` (only
73
79
  * Claude's does, and only Claude's is read). A fresh turn is never retried:
74
80
  * `resume` false answers false whatever the text says.
81
+ *
82
+ * …AND AN EMPTY CLAUDE RESUME THAT FOUND ITS CONVERSATION IS AN EMPTY ANSWER,
83
+ * NOT A LOST CONVERSATION (2026-09-29). Empty used to retry fresh whatever the
84
+ * evidence, so a Claude that emitted init — the conversation WAS there — and
85
+ * then said nothing had the message answered again by a Claude with no memory
86
+ * of the chat, and that fresh conversation's id re-pinned the tab: the
87
+ * follow-up read as if the chat had never happened, and so did every one
88
+ * after it. With init seen, the empty answer stands and settles as what it
89
+ * is, the conversation kept for the next message. Unmeasured init (`sawInit`
90
+ * not true) and the other CLIs keep the old reading: empty is the only sign
91
+ * they give of a conversation that is gone.
75
92
  */
76
- export const resumeRetriesFresh = ({ resume, out, runtime, sawInit }) =>
77
- Boolean(resume) && (!String(out || '').trim() || resumeConversationLost(out, { runtime, sawInit }));
93
+ export const resumeRetriesFresh = ({ resume, out, runtime, sawInit }) => {
94
+ if (!resume) return false;
95
+ if (!String(out || '').trim()) return !(runtime === 'claude' && sawInit === true);
96
+ return resumeConversationLost(out, { runtime, sawInit });
97
+ };
78
98
 
79
99
  /**
80
100
  * ONE SPAWN, AND AT MOST ONE FRESH RETRY — the agent lane's whole sequence,
@@ -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
- export function cliEnv(mcpEnv) {
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
- return withoutMachineCredentials({ ...process.env, ...(mcpEnv ?? {}) });
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. The one
223
- // thing it REMOVES is this daemon's machine credential — see `cliEnv`.
224
- env: cliEnv(mcpEnv),
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).
@@ -14,6 +14,7 @@ import { mkdtempSync, writeFileSync } from 'node:fs';
14
14
  import { tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
  import { MODEL, USER_AGENT } from './config.mjs';
17
+ import { NO_SETTING_SOURCES, personalClaudeSettings, withPersonalSettings } from './claudePersonal.mjs';
17
18
 
18
19
  function claudeMcp(token, mcpUrl) {
19
20
  const dir = mkdtempSync(join(tmpdir(), 'flowviant-mcp-'));
@@ -72,7 +73,7 @@ export const CLAUDE_RUNTIME = {
72
73
  * strongest form of it available anywhere: `--append-system-prompt` sits
73
74
  * above the conversation rather than inside it.
74
75
  */
75
- args({ prompt, system, model, effort, resume, streamJson, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, agentTools }) {
76
+ args({ prompt, system, model, effort, resume, streamJson, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, agentTools, personalSettings }) {
76
77
  const a = [];
77
78
  // THREE ANSWERS TO ONE QUESTION — "what conversation is this?" — and they
78
79
  // are mutually exclusive, never combined.
@@ -99,11 +100,38 @@ export const CLAUDE_RUNTIME = {
99
100
  else if (resumeThreadId) a.push('--resume', resumeThreadId);
100
101
  else if (resume) a.push('--continue');
101
102
  a.push('-p', prompt, '--append-system-prompt', system);
103
+ /**
104
+ * WHOSE SETTINGS A TURN READS (2026-09-29, claudePersonal.mjs).
105
+ *
106
+ * EVERY FENCED POSTURE READS NONE — every profile but `build`: an agent's
107
+ * design and research cards, the pre-review, the planner, intake, the
108
+ * wiki and the capture chat. Its permissions and hooks are its posture's
109
+ * alone; of the person's file it keeps only the login and the model
110
+ * defaults, folded into the posture's `--settings`. Keyed on `build`
111
+ * rather than on a list of fenced names, so a posture added later is
112
+ * fenced unless it is the build.
113
+ *
114
+ * THE BUILD POSTURE KEEPS THE PERSON'S OWN, on a code agent and on a
115
+ * Terminal tab alike. `--dangerously-skip-permissions` already admits
116
+ * everything, so no personal rule can widen it; what a personal file adds
117
+ * there is the operator narrowing their own box (a deny rule held a
118
+ * bypass turn, measured) and their own CLAUDE.md, skills and subagents,
119
+ * which an empty source list would take away with no flag to keep them.
120
+ * Codex's build is not hermetic either, for the same reason
121
+ * (runtimeCodex.mjs). STATED, not closed: under FLOWVIANT_SAFE=1 the
122
+ * build is a curated list, and the operator's own allow rules or
123
+ * `defaultMode` can still widen it — two choices of the same operator on
124
+ * their own box, and a personal allow is the only way a SAFE build runs a
125
+ * tool its list does not name (`pnpm`, `cargo`, `make`). A code agent names `user` so the agent-editable
126
+ * worktree's settings and hooks never load; a Terminal tab names nothing
127
+ * and is the person's Claude Code, project files included. A resumed
128
+ * agent renders the CURRENT base snapshot, not the first turn's cache.
129
+ */
130
+ const fenced = profile !== 'build';
131
+ if (fenced) a.push(...NO_SETTING_SOURCES);
132
+ else if (agentTools) a.push('--setting-sources', 'user');
102
133
  if (agentTools) {
103
- // Keep personal settings, but never load the agent-editable project's
104
- // settings, hooks, skills or CLAUDE.md discovery. A resumed agent must
105
- // render the CURRENT base snapshot, not the first turn's prompt cache.
106
- a.push('--setting-sources', 'user', '--system-prompt-snapshot', 'off');
134
+ a.push('--system-prompt-snapshot', 'off');
107
135
  if (agentTools.pluginDir) a.push('--plugin-dir', agentTools.pluginDir);
108
136
  }
109
137
  // These fenced postures have no project MCP, even if Claude discovers a
@@ -119,7 +147,9 @@ export const CLAUDE_RUNTIME = {
119
147
  // does not refuse the path the prompt just handed it. Before `perm`,
120
148
  // because `--allowedTools` is variadic and would swallow the flag.
121
149
  if (knowledgeDir) a.push('--add-dir', knowledgeDir);
122
- a.push(...perm);
150
+ // `personalSettings` is the caller's copy when it has one (a test); a
151
+ // turn reads the person's file here, at spawn, as the CLI would.
152
+ a.push(...(fenced ? withPersonalSettings(perm, personalSettings ?? personalClaudeSettings()) : perm));
123
153
  return a;
124
154
  },
125
155
  parse: null, // claudeStream.mjs owns its own stream parser (unchanged)
@@ -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 its isolated per-turn home (projectToolRuntimes);
247
- // `:root` reads admit it, so the agent copies what it keeps into the one
248
- // writable directory, where the artifact scan finds it.
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
@@ -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
- // both called "BRIF AI", both bound to the same checkout, so this picker
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
- // skadooble in my calendar repo" is the confusion this exists to end.
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 skadooble law);
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 skadooble bug.
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 skadooble.
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 skadooble case fixed for the person who is actually looking,
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
- return withoutMachineCredentials(env);
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
- if (child) workChildren.delete(child);
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 {