flowviant 0.114.1 → 0.115.0
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/lib/agentTaskKinds.mjs +10 -6
- package/bin/lib/artifacts.mjs +43 -1
- package/bin/lib/claudePersonal.mjs +145 -0
- package/bin/lib/codexPersonal.mjs +467 -0
- package/bin/lib/codexRelay.mjs +63 -0
- package/bin/lib/prompts.mjs +74 -22
- package/bin/lib/resumeLost.mjs +16 -2
- package/bin/lib/runTurn.mjs +24 -1
- package/bin/lib/runtimeClaude.mjs +44 -11
- package/bin/lib/runtimeCodex.mjs +165 -4
- package/bin/lib/runtimeEvents.mjs +9 -2
- package/bin/lib/turnProfile.mjs +3 -2
- package/bin/lib/workAgentTurnExecution.mjs +23 -3
- package/bin/lib/workSessionTurns.mjs +145 -30
- package/package.json +1 -1
package/bin/lib/prompts.mjs
CHANGED
|
@@ -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({
|
|
@@ -1073,8 +1111,22 @@ ${ARTIFACT_CONTRACT_TAIL({
|
|
|
1073
1111
|
* WHICH CONTRACT A TURN RUNS UNDER, by the card's kind. Absent or unknown is
|
|
1074
1112
|
* code, and code is `SYSTEM_AGENT` — the same object, so a code turn's system
|
|
1075
1113
|
* prompt is byte-for-byte what it was before kinds existed.
|
|
1114
|
+
*
|
|
1115
|
+
* …AND WHO IT IS SPOKEN TO (0.115.0, 2026-09-29). The mockup, 3D-model and
|
|
1116
|
+
* deck contracts open "You are the human's own Claude" because Claude ran them
|
|
1117
|
+
* alone; Codex runs them now (`codexDesignFence`), and a contract that calls
|
|
1118
|
+
* Codex Claude is the prompt telling the agent something false about itself.
|
|
1119
|
+
* `runtimeLabel` is the CLI's own name ("Codex"); absent, or Claude's, the
|
|
1120
|
+
* contract is the pinned string itself, byte for byte. Only that opening
|
|
1121
|
+
* names a CLI — nothing else in these contracts is Claude's (no tool names,
|
|
1122
|
+
* no flags), so nothing else changes.
|
|
1076
1123
|
*/
|
|
1077
|
-
|
|
1124
|
+
const CLAUDE_OPENING = "You are the human's own Claude,";
|
|
1125
|
+
export const SYSTEM_AGENT_FOR = (taskKind, runtimeLabel = null) => {
|
|
1126
|
+
const contract = AGENT_CONTRACTS[agentTaskKindOf(taskKind)];
|
|
1127
|
+
if (!runtimeLabel || /^claude\b/i.test(runtimeLabel) || !contract.startsWith(CLAUDE_OPENING)) return contract;
|
|
1128
|
+
return `You are the human's own ${runtimeLabel},${contract.slice(CLAUDE_OPENING.length)}`;
|
|
1129
|
+
};
|
|
1078
1130
|
|
|
1079
1131
|
/**
|
|
1080
1132
|
* THE STRINGS EACH KIND IS HANDED, keyed by the kinds in agentTaskKinds.mjs —
|
package/bin/lib/resumeLost.mjs
CHANGED
|
@@ -78,9 +78,23 @@ export const resumeConversationLost = (text, { runtime, sawInit } = {}) => {
|
|
|
78
78
|
* is gone. `sawInit` is whether THIS spawn's CLI emitted `system.init` (only
|
|
79
79
|
* Claude's does, and only Claude's is read). A fresh turn is never retried:
|
|
80
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.
|
|
81
92
|
*/
|
|
82
|
-
export const resumeRetriesFresh = ({ resume, out, runtime, sawInit }) =>
|
|
83
|
-
|
|
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
|
+
};
|
|
84
98
|
|
|
85
99
|
/**
|
|
86
100
|
* ONE SPAWN, AND AT MOST ONE FRESH RETRY — the agent lane's whole sequence,
|
package/bin/lib/runTurn.mjs
CHANGED
|
@@ -123,6 +123,23 @@ export function runTurn(opts) {
|
|
|
123
123
|
resolve('');
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* THE PERSON'S LOGIN, FOR A POSTURE THAT READS NONE OF THEIR CONFIG
|
|
128
|
+
* (2026-09-29, codexPersonal.mjs). Asked of the runtime that has one
|
|
129
|
+
* (Codex; Claude reads its own inside its adapter), with the environment
|
|
130
|
+
* this turn's CLI will run in — its CODEX_HOME is the agent's own when the
|
|
131
|
+
* lane keeps one — so the file read is the file the CLI would have read.
|
|
132
|
+
* Its `args` go to the adapter; its `env`, the literal secrets those name,
|
|
133
|
+
* to the child below and never to argv.
|
|
134
|
+
*/
|
|
135
|
+
const personal =
|
|
136
|
+
rt.personal?.({
|
|
137
|
+
profile: turnProfile.adapterProfile,
|
|
138
|
+
vaultDir,
|
|
139
|
+
model,
|
|
140
|
+
effort,
|
|
141
|
+
env: { ...process.env, ...(mcpEnv ?? {}) },
|
|
142
|
+
}) ?? null;
|
|
126
143
|
// Pin the model — never inherit the user's global default (which for Claude
|
|
127
144
|
// may be a 1M/long-context tier their subscription can't bill autonomous
|
|
128
145
|
// work on). A per-task override (chosen in the app, validated server-side
|
|
@@ -139,6 +156,7 @@ export function runTurn(opts) {
|
|
|
139
156
|
// which posture a turn is running under. There is no precedence chain left
|
|
140
157
|
// to get wrong: the caller named one profile (turnProfile.mjs).
|
|
141
158
|
const args = rt.args({
|
|
159
|
+
personal,
|
|
142
160
|
prompt,
|
|
143
161
|
system,
|
|
144
162
|
model,
|
|
@@ -237,7 +255,12 @@ export function runTurn(opts) {
|
|
|
237
255
|
// this environment, and handing it a curated one signs it out. What it
|
|
238
256
|
// REMOVES is this daemon's machine credential and, for a turn nobody is
|
|
239
257
|
// sitting at, every way to a window — see `cliEnv`.
|
|
240
|
-
|
|
258
|
+
// The login's literal secrets first, so nothing of the lane's (its MCP
|
|
259
|
+
// token, its CODEX_HOME) can be overwritten by one.
|
|
260
|
+
env: cliEnv(personal?.env ? { ...personal.env, ...(mcpEnv ?? {}) } : mcpEnv, {
|
|
261
|
+
display: display === true,
|
|
262
|
+
browserHome: browser?.dir,
|
|
263
|
+
}),
|
|
241
264
|
});
|
|
242
265
|
if (browser) {
|
|
243
266
|
child.once('close', browser.cleanup);
|
|
@@ -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-'));
|
|
@@ -51,11 +52,14 @@ export const CLAUDE_RUNTIME = {
|
|
|
51
52
|
* "consult" and "plan" currently mean. That is a statement about where the
|
|
52
53
|
* contract was written, not a claim that only Claude could ever satisfy it.
|
|
53
54
|
*/
|
|
54
|
-
// `design` and `research` (0.97.0)
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
55
|
+
// `design` and `research` (0.97.0): each is an `--allowedTools` list with a
|
|
56
|
+
// PATH-SCOPED write (claudePosture.mjs). `research` is Claude's alone;
|
|
57
|
+
// `design` is Codex's too since 0.115.0 (2026-09-29), whose permission
|
|
58
|
+
// profile was measured to write only the artifacts directory
|
|
59
|
+
// (runtimeCodex.mjs `codexDesignFence`). agy's flags cannot express "write
|
|
60
|
+
// only this directory" (runtimeAntigravity.mjs). A runtime that does not
|
|
61
|
+
// declare a posture is refused such a card before spawn, in words — never
|
|
62
|
+
// handed a build turn instead.
|
|
59
63
|
profiles: ['build', 'wiki', 'consult', 'plan', 'design', 'research'],
|
|
60
64
|
/**
|
|
61
65
|
* THE EFFORTS THIS CLI SPELLS — the daemon's copy of the server's
|
|
@@ -72,7 +76,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
72
76
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
73
77
|
* above the conversation rather than inside it.
|
|
74
78
|
*/
|
|
75
|
-
args({ prompt, system, model, effort, resume, streamJson, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, agentTools }) {
|
|
79
|
+
args({ prompt, system, model, effort, resume, streamJson, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, agentTools, personalSettings }) {
|
|
76
80
|
const a = [];
|
|
77
81
|
// THREE ANSWERS TO ONE QUESTION — "what conversation is this?" — and they
|
|
78
82
|
// are mutually exclusive, never combined.
|
|
@@ -99,11 +103,38 @@ export const CLAUDE_RUNTIME = {
|
|
|
99
103
|
else if (resumeThreadId) a.push('--resume', resumeThreadId);
|
|
100
104
|
else if (resume) a.push('--continue');
|
|
101
105
|
a.push('-p', prompt, '--append-system-prompt', system);
|
|
106
|
+
/**
|
|
107
|
+
* WHOSE SETTINGS A TURN READS (2026-09-29, claudePersonal.mjs).
|
|
108
|
+
*
|
|
109
|
+
* EVERY FENCED POSTURE READS NONE — every profile but `build`: an agent's
|
|
110
|
+
* design and research cards, the pre-review, the planner, intake, the
|
|
111
|
+
* wiki and the capture chat. Its permissions and hooks are its posture's
|
|
112
|
+
* alone; of the person's file it keeps only the login and the model
|
|
113
|
+
* defaults, folded into the posture's `--settings`. Keyed on `build`
|
|
114
|
+
* rather than on a list of fenced names, so a posture added later is
|
|
115
|
+
* fenced unless it is the build.
|
|
116
|
+
*
|
|
117
|
+
* THE BUILD POSTURE KEEPS THE PERSON'S OWN, on a code agent and on a
|
|
118
|
+
* Terminal tab alike. `--dangerously-skip-permissions` already admits
|
|
119
|
+
* everything, so no personal rule can widen it; what a personal file adds
|
|
120
|
+
* there is the operator narrowing their own box (a deny rule held a
|
|
121
|
+
* bypass turn, measured) and their own CLAUDE.md, skills and subagents,
|
|
122
|
+
* which an empty source list would take away with no flag to keep them.
|
|
123
|
+
* Codex's build is not hermetic either, for the same reason
|
|
124
|
+
* (runtimeCodex.mjs). STATED, not closed: under FLOWVIANT_SAFE=1 the
|
|
125
|
+
* build is a curated list, and the operator's own allow rules or
|
|
126
|
+
* `defaultMode` can still widen it — two choices of the same operator on
|
|
127
|
+
* their own box, and a personal allow is the only way a SAFE build runs a
|
|
128
|
+
* tool its list does not name (`pnpm`, `cargo`, `make`). A code agent names `user` so the agent-editable
|
|
129
|
+
* worktree's settings and hooks never load; a Terminal tab names nothing
|
|
130
|
+
* and is the person's Claude Code, project files included. A resumed
|
|
131
|
+
* agent renders the CURRENT base snapshot, not the first turn's cache.
|
|
132
|
+
*/
|
|
133
|
+
const fenced = profile !== 'build';
|
|
134
|
+
if (fenced) a.push(...NO_SETTING_SOURCES);
|
|
135
|
+
else if (agentTools) a.push('--setting-sources', 'user');
|
|
102
136
|
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');
|
|
137
|
+
a.push('--system-prompt-snapshot', 'off');
|
|
107
138
|
if (agentTools.pluginDir) a.push('--plugin-dir', agentTools.pluginDir);
|
|
108
139
|
}
|
|
109
140
|
// These fenced postures have no project MCP, even if Claude discovers a
|
|
@@ -119,7 +150,9 @@ export const CLAUDE_RUNTIME = {
|
|
|
119
150
|
// does not refuse the path the prompt just handed it. Before `perm`,
|
|
120
151
|
// because `--allowedTools` is variadic and would swallow the flag.
|
|
121
152
|
if (knowledgeDir) a.push('--add-dir', knowledgeDir);
|
|
122
|
-
a
|
|
153
|
+
// `personalSettings` is the caller's copy when it has one (a test); a
|
|
154
|
+
// turn reads the person's file here, at spawn, as the CLI would.
|
|
155
|
+
a.push(...(fenced ? withPersonalSettings(perm, personalSettings ?? personalClaudeSettings()) : perm));
|
|
123
156
|
return a;
|
|
124
157
|
},
|
|
125
158
|
parse: null, // claudeStream.mjs owns its own stream parser (unchanged)
|
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -12,8 +12,30 @@
|
|
|
12
12
|
import { isAbsolute, join } from 'node:path';
|
|
13
13
|
import { SAFE } from './config.mjs';
|
|
14
14
|
import { parseCodexLine } from './runtimeEvents.mjs';
|
|
15
|
-
import { ARTIFACT_DIR } from './artifacts.mjs';
|
|
15
|
+
import { ARTIFACT_DIR, FENCE_TMP_DIR } from './artifacts.mjs';
|
|
16
|
+
import { codexPersonalTurn } from './codexPersonal.mjs';
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* FLOWVIANT'S OWN TOOLS NEED NO APPROVAL FROM A CHAT NOBODY CAN ANSWER
|
|
20
|
+
* (2026-09-29). The owner's Codex New task chat staged nothing: "flowviant
|
|
21
|
+
* rejected the tool calls because approval is required and this session's
|
|
22
|
+
* policy forbids it". MEASURED on codex-cli 0.156.1 with a fake MCP server
|
|
23
|
+
* whose one tool carries no annotations (Flowviant's carry none): under the
|
|
24
|
+
* capture chat's read-only argv the call failed "MCP tool call requires
|
|
25
|
+
* approval, but approval policy is never" and never reached the server; under
|
|
26
|
+
* the build posture (workspace-write) the same call ran unasked. With
|
|
27
|
+
* `mcp_servers.flowviant.default_tools_approval_mode="approve"` beside the
|
|
28
|
+
* read-only argv it ran — and in the same turn a shell write still failed
|
|
29
|
+
* "Read-only file system" and an escalation still failed "approval policy is
|
|
30
|
+
* Never; reject command".
|
|
31
|
+
*
|
|
32
|
+
* So the approval is this ONE server's, never a blanket `approval_policy`:
|
|
33
|
+
* what its tools may do is the per-turn token's scope, which the server
|
|
34
|
+
* enforces (a capture token stages and reads; it files nothing), and every
|
|
35
|
+
* other door — the shell, file writes, escalation, any other server — keeps
|
|
36
|
+
* the posture's own rule. Wherever the server is mounted: the capture chat
|
|
37
|
+
* needs it, and a Terminal tab's build posture already ran the calls.
|
|
38
|
+
*/
|
|
17
39
|
function codexMcp(token, mcpUrl) {
|
|
18
40
|
return {
|
|
19
41
|
dir: null, // nothing written — the token rides in the environment
|
|
@@ -22,11 +44,23 @@ function codexMcp(token, mcpUrl) {
|
|
|
22
44
|
`mcp_servers.flowviant.url="${mcpUrl}"`,
|
|
23
45
|
'-c',
|
|
24
46
|
'mcp_servers.flowviant.bearer_token_env_var="FLOWVIANT_MCP_TOKEN"',
|
|
47
|
+
'-c',
|
|
48
|
+
'mcp_servers.flowviant.default_tools_approval_mode="approve"',
|
|
25
49
|
],
|
|
26
50
|
env: { FLOWVIANT_MCP_TOKEN: token },
|
|
27
51
|
};
|
|
28
52
|
}
|
|
29
53
|
|
|
54
|
+
/**
|
|
55
|
+
* THE POSTURES THAT READ NONE OF THE PERSON'S CONFIG — the branches of `args()`
|
|
56
|
+
* below that pass `--ignore-user-config`, said once so the adapter and its
|
|
57
|
+
* `personal` hook cannot disagree about which turns carry the person's login
|
|
58
|
+
* back (codexPersonal.mjs). A wiki turn with no vault is a build turn there,
|
|
59
|
+
* and so here.
|
|
60
|
+
*/
|
|
61
|
+
export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
|
|
62
|
+
profile === 'consult' || profile === 'plan' || profile === 'image' || profile === 'design' || (profile === 'wiki' && Boolean(vaultDir));
|
|
63
|
+
|
|
30
64
|
/**
|
|
31
65
|
* WHAT A FENCED CODEX TURN MAY NOT REACH FOR (0.114.0). codex-cli 0.156.1
|
|
32
66
|
* ships `browser_use` (and its external and full-CDP forms), an in-app
|
|
@@ -71,8 +105,13 @@ export const CODEX_RUNTIME = {
|
|
|
71
105
|
* AND `image` (0.114.0), which ONLY Codex declares: the image card's fence —
|
|
72
106
|
* see the branch in `args()`. It is the one kind posture that is not
|
|
73
107
|
* Claude's, because the pictures come from Codex's own image tool.
|
|
108
|
+
*
|
|
109
|
+
* AND `design` (0.115.0), shared with Claude: the mockup, 3D-model and deck
|
|
110
|
+
* cards' fence, expressed as `codexDesignFence` below. Research is NOT
|
|
111
|
+
* declared: its web and Codex's whole-box reads cannot be fenced apart
|
|
112
|
+
* (see that function).
|
|
74
113
|
*/
|
|
75
|
-
profiles: ['build', 'consult', 'wiki', 'plan', 'image'],
|
|
114
|
+
profiles: ['build', 'consult', 'wiki', 'plan', 'image', 'design'],
|
|
76
115
|
/** `model_reasoning_effort` tops out at `xhigh` — there is no `max`, and a
|
|
77
116
|
* queued or skewed job naming it would be an argument Codex refuses. */
|
|
78
117
|
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
@@ -102,7 +141,7 @@ export const CODEX_RUNTIME = {
|
|
|
102
141
|
* placed before it. Appending them after the positional is the kind of argv
|
|
103
142
|
* that parses today and stops parsing on some future clap upgrade.
|
|
104
143
|
*/
|
|
105
|
-
args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools }) {
|
|
144
|
+
args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null }) {
|
|
106
145
|
// Adoption resumes a conversation in ITS OWN CLI's store (claude forks,
|
|
107
146
|
// agy moves) — codex has no adoptable store wired yet. Reaching here
|
|
108
147
|
// with an adopt id is a wiring mistake upstream, and it fails loudly on
|
|
@@ -163,6 +202,10 @@ export const CODEX_RUNTIME = {
|
|
|
163
202
|
|
|
164
203
|
// Sub-agents would be a second turn whose posture nobody here chose.
|
|
165
204
|
a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false', ...NO_REACH_OUT);
|
|
205
|
+
// Image generation ships ON in codex-cli 0.156.1 even under
|
|
206
|
+
// --ignore-user-config (measured 2026-09-29): a turn that asked for no
|
|
207
|
+
// picture is handed no tool that spends image credits.
|
|
208
|
+
a.push('--disable', 'image_generation');
|
|
166
209
|
|
|
167
210
|
// HERMETIC. Without these a user's ~/.codex/config.toml, a project
|
|
168
211
|
// `.rules` execpolicy file, or their own MCP servers can widen a posture
|
|
@@ -190,6 +233,10 @@ export const CODEX_RUNTIME = {
|
|
|
190
233
|
a.push('-c', 'sandbox_mode="read-only"');
|
|
191
234
|
a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
|
|
192
235
|
a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false', ...NO_REACH_OUT);
|
|
236
|
+
// Image generation ships ON in codex-cli 0.156.1 even under
|
|
237
|
+
// --ignore-user-config (measured 2026-09-29): a turn that asked for no
|
|
238
|
+
// picture is handed no tool that spends image credits.
|
|
239
|
+
a.push('--disable', 'image_generation');
|
|
193
240
|
a.push('--ignore-user-config', '--ignore-rules');
|
|
194
241
|
} else if (profile === 'wiki' && vaultDir) {
|
|
195
242
|
// THE CARTOGRAPHER, AND THIS ONE IS STRICTER THAN CLAUDE'S.
|
|
@@ -217,6 +264,7 @@ export const CODEX_RUNTIME = {
|
|
|
217
264
|
// Selected by config: `codex exec` (0.156.1) has no `-P`, so every Codex
|
|
218
265
|
// wiki turn was refused before it started (0.114.0).
|
|
219
266
|
a.push('-c', 'default_permissions="flowviantwiki"');
|
|
267
|
+
a.push('--disable', 'image_generation');
|
|
220
268
|
a.push(...NO_REACH_OUT);
|
|
221
269
|
a.push('--ignore-user-config', '--ignore-rules');
|
|
222
270
|
} else if (profile === 'image') {
|
|
@@ -261,7 +309,12 @@ export const CODEX_RUNTIME = {
|
|
|
261
309
|
a.push('-c', 'permissions.flowviantimage.extends=":read-only"');
|
|
262
310
|
// The table WHOLE (the wiki branch's reason: a dotted key splits on the
|
|
263
311
|
// dots inside the path), the path as a TOML basic string.
|
|
264
|
-
|
|
312
|
+
// …and the fence's own scratch (2026-09-29, the design fence's
|
|
313
|
+
// measurement: a `cat > x <<EOF` needs a temp file, and /tmp is
|
|
314
|
+
// read-only in here), named for the shells through TMPDIR/TMPPREFIX.
|
|
315
|
+
const scratch = join(cwd, FENCE_TMP_DIR);
|
|
316
|
+
a.push('-c', `permissions.flowviantimage.filesystem={${JSON.stringify(artifacts)}="write",${JSON.stringify(scratch)}="write"}`);
|
|
317
|
+
a.push('-c', `shell_environment_policy.set={TMPDIR=${JSON.stringify(scratch)},TMPPREFIX=${JSON.stringify(join(scratch, 'zsh'))}}`);
|
|
265
318
|
a.push('-c', 'default_permissions="flowviantimage"');
|
|
266
319
|
a.push(...NO_REACH_OUT);
|
|
267
320
|
// Nobody is on this end of the pipe to approve an escalation.
|
|
@@ -272,6 +325,10 @@ export const CODEX_RUNTIME = {
|
|
|
272
325
|
a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
|
|
273
326
|
a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false', ...NO_REACH_OUT);
|
|
274
327
|
a.push('--ignore-user-config', '--ignore-rules');
|
|
328
|
+
} else if (profile === 'design') {
|
|
329
|
+
// A MOCKUP, 3D-MODEL OR DECK CARD (0.115.0): the fence is
|
|
330
|
+
// `codexDesignFence`'s, argued and measured at its definition below.
|
|
331
|
+
a.push(...codexDesignFence(cwd));
|
|
275
332
|
} else {
|
|
276
333
|
// The daemon's build posture, mapped: SAFE keeps writes inside the
|
|
277
334
|
// workspace, the default lets the agent run its own tests and git
|
|
@@ -286,13 +343,117 @@ export const CODEX_RUNTIME = {
|
|
|
286
343
|
a.push('-c', `sandbox_mode="${SAFE ? 'workspace-write' : 'danger-full-access'}"`);
|
|
287
344
|
}
|
|
288
345
|
|
|
346
|
+
/**
|
|
347
|
+
* …AND THE PERSON'S LOGIN RIDES BACK BESIDE THE FLAG (2026-09-29,
|
|
348
|
+
* codexPersonal.mjs): where the login lives, the provider and endpoint it
|
|
349
|
+
* signs into, and the model an unpinned turn runs — `-c` pairs after
|
|
350
|
+
* `--ignore-user-config`, never a key that widens the posture above.
|
|
351
|
+
* Keyed on the flag itself, so a fenced branch added later carries the
|
|
352
|
+
* login the day it ignores the file. `personal` is runTurn's, read where
|
|
353
|
+
* the turn's Codex will look (the `personal` hook below); a caller that
|
|
354
|
+
* hands none gets the posture as it was.
|
|
355
|
+
*/
|
|
356
|
+
if (a.includes('--ignore-user-config')) a.push(...(personal?.args ?? []));
|
|
357
|
+
|
|
289
358
|
a.push(...mcp, ...resultSchemaArgs);
|
|
290
359
|
a.push(`${system}\n\n---\n\n${prompt}`);
|
|
291
360
|
return a;
|
|
292
361
|
},
|
|
362
|
+
/**
|
|
363
|
+
* THE PERSON'S LOGIN FOR A FENCED TURN, read once per spawn by runTurn from
|
|
364
|
+
* the turn's own environment (its CODEX_HOME — an agent's own home when the
|
|
365
|
+
* lane keeps one): `{ args, env }`, the `-c` pairs `args()` places and the
|
|
366
|
+
* literal secrets they name, which ride the child's environment, never argv.
|
|
367
|
+
* Null for a turn that reads the person's config itself.
|
|
368
|
+
*/
|
|
369
|
+
personal: ({ profile, vaultDir, env, model, effort } = {}) =>
|
|
370
|
+
codexIgnoresUserConfig({ profile, vaultDir }) ? codexPersonalTurn({ env, model, effort }) : null,
|
|
293
371
|
parse: parseCodexLine,
|
|
294
372
|
/** `codex exec --output-schema <file>` constrains the FINAL message to a
|
|
295
373
|
* JSON Schema. Only needed on the mediated path; the direct path reports
|
|
296
374
|
* through MCP tool calls, which are already structured. */
|
|
297
375
|
resultSchema: (path) => ['--output-schema', path],
|
|
298
376
|
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* THE MOCKUP, 3D-MODEL AND DECK CARDS' FENCE ON CODEX (0.115.0; 2026-09-29, the
|
|
380
|
+
* owner: "its saying 3d models run on claude only but thats not true").
|
|
381
|
+
*
|
|
382
|
+
* The image fence's shape without the image tool: a permission profile that
|
|
383
|
+
* reads the repo and writes ONLY `<worktree>/.flowviant/artifacts/`, selected
|
|
384
|
+
* by `default_permissions` (`codex exec` and `exec resume` refuse `-P` on
|
|
385
|
+
* 0.156.1), no network, no web search, no sub-agents, no browser, computer or
|
|
386
|
+
* apps, nobody's config or rules, and no approvals to wait on. Nothing about
|
|
387
|
+
* the card's contract needs more: every one of the three is one HTML page, and
|
|
388
|
+
* a page's CDN scripts load in the viewer, never on this machine.
|
|
389
|
+
*
|
|
390
|
+
* MEASURED on codex-cli 0.156.1 (Linux, bubblewrap), with gpt-6-luna at low
|
|
391
|
+
* effort, in a scratch git repository (2026-09-29), with exactly this argv:
|
|
392
|
+
* · the WRITE FENCE holds for both of Codex's ways to write: its
|
|
393
|
+
* `apply_patch` of `src/escape-patch.txt` was refused by Codex itself
|
|
394
|
+
* ("patch rejected: writing outside of the project; rejected by user
|
|
395
|
+
* approval settings"), and `echo x > src/escape-shell.txt` and
|
|
396
|
+
* `echo x > /tmp/fv-escape.txt` each failed "read-only file system";
|
|
397
|
+
* `apply_patch` of `.flowviant/artifacts/ok-patch.txt` landed;
|
|
398
|
+
* · NO NETWORK: `curl https://example.com` failed "Could not resolve host";
|
|
399
|
+
* NO WEB: the model reported no search tool;
|
|
400
|
+
* · THE TOOL LIST was `apply_patch`, `clock__curr_time`, `exec_command`,
|
|
401
|
+
* `view_image`, `write_stdin` — and `image_gen__imagegen` WITHOUT
|
|
402
|
+
* `--disable image_generation`: image generation ships ON in 0.156.1 even
|
|
403
|
+
* under `--ignore-user-config`, so it is switched off here by name, and a
|
|
404
|
+
* mockup never spends the person's image credits;
|
|
405
|
+
* · THE CONTRACTS DELIVER: the real mockup, 3D-model and deck contracts each
|
|
406
|
+
* produced their one page under `.flowviant/artifacts/` (the model page
|
|
407
|
+
* with its `glb-ready`/`export-glb` handshake and GLTFExporter, the deck
|
|
408
|
+
* with its sections, arrow keys and print rules) and changed no repository
|
|
409
|
+
* file (`git status` clean but for the ignored directory);
|
|
410
|
+
* · THE SCRATCH IS LOAD-BEARING: a deck turn that wrote its page with a
|
|
411
|
+
* here-document got "can't create temp file for here document: read-only
|
|
412
|
+
* file system", left a 0-byte page and stopped to ask; with the scratch
|
|
413
|
+
* granted and named (below) the same here-document wrote the page, and a
|
|
414
|
+
* resumed turn (`exec resume <id>` with this argv, as `CODEX_RUNTIME.args`
|
|
415
|
+
* builds it) edited it in place.
|
|
416
|
+
*
|
|
417
|
+
* STATED, not closed: Codex's permission profile READS the whole box, where
|
|
418
|
+
* Claude's design list reads only the worktree and the knowledge library. It
|
|
419
|
+
* cannot be narrowed on 0.156.1 — a `"none"` entry over the home directory
|
|
420
|
+
* hides Codex's own sandbox helper when Codex is installed there ("bwrap:
|
|
421
|
+
* execvp …/codex: No such file or directory"), and masking a second FILE
|
|
422
|
+
* fails every command ("bwrap: Can't write data to file …: Bad file
|
|
423
|
+
* descriptor"). With the network and the web shut, the one road out is the
|
|
424
|
+
* artifact itself, which is scrubbed of the machine's known secret values and
|
|
425
|
+
* of any Flowviant credential by shape on the way up — the image fence's
|
|
426
|
+
* standing (0.114.0), and why RESEARCH is not declared here: a write-up needs
|
|
427
|
+
* the web, and a turn that can read `.env` and `~/.flowviant/credentials.json`
|
|
428
|
+
* while holding `web_search` (both measured readable under this profile with
|
|
429
|
+
* `web_search="live"`) is the exfiltration Claude's research list was fenced
|
|
430
|
+
* to prevent.
|
|
431
|
+
*/
|
|
432
|
+
export function codexDesignFence(cwd) {
|
|
433
|
+
if (!cwd || !isAbsolute(cwd)) {
|
|
434
|
+
throw new Error("a design turn needs its worktree to fence — none reached codex's builder");
|
|
435
|
+
}
|
|
436
|
+
const artifacts = join(cwd, ARTIFACT_DIR);
|
|
437
|
+
// THE TURN'S OWN SCRATCH (artifacts.mjs `ensureFenceScratch`, made empty by
|
|
438
|
+
// the lane before spawn): `/tmp` is read-only in here, and a here-document
|
|
439
|
+
// needs a temp file — measured, a `cat > page.html <<EOF` failed "can't
|
|
440
|
+
// create temp file for here document: read-only file system" and the turn
|
|
441
|
+
// stopped to ask. Granted, and named by the two variables the shells read
|
|
442
|
+
// (`TMPDIR`; zsh's `TMPPREFIX`), set for every command through
|
|
443
|
+
// `shell_environment_policy.set` — measured to reach the sandboxed shell.
|
|
444
|
+
const scratch = join(cwd, FENCE_TMP_DIR);
|
|
445
|
+
return [
|
|
446
|
+
'-c', 'permissions.flowviantdesign.extends=":read-only"',
|
|
447
|
+
// The table WHOLE and the path a TOML basic string — the wiki branch's
|
|
448
|
+
// reason: a dotted key splits on the dots inside the path.
|
|
449
|
+
'-c', `permissions.flowviantdesign.filesystem={${JSON.stringify(artifacts)}="write",${JSON.stringify(scratch)}="write"}`,
|
|
450
|
+
'-c', `shell_environment_policy.set={TMPDIR=${JSON.stringify(scratch)},TMPPREFIX=${JSON.stringify(join(scratch, 'zsh'))}}`,
|
|
451
|
+
'-c', 'default_permissions="flowviantdesign"',
|
|
452
|
+
'-c', 'approval_policy="never"',
|
|
453
|
+
'--disable', 'image_generation',
|
|
454
|
+
'-c', 'tools.web_search=false', '-c', 'web_search="disabled"',
|
|
455
|
+
'-c', 'features.multi_agent=false', '-c', 'features.goals=false',
|
|
456
|
+
...NO_REACH_OUT,
|
|
457
|
+
'--ignore-user-config', '--ignore-rules',
|
|
458
|
+
];
|
|
459
|
+
}
|
|
@@ -277,8 +277,15 @@ function humanizeCodexItem(item = {}, cwd = '') {
|
|
|
277
277
|
const verb = first.kind === 'add' ? '+' : first.kind === 'delete' ? '-' : '~';
|
|
278
278
|
return { kind: 'write', path: p, label: `${verb} ${tail || 'file'}` };
|
|
279
279
|
}
|
|
280
|
-
case 'mcp_tool_call':
|
|
281
|
-
|
|
280
|
+
case 'mcp_tool_call': {
|
|
281
|
+
// A FAILED CALL KEEPS ITS OWN WORDS (2026-09-29), beside a label that is
|
|
282
|
+
// unchanged: codex-cli 0.156.1 closes a call it would not run as
|
|
283
|
+
// `status: "failed"`, `error.message` "MCP tool call requires approval,
|
|
284
|
+
// but approval policy is never" (measured), and the tab lane says so
|
|
285
|
+
// in words (codexRelay.mjs) instead of leaving a chat that staged nothing.
|
|
286
|
+
const error = String(item.error?.message ?? '');
|
|
287
|
+
return { kind: 'tool', label: `${item.server ?? 'mcp'}.${item.tool ?? ''}`, ...(error ? { error } : {}) };
|
|
288
|
+
}
|
|
282
289
|
case 'web_search':
|
|
283
290
|
return { kind: 'search', label: `search ${oneLine(item.query, 60)}` };
|
|
284
291
|
default:
|
package/bin/lib/turnProfile.mjs
CHANGED
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
* - design / research / image — a non-code card's posture
|
|
23
23
|
* (agentTaskKinds.mjs), refused on any runtime that does not
|
|
24
24
|
* DECLARE it, because every other adapter's fallback is its
|
|
25
|
-
* build branch.
|
|
26
|
-
*
|
|
25
|
+
* build branch. research is Claude Code's; design is
|
|
26
|
+
* Claude Code's and, since 0.115.0, Codex's; image (0.114.0)
|
|
27
|
+
* is Codex's.
|
|
27
28
|
*
|
|
28
29
|
* Claude's permission list for each name lives beside the lists themselves
|
|
29
30
|
* (`claudePermFor`, claudePosture.mjs); this module owns the vocabulary and the
|