flowviant 0.61.0 → 0.63.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/bin/lib/claude.mjs +11 -1
- package/bin/lib/devResolve.mjs +167 -0
- package/bin/lib/runtimes.mjs +12 -1
- package/bin/lib/work.mjs +150 -0
- package/package.json +1 -1
package/bin/lib/claude.mjs
CHANGED
|
@@ -236,7 +236,17 @@ function handleStreamLine(line, { cwd, emit, onActivity, appendText, answerFromR
|
|
|
236
236
|
//
|
|
237
237
|
// Only ever REPORTED, never enforced. Flowviant does not decide what your
|
|
238
238
|
// Claude can do; it relays what your Claude said it has.
|
|
239
|
-
|
|
239
|
+
// `sessionId` rides along for one reason: a `-p` turn WRITES a transcript,
|
|
240
|
+
// and `localSessions.mjs` offers the newest ended session per directory as
|
|
241
|
+
// ADOPTABLE — so any headless turn we run for our own purposes would leave
|
|
242
|
+
// a phantom untitled session in the `+` menu. The caller that needs to
|
|
243
|
+
// clean up after itself cannot do so without this id.
|
|
244
|
+
if (Array.isArray(ev.skills) || typeof ev.session_id === 'string') {
|
|
245
|
+
onInit?.({
|
|
246
|
+
skills: Array.isArray(ev.skills) ? ev.skills.map(String) : undefined,
|
|
247
|
+
sessionId: typeof ev.session_id === 'string' ? ev.session_id : undefined,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
240
250
|
} else if (ev.type === 'result') {
|
|
241
251
|
// The final assistant text (carries WIKI_DONE / REGROUND_DONE).
|
|
242
252
|
if (typeof ev.result === 'string') appendText(ev.result + '\n');
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT STARTS THIS PROJECT — asked of a Claude, on the machine, in the
|
|
3
|
+
* background.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. Starting a dev server used to require a command a human had
|
|
6
|
+
* typed into a sheet, prefilled `npm run dev`. That prefill is a guess about
|
|
7
|
+
* somebody else's stack, and it was called out as one: "the option to run or
|
|
8
|
+
* start the dev server shouldnt be npm run dev or show it as npm run dev
|
|
9
|
+
* because thats not agnostic to everyones set up. no need to show that."
|
|
10
|
+
*
|
|
11
|
+
* The mechanism is the driver's own: "we are literally asking a claude session
|
|
12
|
+
* to start it for us" — and, decisively, "i still want claude to start the
|
|
13
|
+
* server for me but i dont want it to literally open a chat. have it do it in
|
|
14
|
+
* the background." So this is a headless turn. It reaches no transcript, spends
|
|
15
|
+
* no tab, and leaves no message anybody has to read.
|
|
16
|
+
*
|
|
17
|
+
* IT ANSWERS WITH A STRING AND STARTS NOTHING. The server parses what comes
|
|
18
|
+
* back, through the same `parseDevCommand` a human's answer goes through, and
|
|
19
|
+
* hands the machine an ordinary start job on the next poll. That split is the
|
|
20
|
+
* whole safety story: the policy for what may execute has exactly one
|
|
21
|
+
* implementation, and it lives in the component this repo can actually upgrade.
|
|
22
|
+
* A daemon that decided for itself what counted as a legal command would be a
|
|
23
|
+
* second copy of that policy, free to drift, published, and unrecallable.
|
|
24
|
+
*
|
|
25
|
+
* IT MAY INSTALL. That is not a loophole in the install refusal — it is the
|
|
26
|
+
* refusal's own stated remedy. `parseDevCommand` refuses `npm install` because
|
|
27
|
+
* a SPAWNED command runs lifecycle scripts from the repo and every transitive
|
|
28
|
+
* dependency with no agent in the loop; the file says the remedy is "a turn in
|
|
29
|
+
* the tab: a human asking, an agent doing it". This is exactly that turn, with
|
|
30
|
+
* the human asking by pressing the button. And it is the case that matters: a
|
|
31
|
+
* fresh worktree has no `node_modules` (they are gitignored, so they never come
|
|
32
|
+
* across with the branch), which is precisely the dead end that produced "it
|
|
33
|
+
* was stuck on 'working', does it really take that long to run dev?"
|
|
34
|
+
*
|
|
35
|
+
* IT CLEANS UP AFTER ITSELF. A `-p` turn writes a transcript, and
|
|
36
|
+
* `localSessions.mjs` offers the newest ended session per directory as
|
|
37
|
+
* adoptable — left behind, every resolve would drop a phantom untitled session
|
|
38
|
+
* into the `+` menu.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { runTurn } from './claude.mjs';
|
|
42
|
+
import { removeProbeTranscript } from './runtimes.mjs';
|
|
43
|
+
|
|
44
|
+
/** Long, because an install can sit in front of the answer. The SERVER holds
|
|
45
|
+
* the real ceiling (`RESOLVE_TTL_MS`); this is the machine giving up first so
|
|
46
|
+
* a wedged child does not hold a slot until then. */
|
|
47
|
+
export const DEV_RESOLVE_TIMEOUT_MS = 10 * 60_000;
|
|
48
|
+
|
|
49
|
+
/** The sentinel for "I could not tell", so an honest failure is distinguishable
|
|
50
|
+
* from a model padding an answer it does not have. */
|
|
51
|
+
export const NO_COMMAND = 'NONE';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The argv0s the server will accept. Named in the prompt NOT as a security
|
|
55
|
+
* control — the server enforces it either way, and would refuse anything else
|
|
56
|
+
* with the parser's own words — but because a model that knows the shape of an
|
|
57
|
+
* acceptable answer gives one, and a refused proposal costs the asker a whole
|
|
58
|
+
* round trip to learn nothing.
|
|
59
|
+
*/
|
|
60
|
+
const ALLOWED = [
|
|
61
|
+
'npm', 'pnpm', 'yarn', 'bun', 'node', 'deno', 'go', 'python', 'python3',
|
|
62
|
+
'make', 'cargo', 'rails', 'php', 'dotnet',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export function resolvePrompt() {
|
|
66
|
+
return [
|
|
67
|
+
'Work out the ONE command that starts this project’s development server, and reply with only that command.',
|
|
68
|
+
'',
|
|
69
|
+
'How to work it out: read the repo. Check package.json scripts, Makefile, Procfile, docker-compose, pyproject.toml, Cargo.toml, README — whatever this project actually uses. Prefer the script the project itself documents for local development.',
|
|
70
|
+
'',
|
|
71
|
+
'You MAY install dependencies first if they are missing (for example a worktree with no node_modules). Do that before answering.',
|
|
72
|
+
'',
|
|
73
|
+
'Rules for the answer:',
|
|
74
|
+
`- It must begin with one of: ${ALLOWED.join(', ')} — or a ./path to a script in this repo.`,
|
|
75
|
+
'- One line. No shell operators (&&, |, ;, >, $, backticks). If the project needs several steps, name a script in the repo that does them.',
|
|
76
|
+
'- Not an install command. Install as part of your work above if needed; the answer is the command that RUNS the server.',
|
|
77
|
+
'- It must not daemonize or background itself. It should stay in the foreground; something else supervises it.',
|
|
78
|
+
`- If you genuinely cannot tell, reply exactly ${NO_COMMAND}.`,
|
|
79
|
+
'',
|
|
80
|
+
'Reply with the command alone — no explanation, no backticks, no prose.',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Last plausible command line out of whatever the model said.
|
|
86
|
+
*
|
|
87
|
+
* DELIBERATELY FORGIVING, because the cost of being wrong is low and asymmetric:
|
|
88
|
+
* the server parses this and refuses anything outside the policy, naming what
|
|
89
|
+
* was proposed. Being strict here would turn a model that wrapped its answer in
|
|
90
|
+
* backticks into a failure the asker cannot act on, having already paid for the
|
|
91
|
+
* turn.
|
|
92
|
+
*/
|
|
93
|
+
export function pickCommand(text) {
|
|
94
|
+
const lines = String(text ?? '')
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((l) => l.trim())
|
|
97
|
+
// Fence markers and bullet/quote decoration, which are formatting rather
|
|
98
|
+
// than part of anybody's command.
|
|
99
|
+
.filter((l) => l && !/^```/.test(l))
|
|
100
|
+
.map((l) => l.replace(/^[-*>\s]+/, '').replace(/^`+|`+$/g, '').trim())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
if (lines.length === 0) return null;
|
|
103
|
+
// The LAST such line: a model that explains before complying puts the answer
|
|
104
|
+
// at the end, and one that complies exactly has only one line anyway.
|
|
105
|
+
const last = lines[lines.length - 1];
|
|
106
|
+
if (!last || last === NO_COMMAND) return null;
|
|
107
|
+
// A sentence is not a command. Cheap shape check so obvious prose becomes
|
|
108
|
+
// "could not work it out" rather than a refusal quoting a paragraph back.
|
|
109
|
+
if (last.split(/\s+/).length > 8 || /[.!?]$/.test(last)) return null;
|
|
110
|
+
return last;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Run the turn. Resolves `{ command }` or `{ error }` — never throws, because
|
|
115
|
+
* the caller's only job with a failure is to relay it, and an exception at this
|
|
116
|
+
* boundary would strand the row.
|
|
117
|
+
*/
|
|
118
|
+
export async function resolveDevCommandOnMachine({ cwd, model, log, onActivity }) {
|
|
119
|
+
let sessionId = null;
|
|
120
|
+
let timer;
|
|
121
|
+
try {
|
|
122
|
+
const out = await Promise.race([
|
|
123
|
+
runTurn({
|
|
124
|
+
prompt: resolvePrompt(),
|
|
125
|
+
cwd,
|
|
126
|
+
streamJson: true,
|
|
127
|
+
answerFromResult: true,
|
|
128
|
+
model,
|
|
129
|
+
label: 'dev',
|
|
130
|
+
/**
|
|
131
|
+
* THE TURN'S OWN HUMANIZED TAIL, forwarded so a browser can watch it.
|
|
132
|
+
*
|
|
133
|
+
* This is the only feature in the product where a Claude does work
|
|
134
|
+
* nobody can see — no transcript, by design — and the driver's answer
|
|
135
|
+
* to that is the right one: "we could have it stream the output for
|
|
136
|
+
* transparency on the menu." So the same `read …` / `+ npm install …`
|
|
137
|
+
* lines a tab relays are forwarded here. It is the CLI's own stdout
|
|
138
|
+
* humanized, never an inference about it, which is the standing rule
|
|
139
|
+
* for every activity readout in this product.
|
|
140
|
+
*/
|
|
141
|
+
onActivity,
|
|
142
|
+
onInit: (i) => {
|
|
143
|
+
if (i?.sessionId) sessionId = i.sessionId;
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
new Promise((r) => {
|
|
147
|
+
timer = setTimeout(() => r(null), DEV_RESOLVE_TIMEOUT_MS);
|
|
148
|
+
timer.unref?.();
|
|
149
|
+
}),
|
|
150
|
+
]);
|
|
151
|
+
if (out === null) {
|
|
152
|
+
return { error: 'your Claude did not finish working out how to start this project in time.' };
|
|
153
|
+
}
|
|
154
|
+
const command = pickCommand(out);
|
|
155
|
+
if (!command) {
|
|
156
|
+
return { error: 'your Claude could not work out how to start this project.' };
|
|
157
|
+
}
|
|
158
|
+
log?.(`dev: resolved start command — ${command}`);
|
|
159
|
+
return { command };
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return { error: `your Claude could not be run here: ${e?.message ?? 'unknown error'}` };
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
// After the turn, so the delete does not race a child still writing.
|
|
165
|
+
if (sessionId) setTimeout(() => removeProbeTranscript(cwd, sessionId), 750).unref?.();
|
|
166
|
+
}
|
|
167
|
+
}
|
package/bin/lib/runtimes.mjs
CHANGED
|
@@ -1054,7 +1054,18 @@ function transcriptCandidates(cwd, sessionId) {
|
|
|
1054
1054
|
return out;
|
|
1055
1055
|
}
|
|
1056
1056
|
|
|
1057
|
-
|
|
1057
|
+
/**
|
|
1058
|
+
* Delete the transcript a headless `-p` turn left behind.
|
|
1059
|
+
*
|
|
1060
|
+
* EXPORTED because the skills probe is no longer the only thing that runs one:
|
|
1061
|
+
* resolving a project's dev command is a background Claude turn too, and every
|
|
1062
|
+
* such turn has the same footprint. `claude -p` writes
|
|
1063
|
+
* `~/.claude/projects/<munged-cwd>/<id>.jsonl` at startup, and
|
|
1064
|
+
* `localSessions.mjs` offers the newest ENDED session per directory as
|
|
1065
|
+
* ADOPTABLE — so anything we run for our own purposes would put a phantom
|
|
1066
|
+
* untitled session in somebody's `+` menu.
|
|
1067
|
+
*/
|
|
1068
|
+
export function removeProbeTranscript(cwd, sessionId) {
|
|
1058
1069
|
if (!sessionId || !/^[A-Za-z0-9_-]{8,64}$/.test(sessionId)) return;
|
|
1059
1070
|
for (const f of transcriptCandidates(cwd, sessionId)) {
|
|
1060
1071
|
try {
|
package/bin/lib/work.mjs
CHANGED
|
@@ -36,11 +36,13 @@ import {
|
|
|
36
36
|
USER_AGENT,
|
|
37
37
|
REFRESH_BEFORE_SECONDS,
|
|
38
38
|
DAEMON_INSTANCE,
|
|
39
|
+
MODEL,
|
|
39
40
|
} from './config.mjs';
|
|
40
41
|
import { git, gitRaw, splitNul, baseBranchName, isSafePathSegment } from './git.mjs';
|
|
41
42
|
import { listenersIn, listenersSupported } from './listeners.mjs';
|
|
42
43
|
import { openTunnel } from './preview.mjs';
|
|
43
44
|
import { startDevServer, reapOrphanDevRuns, killDevRunEntry } from './devServer.mjs';
|
|
45
|
+
import { resolveDevCommandOnMachine } from './devResolve.mjs';
|
|
44
46
|
import { c, note, ok, warn } from './ui.mjs';
|
|
45
47
|
import { mcpFor, runTurn } from './claude.mjs';
|
|
46
48
|
import {
|
|
@@ -109,6 +111,8 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
109
111
|
const PREVIEW_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/preview-done');
|
|
110
112
|
const DEV_RUN_CLAIM_URL = FLEET_URL.replace(/\/agents\/?$/, '/dev-run-claim');
|
|
111
113
|
const DEV_RUN_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/dev-run-done');
|
|
114
|
+
const DEV_RUN_RESOLVED_URL = FLEET_URL.replace(/\/agents\/?$/, '/dev-run-resolved');
|
|
115
|
+
const DEV_RUN_PROGRESS_URL = FLEET_URL.replace(/\/agents\/?$/, '/dev-run-progress');
|
|
112
116
|
const SESSION_COMMANDS_URL = FLEET_URL.replace(/\/agents\/?$/, '/session-commands');
|
|
113
117
|
const ATTACHMENT_URL = FLEET_URL.replace(/\/agents\/?$/, '/attachment');
|
|
114
118
|
const workAnswering = new Set(); // turn ids currently queued/running here
|
|
@@ -774,6 +778,89 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
774
778
|
}
|
|
775
779
|
};
|
|
776
780
|
|
|
781
|
+
/**
|
|
782
|
+
* The answer to a `resolve`, handed up as a STRING for the server to parse.
|
|
783
|
+
*
|
|
784
|
+
* Never argv: `parseDevCommand` is the single owner of what may execute, and
|
|
785
|
+
* a second implementation of that policy inside the one component a deploy
|
|
786
|
+
* cannot upgrade is exactly the drift this product keeps closing.
|
|
787
|
+
*/
|
|
788
|
+
/**
|
|
789
|
+
* THE RESOLVE TURN'S OUTPUT, streamed while it happens.
|
|
790
|
+
*
|
|
791
|
+
* A headless turn is the one place in this product where a Claude works and
|
|
792
|
+
* nobody can see it — deliberately, since it must not reach the transcript —
|
|
793
|
+
* and the answer to that is transparency rather than a promise: "we could
|
|
794
|
+
* have it stream the output for transparency on the menu."
|
|
795
|
+
*
|
|
796
|
+
* THROTTLED, and the tail is BOUNDED at the machine. This is a per-line hook
|
|
797
|
+
* on a turn that may run for minutes; posting each line would be hundreds of
|
|
798
|
+
* requests, and sending the whole transcript would grow without limit. Same
|
|
799
|
+
* shape as the session activity relay, for the same reasons.
|
|
800
|
+
*/
|
|
801
|
+
const devProgress = new Map(); // sessionId → { lines, at, timer }
|
|
802
|
+
const DEV_PROGRESS_MS = 2_000;
|
|
803
|
+
const DEV_PROGRESS_LINES = 40;
|
|
804
|
+
|
|
805
|
+
const flushDevProgress = async (sessionId) => {
|
|
806
|
+
const st = devProgress.get(sessionId);
|
|
807
|
+
if (!st || !st.lines.length) return;
|
|
808
|
+
st.at = Date.now();
|
|
809
|
+
const logTail = st.lines.join('\n').slice(-4000);
|
|
810
|
+
try {
|
|
811
|
+
await fetch(DEV_RUN_PROGRESS_URL, {
|
|
812
|
+
method: 'POST',
|
|
813
|
+
headers: {
|
|
814
|
+
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
815
|
+
'User-Agent': USER_AGENT,
|
|
816
|
+
'Content-Type': 'application/json',
|
|
817
|
+
},
|
|
818
|
+
signal: AbortSignal.timeout(15_000),
|
|
819
|
+
body: JSON.stringify({ sessionId, instance: DAEMON_INSTANCE, logTail }),
|
|
820
|
+
});
|
|
821
|
+
} catch {
|
|
822
|
+
/* progress is best-effort — the row's own state is the truth */
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const noteDevProgress = (sessionId, label) => {
|
|
827
|
+
if (!label) return;
|
|
828
|
+
const st = devProgress.get(sessionId) ?? { lines: [], at: 0, timer: null };
|
|
829
|
+
st.lines.push(String(label).slice(0, 300));
|
|
830
|
+
if (st.lines.length > DEV_PROGRESS_LINES) st.lines.splice(0, st.lines.length - DEV_PROGRESS_LINES);
|
|
831
|
+
devProgress.set(sessionId, st);
|
|
832
|
+
// Leading-edge post, then a trailing one — so the FIRST line appears at
|
|
833
|
+
// once (an empty panel for two seconds reads as nothing happening) and the
|
|
834
|
+
// last line is never left unsent.
|
|
835
|
+
if (Date.now() - st.at > DEV_PROGRESS_MS) {
|
|
836
|
+
void flushDevProgress(sessionId);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (st.timer) return;
|
|
840
|
+
st.timer = setTimeout(() => {
|
|
841
|
+
st.timer = null;
|
|
842
|
+
void flushDevProgress(sessionId);
|
|
843
|
+
}, DEV_PROGRESS_MS);
|
|
844
|
+
st.timer.unref?.();
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
const postDevResolved = async (body) => {
|
|
848
|
+
try {
|
|
849
|
+
await fetch(DEV_RUN_RESOLVED_URL, {
|
|
850
|
+
method: 'POST',
|
|
851
|
+
headers: {
|
|
852
|
+
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
853
|
+
'User-Agent': USER_AGENT,
|
|
854
|
+
'Content-Type': 'application/json',
|
|
855
|
+
},
|
|
856
|
+
signal: AbortSignal.timeout(30_000),
|
|
857
|
+
body: JSON.stringify({ ...body, instance: DAEMON_INSTANCE }),
|
|
858
|
+
});
|
|
859
|
+
} catch {
|
|
860
|
+
/* the row's own TTL is the backstop */
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
|
|
777
864
|
const stopDevRun = async (sessionId, reason) => {
|
|
778
865
|
const live = liveDevRuns.get(sessionId);
|
|
779
866
|
liveDevRuns.delete(sessionId);
|
|
@@ -798,6 +885,69 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
798
885
|
continue;
|
|
799
886
|
}
|
|
800
887
|
|
|
888
|
+
/**
|
|
889
|
+
* RESOLVE — ask a Claude what starts this project, and say so. Starts
|
|
890
|
+
* NOTHING.
|
|
891
|
+
*
|
|
892
|
+
* It exists because the sheet used to open with a text field prefilled
|
|
893
|
+
* `npm run dev`, which presumes a stack. The turn is headless and reaches
|
|
894
|
+
* no transcript: "i still want claude to start the server for me but i
|
|
895
|
+
* dont want it to literally open a chat. have it do it in the
|
|
896
|
+
* background."
|
|
897
|
+
*
|
|
898
|
+
* IT TAKES THE PLACE LOCK, through the same `chainFor` every turn goes
|
|
899
|
+
* through, and that is deliberate rather than incidental. The turn reads
|
|
900
|
+
* the repo and may `npm install` into this worktree; running it beside a
|
|
901
|
+
* tab turn editing the same directory is the exact collision the chain
|
|
902
|
+
* exists to prevent. The cost is that a resolve makes the tab wait, which
|
|
903
|
+
* is honest — you cannot usefully build while an install is running
|
|
904
|
+
* anyway — and it is bounded, because this turn ENDS. That is the whole
|
|
905
|
+
* reason it answers with a command instead of running one: a foreground
|
|
906
|
+
* `npm run dev` would never return, and the lock would be held for as
|
|
907
|
+
* long as the server lived.
|
|
908
|
+
*/
|
|
909
|
+
if (job?.action === 'resolve') {
|
|
910
|
+
if (liveDevRuns.has(sessionId)) continue;
|
|
911
|
+
if (devRunClaiming.has(sessionId)) continue;
|
|
912
|
+
devRunClaiming.add(sessionId);
|
|
913
|
+
void (async () => {
|
|
914
|
+
try {
|
|
915
|
+
if (!(await claimDevRun(sessionId))) return; // somebody else has it
|
|
916
|
+
const wt = placeDir(sessionId);
|
|
917
|
+
const out = await chainFor(placeOf(sessionId), () =>
|
|
918
|
+
resolveDevCommandOnMachine({
|
|
919
|
+
cwd: wt,
|
|
920
|
+
// The machine's own pin, exactly as a tab turn gets — never
|
|
921
|
+
// the user's global default, which for Claude may be a
|
|
922
|
+
// long-context tier their subscription cannot bill autonomous
|
|
923
|
+
// work on. This turn is autonomous by definition.
|
|
924
|
+
model: MODEL,
|
|
925
|
+
log: (m) => note(`${sessionId.slice(0, 8)}: ${m}`),
|
|
926
|
+
})
|
|
927
|
+
);
|
|
928
|
+
// The last lines, before the row leaves the resolving state.
|
|
929
|
+
await flushDevProgress(sessionId);
|
|
930
|
+
await postDevResolved({
|
|
931
|
+
sessionId,
|
|
932
|
+
command: out?.command ?? null,
|
|
933
|
+
error: out?.error ?? null,
|
|
934
|
+
});
|
|
935
|
+
} catch (e) {
|
|
936
|
+
await postDevResolved({
|
|
937
|
+
sessionId,
|
|
938
|
+
command: null,
|
|
939
|
+
error: `the machine could not run that turn: ${e?.message ?? 'unknown error'}`,
|
|
940
|
+
});
|
|
941
|
+
} finally {
|
|
942
|
+
const st = devProgress.get(sessionId);
|
|
943
|
+
if (st?.timer) clearTimeout(st.timer);
|
|
944
|
+
devProgress.delete(sessionId);
|
|
945
|
+
devRunClaiming.delete(sessionId);
|
|
946
|
+
}
|
|
947
|
+
})();
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
|
|
801
951
|
// RE-VALIDATE THE SHAPE at this boundary. The server parsed the string
|
|
802
952
|
// and owns the policy; the machine owns the refusal to execute something
|
|
803
953
|
// malformed, because one place doing a check is one deploy away from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
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": {
|