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.
- package/README.md +9 -0
- package/bin/cli.mjs +2 -2
- package/bin/lib/artifactContracts.mjs +26 -0
- package/bin/lib/claudePersonal.mjs +145 -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 +62 -22
- package/bin/lib/resumeLost.mjs +22 -2
- package/bin/lib/runTurn.mjs +26 -6
- package/bin/lib/runtimeClaude.mjs +36 -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 +116 -23
- package/package.json +1 -1
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';
|
|
@@ -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 —
|
|
484
|
-
staged
|
|
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)
|
|
488
|
-
|
|
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
|
-
(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
508
|
-
stages without ceremony — no question
|
|
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,
|
|
530
|
-
|
|
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
|
|
562
|
-
about,
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
something they
|
|
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
|
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
|
/**
|
|
@@ -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
|
-
|
|
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,
|
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).
|
|
@@ -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
|
-
|
|
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
|
|
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)
|
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 {
|