flowviant 0.114.1 → 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/lib/claudePersonal.mjs +145 -0
- package/bin/lib/prompts.mjs +59 -21
- package/bin/lib/resumeLost.mjs +16 -2
- package/bin/lib/runtimeClaude.mjs +36 -6
- package/bin/lib/workSessionTurns.mjs +111 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,15 @@ Two knobs bound the blast radius, and both are worth setting on a shared box:
|
|
|
147
147
|
its permission engine is machine-wide — which is surfaced in the app rather
|
|
148
148
|
than papered over.
|
|
149
149
|
|
|
150
|
+
Fenced turns — design, 3D-model, deck and research cards, the pre-review,
|
|
151
|
+
the planner, intake, the wiki and the New task chat — read none of your Claude
|
|
152
|
+
Code settings files: their permissions and hooks are Flowviant's posture
|
|
153
|
+
alone, and of `~/.claude/settings.json` they keep only how Claude signs in
|
|
154
|
+
(`apiKeyHelper`, `env` and the like) and your model defaults. Code agents and
|
|
155
|
+
Terminal tabs still read yours, so your own allow rules and `defaultMode` add
|
|
156
|
+
to `FLOWVIANT_SAFE=1`'s allowlist there. Codex's fenced turns run with
|
|
157
|
+
`--ignore-user-config` the same way.
|
|
158
|
+
|
|
150
159
|
The posture is reported on every poll and shown in the project's
|
|
151
160
|
Settings → Machine section, so the team can see whether the box runs the
|
|
152
161
|
guarded toolset or full permissions.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A FENCED CLAUDE TURN TAKES ITS PERMISSIONS AND HOOKS FROM FLOWVIANT ALONE
|
|
3
|
+
* (2026-09-29, the owner, asked whether an agent's fence should ignore the
|
|
4
|
+
* person's own permission settings: "yes, the agent's fence can ignore").
|
|
5
|
+
*
|
|
6
|
+
* A 3D-model agent under the design posture, whose whole shell is
|
|
7
|
+
* `Bash(ls:*)`, ran `google-chrome` and Playwright scripts. Every fenced lane
|
|
8
|
+
* read the person's `~/.claude/settings.json` — an agent turn through
|
|
9
|
+
* `--setting-sources user`, the pre-review, planner, intake, wiki and capture
|
|
10
|
+
* chat by naming no source at all, which ALSO reads the cwd's
|
|
11
|
+
* `.claude/settings*.json`, and the pre-review's cwd is the agent's own
|
|
12
|
+
* worktree. A settings file is a second author of the posture. MEASURED on
|
|
13
|
+
* Claude Code 2.1.284 — see claudePersonal.test.mjs for the probes and the
|
|
14
|
+
* offline check that pins them on the real CLI. In short: a personal
|
|
15
|
+
* `permissions.allow` rule admitted a command the list refused, a personal
|
|
16
|
+
* `defaultMode: "bypassPermissions"` turned the fenced turn into a bypass, and
|
|
17
|
+
* personal and worktree hooks fired; under `--setting-sources ''` none of them
|
|
18
|
+
* loaded, and the turn ran on the same login.
|
|
19
|
+
*
|
|
20
|
+
* AN EMPTY SOURCE LIST DROPS THE WHOLE USER SOURCE, not only its permissions:
|
|
21
|
+
* `~/.claude/CLAUDE.md`, personal skills and subagents went too (measured),
|
|
22
|
+
* and with the file go the plugins it enables. No flag keeps half of it — a
|
|
23
|
+
* flag setting's allow rules only add to the person's. On a fenced lane that
|
|
24
|
+
* is the right loss: the posture and the contract are Flowviant's, the base
|
|
25
|
+
* branch's instructions and skills arrive through the prompt and the reviewed
|
|
26
|
+
* plugin (`--plugin-dir` still loads, measured), and a personal plugin brings
|
|
27
|
+
* hooks of its own. Codex's fenced postures are hermetic the same way
|
|
28
|
+
* (`--ignore-user-config`, runtimeCodex.mjs).
|
|
29
|
+
*
|
|
30
|
+
* WHAT IS KEPT is what makes the CLI the person's CLI, never what it may do:
|
|
31
|
+
* how it signs in and reaches its provider, how a model the turn does not pin
|
|
32
|
+
* runs, and how long the person's own store is kept. An ALLOWLIST, so a key a
|
|
33
|
+
* later CLI adds — a sandbox auto-allow, a permission mode — never rides by
|
|
34
|
+
* default. It rides INSIDE the posture's own `--settings`, because the CLI
|
|
35
|
+
* reads one: given two, only the last one's hook fired (measured).
|
|
36
|
+
*/
|
|
37
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
38
|
+
import { homedir } from 'node:os';
|
|
39
|
+
import { join } from 'node:path';
|
|
40
|
+
import { MACHINE_CREDENTIAL_ENV } from './machineEnv.mjs';
|
|
41
|
+
import { WINDOW_ENV_REMOVED, withoutWindows } from './noWindowEnv.mjs';
|
|
42
|
+
|
|
43
|
+
/** The flag pair that loads no settings file — user, project or local.
|
|
44
|
+
* Managed (policy) settings and `--settings` still apply. */
|
|
45
|
+
export const NO_SETTING_SOURCES = Object.freeze(['--setting-sources', '']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The personal settings a fenced turn keeps, by key.
|
|
49
|
+
*
|
|
50
|
+
* · THE LOGIN — the helpers the CLI runs to authenticate and where it signs
|
|
51
|
+
* in, and `env`, where a login routes (ANTHROPIC_BASE_URL,
|
|
52
|
+
* CLAUDE_CODE_USE_BEDROCK, AWS_PROFILE). `env` gives the person's file no
|
|
53
|
+
* more reach than the operator's shell, which every turn inherits already;
|
|
54
|
+
* what a turn's environment promises is still removed from it
|
|
55
|
+
* (`keptEnv`).
|
|
56
|
+
* · HOW AN UNPINNED MODEL RUNS ("Machine default uses the CLI's own
|
|
57
|
+
* setting"): per-model effort, thinking, the model map a provider needs.
|
|
58
|
+
* `model` itself is not here: every Claude turn names `--model`, the
|
|
59
|
+
* daemon's pin when the card has none (config.mjs), so the person's
|
|
60
|
+
* default model never reached a daemon turn before either.
|
|
61
|
+
* · THE PERSON'S OWN STORE: how long transcripts live and whether memory is
|
|
62
|
+
* kept, so a fenced turn's start never prunes or writes on a default the
|
|
63
|
+
* person turned off.
|
|
64
|
+
*/
|
|
65
|
+
export const PERSONAL_KEPT = Object.freeze([
|
|
66
|
+
'apiKeyHelper',
|
|
67
|
+
'proxyAuthHelper',
|
|
68
|
+
'awsAuthRefresh',
|
|
69
|
+
'awsCredentialExport',
|
|
70
|
+
'gcpAuthRefresh',
|
|
71
|
+
'forceLoginMethod',
|
|
72
|
+
'forceLoginOrgUUID',
|
|
73
|
+
'forceLoginGatewayUrl',
|
|
74
|
+
'env',
|
|
75
|
+
'modelOverrides',
|
|
76
|
+
'modelSettings',
|
|
77
|
+
'effortLevel',
|
|
78
|
+
'maxEffortLevel',
|
|
79
|
+
'alwaysThinkingEnabled',
|
|
80
|
+
'fallbackModel',
|
|
81
|
+
'cleanupPeriodDays',
|
|
82
|
+
'autoMemoryEnabled',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
/** What a turn nobody is sitting at has set or removed (noWindowEnv.mjs) and
|
|
86
|
+
* the machine credential (machineEnv.mjs): never put back by a settings file. */
|
|
87
|
+
const TURN_OWNED_ENV = new Set([
|
|
88
|
+
...MACHINE_CREDENTIAL_ENV,
|
|
89
|
+
...WINDOW_ENV_REMOVED,
|
|
90
|
+
...Object.keys(withoutWindows({}, '/')),
|
|
91
|
+
]);
|
|
92
|
+
function keptEnv(env) {
|
|
93
|
+
if (!env || typeof env !== 'object' || Array.isArray(env)) return null;
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const [k, v] of Object.entries(env)) if (typeof v === 'string' && !TURN_OWNED_ENV.has(k)) out[k] = v;
|
|
96
|
+
return Object.keys(out).length ? out : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Far past any real settings file; a bigger one is not read. */
|
|
100
|
+
const SETTINGS_MAX_BYTES = 1024 * 1024;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The person's user settings file — `$CLAUDE_CONFIG_DIR/settings.json`, else
|
|
104
|
+
* `~/.claude/settings.json`, where the CLI itself looks — reduced to
|
|
105
|
+
* PERSONAL_KEPT. `{}` when it is absent, unreadable or not a JSON object: the
|
|
106
|
+
* turn then runs on the CLI's stored login and defaults, and the fence is the
|
|
107
|
+
* same either way.
|
|
108
|
+
*/
|
|
109
|
+
export function personalClaudeSettings(env = process.env) {
|
|
110
|
+
const dir = env.CLAUDE_CONFIG_DIR || join(env.HOME || homedir(), '.claude');
|
|
111
|
+
const path = join(dir, 'settings.json');
|
|
112
|
+
let raw;
|
|
113
|
+
try {
|
|
114
|
+
if (statSync(path).size > SETTINGS_MAX_BYTES) return {};
|
|
115
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
116
|
+
} catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const key of PERSONAL_KEPT) {
|
|
122
|
+
if (!Object.hasOwn(raw, key) || raw[key] == null) continue;
|
|
123
|
+
if (key !== 'env') out[key] = raw[key];
|
|
124
|
+
else {
|
|
125
|
+
const kept = keptEnv(raw.env);
|
|
126
|
+
if (kept) out.env = kept;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `perm` with `kept` folded into its `--settings` — the posture's keys win, so
|
|
134
|
+
* a kept key a posture ever names is the fence's word. A posture with no
|
|
135
|
+
* `--settings` gets one before its lists (`--allowedTools` is variadic). With
|
|
136
|
+
* nothing kept, `perm` is returned as it was.
|
|
137
|
+
*/
|
|
138
|
+
export function withPersonalSettings(perm, kept) {
|
|
139
|
+
const personal = kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : {};
|
|
140
|
+
if (!Object.keys(personal).length) return [...perm];
|
|
141
|
+
const at = perm.indexOf('--settings');
|
|
142
|
+
if (at < 0) return ['--settings', JSON.stringify(personal), ...perm];
|
|
143
|
+
const posture = JSON.parse(perm[at + 1]);
|
|
144
|
+
return [...perm.slice(0, at), '--settings', JSON.stringify({ ...personal, ...posture }), ...perm.slice(at + 2)];
|
|
145
|
+
}
|
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({
|
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,
|
|
@@ -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)
|
|
@@ -59,6 +59,14 @@ import { createCommandAudit } from './workCommandAudit.mjs';
|
|
|
59
59
|
export const PLAN_TURN_SENTENCE =
|
|
60
60
|
'THIS IS A PLANNING TURN: the person switched this tab to plan mode, so read what you need, decide, and answer with the plan itself as your reply — change nothing (the CLI refuses every write this turn, whatever the mechanics above say about editing, committing or artifacts), and do not try to leave plan mode; they switch it off in the tab when they want the plan carried out.';
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* HOW LONG, AND HOW MANY, SETTLED TURNS THE LANE REMEMBERS (2026-09-29) — see
|
|
64
|
+
* `settledHere` below. Ten minutes is far past any roster a poll could still
|
|
65
|
+
* be holding; the cap keeps a box that answers all day from growing the map.
|
|
66
|
+
*/
|
|
67
|
+
export const SETTLED_HERE_TTL_MS = 10 * 60 * 1000;
|
|
68
|
+
export const SETTLED_HERE_MAX = 500;
|
|
69
|
+
|
|
62
70
|
export function createWorkSessionTurns({
|
|
63
71
|
repoRoot,
|
|
64
72
|
baseDir,
|
|
@@ -91,14 +99,67 @@ export function createWorkSessionTurns({
|
|
|
91
99
|
// The real CLI driver; injectable only so the lane can be driven in a test
|
|
92
100
|
// without spending a model turn (the wiki runner's shape).
|
|
93
101
|
runTurn = defaultRunTurn,
|
|
102
|
+
// The clock `settledHere` ages by; injectable only for its bound's test.
|
|
103
|
+
now = Date.now,
|
|
94
104
|
}) {
|
|
95
105
|
const workAnswering = new Set(); // turn ids currently queued/running here
|
|
96
106
|
const MAX_WORK_TRIES = 3;
|
|
97
107
|
|
|
108
|
+
/**
|
|
109
|
+
* A TURN THIS LANE SETTLED IS NEVER RUN AGAIN (2026-09-29).
|
|
110
|
+
*
|
|
111
|
+
* A delivered settle forgets the turn everywhere: the report queue drops it
|
|
112
|
+
* from `pendingWorkReports` and `workAttempts`, and the turn's `finally`
|
|
113
|
+
* drops it from `workAnswering`. A roster READ made before that settle
|
|
114
|
+
* landed still offers it — the poll was in flight while the POST went out —
|
|
115
|
+
* and nothing was left to say it had run: the turn ran twice, settled
|
|
116
|
+
* twice, the rerun's reply overwrote the first, and the person's follow-up
|
|
117
|
+
* waited behind a turn nobody asked for. So every settle this lane makes
|
|
118
|
+
* goes through `settleTurn`, which remembers the turn id, and a job in the
|
|
119
|
+
* map is skipped where jobs are picked. Remembered BEFORE the POST: a
|
|
120
|
+
* settle still in flight is covered by `workAnswering`, one that could not
|
|
121
|
+
* be delivered by `pendingWorkReports`, and this covers the one that was.
|
|
122
|
+
*/
|
|
123
|
+
const settledHere = new Map(); // turnId -> when this lane settled it, oldest first
|
|
124
|
+
const forgetStale = (at) => {
|
|
125
|
+
for (const [id, when] of settledHere) {
|
|
126
|
+
if (settledHere.size <= SETTLED_HERE_MAX && at - when < SETTLED_HERE_TTL_MS) break;
|
|
127
|
+
settledHere.delete(id);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const settledRecently = (turnId) => {
|
|
131
|
+
forgetStale(now());
|
|
132
|
+
return settledHere.has(turnId);
|
|
133
|
+
};
|
|
134
|
+
const settleTurn = (turnId, payload) => {
|
|
135
|
+
const at = now();
|
|
136
|
+
settledHere.delete(turnId); // re-inserted at the end: the map stays in time order
|
|
137
|
+
settledHere.set(turnId, at);
|
|
138
|
+
forgetStale(at);
|
|
139
|
+
return settleWorkTurn(turnId, payload);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A PLACE A TURN OF THIS PROCESS IS STARTING IN (2026-09-29) — the turn
|
|
144
|
+
* lock's in-process half, keyed by the lock's own path.
|
|
145
|
+
*
|
|
146
|
+
* `flowviant-turn.lock` is written at SPAWN, several awaits after it is
|
|
147
|
+
* read (a credential mint, the attachment fetch), so two turns of one
|
|
148
|
+
* session offered in one roster both read "unlocked" and both spawned: two
|
|
149
|
+
* CLIs resuming one conversation at once, the incoherence the lock exists
|
|
150
|
+
* to prevent. The claim is checked and taken synchronously beside the file
|
|
151
|
+
* check, so the second turn waits for a later poll like any locked one,
|
|
152
|
+
* and it is released in the turn's outer `finally`, after its settle.
|
|
153
|
+
*/
|
|
154
|
+
const startingIn = new Set();
|
|
155
|
+
|
|
98
156
|
const processWorkTurns = (jobs) => {
|
|
99
157
|
for (const job of jobs ?? []) {
|
|
100
158
|
if (!job || typeof job.id !== 'string' || !job.body || !job.sessionId) continue;
|
|
101
159
|
if (workAnswering.has(job.id)) continue;
|
|
160
|
+
// Settled by this lane already: a roster read from before the settle
|
|
161
|
+
// landed. See `settledHere`.
|
|
162
|
+
if (settledRecently(job.id)) continue;
|
|
102
163
|
// The turn already RAN and its answer sits in the delivery queue — never
|
|
103
164
|
// run it again while the report is merely undelivered.
|
|
104
165
|
if (pendingWorkReports.has(job.id)) continue;
|
|
@@ -140,7 +201,7 @@ export function createWorkSessionTurns({
|
|
|
140
201
|
* tab for the server's whole expiry window.
|
|
141
202
|
*/
|
|
142
203
|
if (place !== REPO_PLACE && !isSafePathSegment(place)) {
|
|
143
|
-
void
|
|
204
|
+
void settleTurn(job.id, {
|
|
144
205
|
ok: false,
|
|
145
206
|
answer:
|
|
146
207
|
'the server named a working directory this machine refuses to use — close and reopen the tab, then send the message again',
|
|
@@ -178,12 +239,15 @@ export function createWorkSessionTurns({
|
|
|
178
239
|
* out here beside `usage`, and spread wherever it is. Last write wins
|
|
179
240
|
* across a fresh retry: that spawn is the one that answered. */
|
|
180
241
|
let model = null;
|
|
242
|
+
/** The `startingIn` key this turn claimed — released in the outer
|
|
243
|
+
* `finally`, and only by the turn that took it. */
|
|
244
|
+
let startClaim = null;
|
|
181
245
|
try {
|
|
182
246
|
const tries = workAttempts.get(job.id) ?? 0;
|
|
183
247
|
if (tries >= MAX_WORK_TRIES) {
|
|
184
248
|
// Out of local tries: SETTLE, don't skip — a silently skipped turn
|
|
185
249
|
// strands the tab for the server's whole 24h expiry window.
|
|
186
|
-
await
|
|
250
|
+
await settleTurn(job.id, {
|
|
187
251
|
ok: false,
|
|
188
252
|
answer: `the turn failed ${tries} times on this machine — check the daemon log, then send the message again`,
|
|
189
253
|
});
|
|
@@ -209,7 +273,7 @@ export function createWorkSessionTurns({
|
|
|
209
273
|
typeof job.adopt.id !== 'string' ||
|
|
210
274
|
!/^[0-9a-f][0-9a-f-]{6,62}$/i.test(job.adopt.id)
|
|
211
275
|
) {
|
|
212
|
-
await
|
|
276
|
+
await settleTurn(job.id, {
|
|
213
277
|
ok: false,
|
|
214
278
|
answer: 'that terminal session id is not one this machine can resume',
|
|
215
279
|
});
|
|
@@ -223,7 +287,7 @@ export function createWorkSessionTurns({
|
|
|
223
287
|
srcCwd = null;
|
|
224
288
|
}
|
|
225
289
|
if (!srcCwd) {
|
|
226
|
-
await
|
|
290
|
+
await settleTurn(job.id, {
|
|
227
291
|
ok: false,
|
|
228
292
|
answer: "the terminal session's directory no longer exists on the machine",
|
|
229
293
|
});
|
|
@@ -247,14 +311,14 @@ export function createWorkSessionTurns({
|
|
|
247
311
|
/* keep the literal path */
|
|
248
312
|
}
|
|
249
313
|
if (!under(srcCwd, realRoot)) {
|
|
250
|
-
await
|
|
314
|
+
await settleTurn(job.id, {
|
|
251
315
|
ok: false,
|
|
252
316
|
answer: "the terminal session's directory is outside this project's repository",
|
|
253
317
|
});
|
|
254
318
|
return;
|
|
255
319
|
}
|
|
256
320
|
if (under(srcCwd, realBase)) {
|
|
257
|
-
await
|
|
321
|
+
await settleTurn(job.id, {
|
|
258
322
|
ok: false,
|
|
259
323
|
answer:
|
|
260
324
|
"that directory is one of the daemon's own worktrees — its session is already a tab, not something to adopt",
|
|
@@ -264,7 +328,7 @@ export function createWorkSessionTurns({
|
|
|
264
328
|
try {
|
|
265
329
|
srcHead = git(['rev-parse', 'HEAD'], srcCwd);
|
|
266
330
|
} catch {
|
|
267
|
-
await
|
|
331
|
+
await settleTurn(job.id, {
|
|
268
332
|
ok: false,
|
|
269
333
|
answer:
|
|
270
334
|
"the terminal session's directory is not a usable git checkout (no HEAD to branch from)",
|
|
@@ -281,7 +345,7 @@ export function createWorkSessionTurns({
|
|
|
281
345
|
? isAgyConversationLive(job.adopt.id)
|
|
282
346
|
: isTerminalSessionLive(job.adopt.id);
|
|
283
347
|
if (adoptLive) {
|
|
284
|
-
await
|
|
348
|
+
await settleTurn(job.id, {
|
|
285
349
|
ok: false,
|
|
286
350
|
answer:
|
|
287
351
|
'That terminal session is still open on the machine — close it there first, then adopt.',
|
|
@@ -297,7 +361,7 @@ export function createWorkSessionTurns({
|
|
|
297
361
|
// the session's own id, which is what every tab has always done.
|
|
298
362
|
const dir = placeWtFor(place, adopting ? srcHead : undefined);
|
|
299
363
|
if (!dir) {
|
|
300
|
-
await
|
|
364
|
+
await settleTurn(job.id, {
|
|
301
365
|
ok: false,
|
|
302
366
|
answer:
|
|
303
367
|
'the session worktree could not be opened on the machine — check the daemon log',
|
|
@@ -308,13 +372,20 @@ export function createWorkSessionTurns({
|
|
|
308
372
|
// life, most likely; the lock outlives a restart. Leave the job
|
|
309
373
|
// pending and look again next poll; spawning a second CLI would put
|
|
310
374
|
// two Claudes in one held context. Costs no attempt: nothing ran.
|
|
375
|
+
// …AND ONE OF OURS MAY BE ABOUT TO: `startingIn`, checked and
|
|
376
|
+
// claimed with no await between, because the file is only written
|
|
377
|
+
// at spawn. Keyed by the lock's path (the directory, when the git
|
|
378
|
+
// dir cannot be named) — the file lock's own scope.
|
|
311
379
|
const lockPath = sessionMetaPath(dir.wt, 'flowviant-turn.lock');
|
|
312
|
-
|
|
380
|
+
const startKey = lockPath || dir.wt;
|
|
381
|
+
if (startingIn.has(startKey) || turnLockedByLivePid(lockPath)) {
|
|
313
382
|
warn(
|
|
314
383
|
`a turn is already running in "${job.sessionName || job.sessionId}" — waiting for it to finish`
|
|
315
384
|
);
|
|
316
385
|
return;
|
|
317
386
|
}
|
|
387
|
+
startingIn.add(startKey);
|
|
388
|
+
startClaim = startKey;
|
|
318
389
|
// WHICH BRAIN the roster says this tab speaks (null/absent = Claude,
|
|
319
390
|
// which is what every tab ran on until now) — honored by
|
|
320
391
|
// sessionRuntime: on a first turn a named runtime IS the pick, and a
|
|
@@ -327,14 +398,14 @@ export function createWorkSessionTurns({
|
|
|
327
398
|
if (rt.mismatch) {
|
|
328
399
|
// Something upstream changed this tab's identity mid-life. A held
|
|
329
400
|
// context must never be answered by a different brain — say so.
|
|
330
|
-
await
|
|
401
|
+
await settleTurn(job.id, {
|
|
331
402
|
ok: false,
|
|
332
403
|
answer: `this tab is pinned to ${rt.mismatch.pin} but the server says it is a ${rt.mismatch.runtime} tab — reopen a new tab`,
|
|
333
404
|
});
|
|
334
405
|
return;
|
|
335
406
|
}
|
|
336
407
|
if (rt.missing) {
|
|
337
|
-
await
|
|
408
|
+
await settleTurn(job.id, {
|
|
338
409
|
ok: false,
|
|
339
410
|
answer: `this session runs on ${rt.missing}, which is no longer installed on the machine — reinstall it, or open a new tab`,
|
|
340
411
|
});
|
|
@@ -346,14 +417,14 @@ export function createWorkSessionTurns({
|
|
|
346
417
|
// tab can run on (Antigravity has no MCP config, and the session's
|
|
347
418
|
// whole control plane rides one). An honest sentence beats the
|
|
348
419
|
// mcpFor throw this used to crash into every turn.
|
|
349
|
-
await
|
|
420
|
+
await settleTurn(job.id, {
|
|
350
421
|
ok: false,
|
|
351
422
|
answer: `this session runs on ${rt.unsupported}, which cannot drive a Terminal tab on this machine — open a new tab`,
|
|
352
423
|
});
|
|
353
424
|
return;
|
|
354
425
|
}
|
|
355
426
|
if (!rt.id) {
|
|
356
|
-
await
|
|
427
|
+
await settleTurn(job.id, {
|
|
357
428
|
ok: false,
|
|
358
429
|
answer:
|
|
359
430
|
'No coding CLI is installed on the machine — install Claude Code (or another supported CLI), then send the message again',
|
|
@@ -366,7 +437,7 @@ export function createWorkSessionTurns({
|
|
|
366
437
|
// (--conversation). Codex has no adoptable store yet, and its
|
|
367
438
|
// args builder backstops this with a loud throw — but a sentence
|
|
368
439
|
// here beats a stack there.
|
|
369
|
-
await
|
|
440
|
+
await settleTurn(job.id, {
|
|
370
441
|
ok: false,
|
|
371
442
|
answer:
|
|
372
443
|
'adopting this terminal session needs its own CLI on the machine — install it, then try again',
|
|
@@ -383,7 +454,7 @@ export function createWorkSessionTurns({
|
|
|
383
454
|
*/
|
|
384
455
|
const planTurn = job.planMode === true && job.capture !== true;
|
|
385
456
|
if (planTurn && rt.id !== 'claude') {
|
|
386
|
-
await
|
|
457
|
+
await settleTurn(job.id, {
|
|
387
458
|
ok: false,
|
|
388
459
|
answer: `plan mode runs on Claude Code only, and this tab is ${rt.id} — turn plan off in the tab, then send the message again`,
|
|
389
460
|
});
|
|
@@ -409,7 +480,7 @@ export function createWorkSessionTurns({
|
|
|
409
480
|
return;
|
|
410
481
|
}
|
|
411
482
|
if (mint?.gone) {
|
|
412
|
-
await
|
|
483
|
+
await settleTurn(job.id, {
|
|
413
484
|
ok: false,
|
|
414
485
|
answer:
|
|
415
486
|
'Flowviant no longer offers this session to this machine — the tab may have been closed or moved',
|
|
@@ -417,7 +488,7 @@ export function createWorkSessionTurns({
|
|
|
417
488
|
return;
|
|
418
489
|
}
|
|
419
490
|
if (!mint?.token) {
|
|
420
|
-
await
|
|
491
|
+
await settleTurn(job.id, {
|
|
421
492
|
ok: false,
|
|
422
493
|
answer:
|
|
423
494
|
'the machine could not mint a session credential from Flowviant — check its connection, then send the message again',
|
|
@@ -765,6 +836,11 @@ export function createWorkSessionTurns({
|
|
|
765
836
|
* that tab replied identically, with nothing on any surface able
|
|
766
837
|
* to clear it. A tab bricked forever by its own CLI pruning its
|
|
767
838
|
* history, which it does on its own schedule.
|
|
839
|
+
*
|
|
840
|
+
* An EMPTY Claude resume that emitted init is not retried
|
|
841
|
+
* (2026-09-29, resumeLost.mjs): its conversation was found, and a
|
|
842
|
+
* fresh run would answer with no memory of the chat and re-pin the
|
|
843
|
+
* tab to that new conversation.
|
|
768
844
|
*/
|
|
769
845
|
if (
|
|
770
846
|
!adopting &&
|
|
@@ -831,7 +907,7 @@ export function createWorkSessionTurns({
|
|
|
831
907
|
// transcript is most likely gone (cleaned, expired, deleted). Say
|
|
832
908
|
// exactly that; no sessionRef is recorded, so the server keeps
|
|
833
909
|
// offering the adoption and a retry after the user checks is cheap.
|
|
834
|
-
await
|
|
910
|
+
await settleTurn(job.id, {
|
|
835
911
|
ok: false,
|
|
836
912
|
answer: "Couldn't resume the terminal session — it may have been removed.",
|
|
837
913
|
// Whatever it DID before coming back empty is exactly the
|
|
@@ -862,13 +938,24 @@ export function createWorkSessionTurns({
|
|
|
862
938
|
}
|
|
863
939
|
}
|
|
864
940
|
}
|
|
865
|
-
|
|
941
|
+
/**
|
|
942
|
+
* NO WORDS, SAID AS MEASURED (2026-09-29). Claude's init event means
|
|
943
|
+
* it ran in this conversation — kept, and resumed by the next
|
|
944
|
+
* message, since an empty resume that found it no longer runs fresh
|
|
945
|
+
* (resumeLost.mjs) — and its result held no words. "Signed out" is
|
|
946
|
+
* a guess, left to the case where no init came.
|
|
947
|
+
*/
|
|
948
|
+
const noWords =
|
|
949
|
+
rt.id === 'claude' && seenClaudeSession
|
|
950
|
+
? 'Claude Code ran in this conversation and returned no words — send the message again'
|
|
951
|
+
: 'the turn produced no output on the machine — its CLI may be signed out; try again';
|
|
952
|
+
await settleTurn(job.id, {
|
|
866
953
|
ok: answer.length > 0,
|
|
867
954
|
answer:
|
|
868
955
|
answer.length > 0
|
|
869
956
|
? // Scrub: a reply can quote config or env-adjacent code.
|
|
870
957
|
envScrub(answer).slice(0, 16000)
|
|
871
|
-
:
|
|
958
|
+
: noWords,
|
|
872
959
|
// Only a turn that actually SPOKE proves a conversation lives
|
|
873
960
|
// here. Recording the path unconditionally is how a crashed first
|
|
874
961
|
// turn used to brick resume for the session's whole life.
|
|
@@ -883,7 +970,7 @@ export function createWorkSessionTurns({
|
|
|
883
970
|
if (answer.length > 0) ok(`${c.cyan('tab')} ${c.dim('— replied in the session')}`);
|
|
884
971
|
else warn('session turn produced no output — settled as failed');
|
|
885
972
|
} catch (e) {
|
|
886
|
-
await
|
|
973
|
+
await settleTurn(job.id, {
|
|
887
974
|
ok: false,
|
|
888
975
|
// Scrub, like every string that leaves this machine: an exception
|
|
889
976
|
// routinely quotes command output, and command output can quote a
|
|
@@ -899,6 +986,7 @@ export function createWorkSessionTurns({
|
|
|
899
986
|
warn(`session turn failed: ${e?.message ?? e}`);
|
|
900
987
|
} finally {
|
|
901
988
|
workAnswering.delete(job.id);
|
|
989
|
+
if (startClaim) startingIn.delete(startClaim);
|
|
902
990
|
// Nothing spawned, or everything already has: releasing twice is the
|
|
903
991
|
// normal case and costs nothing. A reservation that leaked would
|
|
904
992
|
// shrink this machine's ceiling for the life of the process.
|
|
@@ -928,5 +1016,5 @@ export function createWorkSessionTurns({
|
|
|
928
1016
|
}
|
|
929
1017
|
};
|
|
930
1018
|
|
|
931
|
-
return { processWorkTurns, workAnswering };
|
|
1019
|
+
return { processWorkTurns, workAnswering, settledHere };
|
|
932
1020
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.114.
|
|
3
|
+
"version": "0.114.2",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant — Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|