flowviant 0.115.0 → 0.117.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 +31 -12
- package/bin/cli.mjs +13 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +56 -24
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +4 -7
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactReporter.mjs +16 -8
- package/bin/lib/artifacts.mjs +64 -60
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +119 -42
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +7 -11
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +180 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +88 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +3 -5
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +7 -9
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +130 -170
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/readGuard.mjs +41 -7
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +101 -16
- package/bin/lib/projectTools.mjs +50 -32
- package/bin/lib/prompts.mjs +41 -19
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/runTurn.mjs +268 -210
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +140 -20
- package/bin/lib/runtimeClaude.mjs +55 -5
- package/bin/lib/runtimeCodex.mjs +245 -205
- package/bin/lib/runtimeDetection.mjs +2 -2
- package/bin/lib/runtimeEvents.mjs +16 -10
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +24 -11
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +1 -1
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +17 -7
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +99 -145
- package/bin/lib/workAgentPrecheck.mjs +71 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +46 -53
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +4 -3
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +109 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +11 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +96 -30
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +111 -189
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workTurnShape.mjs +163 -0
- package/npm-shrinkwrap.json +59 -0
- package/package.json +3 -2
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
package/bin/lib/workShip.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/** Session branch shipping and settlement. */
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { git, baseBranchName, isSafePathSegment } from './git.mjs';
|
|
4
|
+
import { git, baseBranchName, isSafePathSegment, sessionBranch, stripUrlUserinfo } from './git.mjs';
|
|
5
5
|
import { mergeOutward as shipMergeOutward } from './shipMerge.mjs';
|
|
6
6
|
import { gitMergeIn } from './gitMergeIdentity.mjs';
|
|
7
7
|
import { c, note, ok, warn } from './ui.mjs';
|
|
8
8
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
9
9
|
import { REPO_PLACE } from './workPlaces.mjs';
|
|
10
10
|
import { turnLockedByLivePid } from './turnLock.mjs';
|
|
11
|
+
import { printable } from './printable.mjs';
|
|
11
12
|
|
|
12
13
|
export function createWorkShipper({
|
|
13
14
|
inPlace,
|
|
@@ -17,6 +18,8 @@ export function createWorkShipper({
|
|
|
17
18
|
baseDir,
|
|
18
19
|
gitNet,
|
|
19
20
|
baseRef,
|
|
21
|
+
placeDir,
|
|
22
|
+
branchAt,
|
|
20
23
|
placeWtFor,
|
|
21
24
|
sessionMetaPath,
|
|
22
25
|
shipping,
|
|
@@ -55,6 +58,11 @@ export function createWorkShipper({
|
|
|
55
58
|
// delivers it; re-running the ship would misread its own success.
|
|
56
59
|
if (pendingShipReports.has(job.sessionId)) continue;
|
|
57
60
|
shipping.add(job.sessionId);
|
|
61
|
+
// A TAB'S NAME, which its person wrote, printed on this console and put
|
|
62
|
+
// in the merge commit's subject — scrubbed here, where the job is read,
|
|
63
|
+
// so no line below can print an escape sequence (2026-10-10, the audit's
|
|
64
|
+
// F097; see printable.mjs). The id is checked as a path segment below.
|
|
65
|
+
const sessionName = printable(job.sessionName);
|
|
58
66
|
// A WRITER, keyed by PLACE: ship folds and merges with git in this
|
|
59
67
|
// directory, and no CLI running here can coordinate with that. Keyed by
|
|
60
68
|
// place rather than by session id, which is what this used to do while
|
|
@@ -74,7 +82,7 @@ export function createWorkShipper({
|
|
|
74
82
|
await done({ ok: false, error: 'invalid session id' });
|
|
75
83
|
return;
|
|
76
84
|
}
|
|
77
|
-
note(`${c.cyan('ship')} ${c.dim(`— "${
|
|
85
|
+
note(`${c.cyan('ship')} ${c.dim(`— "${sessionName || job.sessionId}"`)}`);
|
|
78
86
|
/**
|
|
79
87
|
* WHAT IS ACTUALLY CHECKED OUT — not what we named it at birth.
|
|
80
88
|
*
|
|
@@ -99,20 +107,20 @@ export function createWorkShipper({
|
|
|
99
107
|
*
|
|
100
108
|
* A directory that is gone (a retired or closed tab) cannot be asked,
|
|
101
109
|
* so the recorded name is the fallback — the one case where the name
|
|
102
|
-
* is the only thing there is.
|
|
110
|
+
* is the only thing there is. The recorded name is the branch the
|
|
111
|
+
* PLACE was cut on, `session/<place>`: a person's `u-` place lives on
|
|
112
|
+
* `session/u-<id>`, and falling back to `session/<this tab's id>`
|
|
113
|
+
* answered "nothing to ship" over its committed work (audit
|
|
114
|
+
* 2026-10-10, F168). The checkout always exists, so its fallback is
|
|
115
|
+
* never read and keeps the tab's own name.
|
|
103
116
|
*/
|
|
104
117
|
// The session's PLACE — the directory it actually works in.
|
|
105
118
|
const shipPlace = placeOf(job.sessionId);
|
|
106
|
-
const wt =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
branch = git(['symbolic-ref', '--short', 'HEAD'], wt);
|
|
112
|
-
} catch {
|
|
113
|
-
detached = true;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
119
|
+
const wt = placeDir(job.sessionId);
|
|
120
|
+
const { branch, detached } = branchAt(
|
|
121
|
+
wt,
|
|
122
|
+
sessionBranch(shipPlace === REPO_PLACE ? job.sessionId : shipPlace)
|
|
123
|
+
);
|
|
116
124
|
// THE ONE REFUSAL LEFT, and it is not policy. A detached HEAD names
|
|
117
125
|
// no branch, so there is nothing to merge and nothing to record —
|
|
118
126
|
// that is an ambiguity in git, not a rule of ours.
|
|
@@ -177,13 +185,19 @@ export function createWorkShipper({
|
|
|
177
185
|
tip,
|
|
178
186
|
count,
|
|
179
187
|
branch,
|
|
180
|
-
label:
|
|
188
|
+
label: sessionName || job.sessionId.slice(0, 8),
|
|
181
189
|
git,
|
|
182
190
|
gitMerge,
|
|
183
191
|
repoRoot,
|
|
184
192
|
tmpDir: join(baseDir, 'ship', job.sessionId),
|
|
185
193
|
baseRef,
|
|
186
|
-
|
|
194
|
+
// A QUERY, never `placeWtFor`: that one re-cuts a missing
|
|
195
|
+
// worktree, and on the ENDED path the directory is retired on
|
|
196
|
+
// purpose — the branch is the session now. Re-attaching it here
|
|
197
|
+
// resurrected a closed tab's directory with its branch checked
|
|
198
|
+
// out, which then blocked the merged-branch sweep (audit
|
|
199
|
+
// 2026-10-10, F160). The live path below opened it already.
|
|
200
|
+
workingTree: existsSync(wt) ? wt : null,
|
|
187
201
|
warn,
|
|
188
202
|
});
|
|
189
203
|
// Idempotency: base already contains the branch tip. A re-offered
|
|
@@ -245,10 +259,14 @@ export function createWorkShipper({
|
|
|
245
259
|
'conflicts with main — the tab is closed, so open a new session from this branch to resolve them, then ship again',
|
|
246
260
|
});
|
|
247
261
|
} else {
|
|
262
|
+
// Userinfo stripped as well as scrubbed (2026-10-10, F153): the
|
|
263
|
+
// outward merge pushes to `origin`, and a push names the remote.
|
|
248
264
|
const line = envScrub(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
265
|
+
stripUrlUserinfo(
|
|
266
|
+
String(detail)
|
|
267
|
+
.split('\n')
|
|
268
|
+
.find((l) => l.trim()) ?? 'git merge failed'
|
|
269
|
+
)
|
|
252
270
|
);
|
|
253
271
|
await done({ ok: false, error: `the merge failed: ${line.slice(0, 300)}` });
|
|
254
272
|
}
|
|
@@ -273,7 +291,7 @@ export function createWorkShipper({
|
|
|
273
291
|
// path, and the job re-offers next poll.
|
|
274
292
|
if (turnLockedByLivePid(sessionMetaPath(dir.wt, 'flowviant-turn.lock'))) {
|
|
275
293
|
warn(
|
|
276
|
-
`a turn is still running in "${
|
|
294
|
+
`a turn is still running in "${sessionName || job.sessionId}" — ship waits for it`
|
|
277
295
|
);
|
|
278
296
|
deferred = true;
|
|
279
297
|
return;
|
|
@@ -313,9 +331,11 @@ export function createWorkShipper({
|
|
|
313
331
|
// An honest error beats a fabricated conflict — the human can
|
|
314
332
|
// only fix what they are told about.
|
|
315
333
|
const line = envScrub(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
334
|
+
stripUrlUserinfo(
|
|
335
|
+
String(detail)
|
|
336
|
+
.split('\n')
|
|
337
|
+
.find((l) => l.trim()) ?? 'git merge failed'
|
|
338
|
+
)
|
|
319
339
|
);
|
|
320
340
|
await done({ ok: false, error: `the merge failed: ${line.slice(0, 300)}` });
|
|
321
341
|
}
|
|
@@ -347,7 +367,7 @@ export function createWorkShipper({
|
|
|
347
367
|
warn(`ship failed: ${e?.message ?? e}`);
|
|
348
368
|
await done({
|
|
349
369
|
ok: false,
|
|
350
|
-
error: envScrub(
|
|
370
|
+
error: envScrub(stripUrlUserinfo(e?.message ?? 'the merge failed')).slice(0, 500),
|
|
351
371
|
});
|
|
352
372
|
} finally {
|
|
353
373
|
if (!settled && !deferred) {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT A TERMINAL TAB'S TURN IS — plain, plan, capture or build — decided
|
|
3
|
+
* once, as data (audit 2026-10-10, F159). Pure: no I/O, no state.
|
|
4
|
+
*
|
|
5
|
+
* What a turn mounts and says is ONE decision over the runtime, the job's
|
|
6
|
+
* plan and capture switches and whether it adopts, and it was spelled as six
|
|
7
|
+
* parallel ternaries through the lane's lifecycle — the MCP mount, the
|
|
8
|
+
* kickoff, the profile, the system prompt, its artifacts paragraph, the
|
|
9
|
+
* display — plus runtime ids hard-coded where the RUNTIMES row should answer. Missing one of them gave an
|
|
10
|
+
* incoherent turn (a mounted MCP under a plain prompt, a plan profile beside
|
|
11
|
+
* the build contract), and nothing could show it short of a run. So the lane
|
|
12
|
+
* (workSessionTurns.mjs) asks `sessionTurnShape` once, the moment its runtime
|
|
13
|
+
* resolves, settles its refusal in words if it has one, and reads every
|
|
14
|
+
* other answer off it; the prompt pair is composed here too.
|
|
15
|
+
*
|
|
16
|
+
* The runtime facts come off the row (runtime{Claude,Codex,Antigravity}.mjs):
|
|
17
|
+
* `tab` ('plain' runs no MCP), `planTurns` (the CLI can spell plan mode) and
|
|
18
|
+
* `adopt` ('fork' | 'move' | null). The LIFECYCLE stays the lane's — its
|
|
19
|
+
* refusal order is the contract — and only this composition moved.
|
|
20
|
+
*/
|
|
21
|
+
import { runtimeById } from './runtimes.mjs';
|
|
22
|
+
import {
|
|
23
|
+
SYSTEM_WORK,
|
|
24
|
+
SYSTEM_CAPTURE,
|
|
25
|
+
SYSTEM_WORK_PLAIN,
|
|
26
|
+
WORK_TURN_KICKOFF,
|
|
27
|
+
CAPTURE_TURN_KICKOFF,
|
|
28
|
+
WORK_TURN_KICKOFF_PLAIN,
|
|
29
|
+
withProjectContext,
|
|
30
|
+
} from './prompts.mjs';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* THE ONE SENTENCE A PLAN TURN'S SYSTEM PROMPT GAINS (0.97.0), composed here
|
|
34
|
+
* rather than in prompts.mjs because it is a MODIFIER on whichever contract
|
|
35
|
+
* the tab runs, not a contract of its own.
|
|
36
|
+
*
|
|
37
|
+
* The enforcement is not this sentence — it is `--permission-mode plan`, under
|
|
38
|
+
* which the CLI itself refuses every write (claudePosture.mjs, PLAN_MODE_PERM). What
|
|
39
|
+
* the sentence buys is the ANSWER: the plain contract above it says "edit
|
|
40
|
+
* freely, commit", and a turn told only that would spend itself discovering
|
|
41
|
+
* refusals. It also says not to reach for `ExitPlanMode` (measured: disabled
|
|
42
|
+
* under `-p`, and the probe turn then asked the person to leave plan mode
|
|
43
|
+
* themselves) — leaving plan mode is the person's switch in the tab.
|
|
44
|
+
*/
|
|
45
|
+
export const PLAN_TURN_SENTENCE =
|
|
46
|
+
'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.';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The turn's shape. `runtime` is the resolved RUNTIMES id; `job` is the
|
|
50
|
+
* roster's job, whose `planMode` and `capture` switches count only when
|
|
51
|
+
* exactly `true` (the job is handed whole rather than its switches spelled
|
|
52
|
+
* as keys: `planMode` is also a retired runTurn switch, and a lane that
|
|
53
|
+
* spells it is caught by turnProfile's ban); `adopting` is the lane's
|
|
54
|
+
* verdict that this first turn adopts a terminal session.
|
|
55
|
+
*
|
|
56
|
+
* Returns `{ refusal?, plain, plan, capture, kickoff, systemBase,
|
|
57
|
+
* planSentence, artifacts, profile, mountMcp, display }`:
|
|
58
|
+
* refusal — the sentence the turn settles in, unrun, when it has one;
|
|
59
|
+
* kickoff /
|
|
60
|
+
* systemBase — which contract the prompt pair runs: 'plain' | 'capture' | 'work';
|
|
61
|
+
* planSentence — PLAN_TURN_SENTENCE is appended to the system prompt;
|
|
62
|
+
* artifacts — this turn's contract may carry the artifacts paragraph
|
|
63
|
+
* (`sessionTurnSystem` adds it only while the server takes
|
|
64
|
+
* one, as the lane reads it at spawn);
|
|
65
|
+
* profile — the permission profile: 'build' | 'plan' | 'plan-mode';
|
|
66
|
+
* mountMcp — the session's MCP config is mounted;
|
|
67
|
+
* display — the CLI keeps its environment's display.
|
|
68
|
+
*/
|
|
69
|
+
export function sessionTurnShape({ runtime, adopting = false, job = null }) {
|
|
70
|
+
const row = runtimeById(runtime);
|
|
71
|
+
// A PLAIN tab (agy) mounts no MCP: no credential to mint, no config to
|
|
72
|
+
// write. The trade is stated in SYSTEM_WORK_PLAIN — no cards, no streaming
|
|
73
|
+
// — and the honesty survives on the existing rails: the answer lands via
|
|
74
|
+
// work-turn-done, the rail says "no card yet", and ship-time reconciliation
|
|
75
|
+
// books every branch commit.
|
|
76
|
+
const plain = row?.tab === 'plain';
|
|
77
|
+
// A CAPTURE chat (the board's New task conversation): its own system
|
|
78
|
+
// prompt, its own kickoff, and the scratch planner's READ-ONLY permission
|
|
79
|
+
// profile — the prompt says stage-never-file and the profile is what makes
|
|
80
|
+
// "read-only" true rather than asserted. Server-flagged per job; a server
|
|
81
|
+
// too old to flag it simply runs an ordinary tab, which the web's version
|
|
82
|
+
// floor prevents ever being offered.
|
|
83
|
+
const isCapture = job?.capture === true;
|
|
84
|
+
/**
|
|
85
|
+
* A PLAN TURN (0.97.0): the tab's switch, off the job. Only on a CLI whose
|
|
86
|
+
* row can spell it (`planTurns` — Claude Code's `--permission-mode plan`); a
|
|
87
|
+
* codex or agy turn is refused in words rather than run as a build turn
|
|
88
|
+
* wearing the word. The server never sends the key for a non-Claude tab;
|
|
89
|
+
* this is the machine's own half. Never on a capture chat, which is
|
|
90
|
+
* read-only by its own profile already.
|
|
91
|
+
*
|
|
92
|
+
* A PLAN TURN RUNS PLAIN TOO: measured on 2.1.281, plan mode refuses every
|
|
93
|
+
* `mcp__flowviant` call ("Cannot call … while in plan mode") unless the
|
|
94
|
+
* tool is annotated read-only, and none of the session tools are — so
|
|
95
|
+
* mounting the control plane would hand the turn a list of tools it can
|
|
96
|
+
* only fail at. The lane still mints the credential: its lease answers
|
|
97
|
+
* (held elsewhere, gone) are about the SESSION, and a plan turn needs them
|
|
98
|
+
* as much as any other.
|
|
99
|
+
*/
|
|
100
|
+
const plan = job?.planMode === true && !isCapture;
|
|
101
|
+
// An adopt id names a conversation in ITS OWN CLI's store: claude forks it
|
|
102
|
+
// (--resume --fork-session), agy moves it (--conversation). A CLI whose row
|
|
103
|
+
// declares no adoption (Codex: no adoptable store yet) is refused here —
|
|
104
|
+
// its args builder backstops this with a loud throw, but a sentence beats
|
|
105
|
+
// a stack. Checked BEFORE plan mode: the order the lane always settled in.
|
|
106
|
+
const refusal =
|
|
107
|
+
adopting && !row?.adopt
|
|
108
|
+
? 'adopting this terminal session needs its own CLI on the machine — install it, then try again'
|
|
109
|
+
: plan && !row?.planTurns
|
|
110
|
+
? `plan mode runs on Claude Code only, and this tab is ${runtime} — turn plan off in the tab, then send the message again`
|
|
111
|
+
: null;
|
|
112
|
+
// ONE CONTRACT for the prompt pair: a plain tab and a plan turn run the
|
|
113
|
+
// plain one (no Flowviant tools to name, one complete report instead of a
|
|
114
|
+
// stream), a capture chat its own, every other tab the work contract.
|
|
115
|
+
const contract = plain || plan ? 'plain' : isCapture ? 'capture' : 'work';
|
|
116
|
+
return {
|
|
117
|
+
...(refusal ? { refusal } : {}),
|
|
118
|
+
plain,
|
|
119
|
+
plan,
|
|
120
|
+
capture: isCapture,
|
|
121
|
+
kickoff: contract,
|
|
122
|
+
systemBase: contract,
|
|
123
|
+
planSentence: plan,
|
|
124
|
+
// ARTIFACTS (0.94.0): every tab but the read-only capture chat — and not
|
|
125
|
+
// a plan turn, which cannot write one.
|
|
126
|
+
artifacts: !isCapture && !plan,
|
|
127
|
+
// ONE PROFILE (turnProfile.mjs): the capture chat's read-only `plan`
|
|
128
|
+
// list; or a plan turn's `plan-mode` — `--permission-mode plan` INSTEAD
|
|
129
|
+
// of the build posture, never beside `--dangerously-skip-permissions`,
|
|
130
|
+
// which silently wins (claudePosture.mjs, PLAN_MODE_PERM); else the build
|
|
131
|
+
// posture.
|
|
132
|
+
profile: isCapture ? 'plan' : plan ? 'plan-mode' : 'build',
|
|
133
|
+
mountMcp: !plain && !plan,
|
|
134
|
+
// A PERSON AT THE KEYBOARD MAY ASK FOR A WINDOW (2026-09-29): a Terminal
|
|
135
|
+
// tab keeps its environment's display. The capture chat does not — it
|
|
136
|
+
// only reads and stages cards, and the person talking to it may be on a
|
|
137
|
+
// phone (noWindowEnv.mjs).
|
|
138
|
+
display: !isCapture,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const KICKOFFS = { plain: WORK_TURN_KICKOFF_PLAIN, capture: CAPTURE_TURN_KICKOFF, work: WORK_TURN_KICKOFF };
|
|
143
|
+
const SYSTEMS = { plain: SYSTEM_WORK_PLAIN, capture: SYSTEM_CAPTURE, work: SYSTEM_WORK };
|
|
144
|
+
|
|
145
|
+
/** The turn's opening message under its contract. The plain kickoff names no
|
|
146
|
+
* session id (WORK_TURN_KICKOFF_PLAIN takes none) — it has no tools to pass
|
|
147
|
+
* one to. */
|
|
148
|
+
export const sessionTurnKickoff = (shape, { sessionId, sessionName, message, askedByName }) =>
|
|
149
|
+
KICKOFFS[shape.kickoff]({ sessionId, sessionName, message, askedByName });
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The turn's system prompt: its contract with the project's knowledge
|
|
153
|
+
* paragraph, the artifacts paragraph only where the shape allows one AND the
|
|
154
|
+
* server can show one (`artifactsAccepted`, read by the lane at spawn), and a
|
|
155
|
+
* plan turn's sentence appended.
|
|
156
|
+
*/
|
|
157
|
+
export const sessionTurnSystem = (shape, { knowledgeDir, artifactsAccepted }) => {
|
|
158
|
+
const contract = withProjectContext(SYSTEMS[shape.systemBase], {
|
|
159
|
+
knowledgeDir,
|
|
160
|
+
artifacts: shape.artifacts && artifactsAccepted,
|
|
161
|
+
});
|
|
162
|
+
return shape.planSentence ? `${contract}\n\n${PLAN_TURN_SENTENCE}` : contract;
|
|
163
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flowviant",
|
|
3
|
+
"version": "0.117.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "flowviant",
|
|
9
|
+
"version": "0.117.0",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"libsodium-wrappers-sumo": "^0.8.4",
|
|
13
|
+
"ws": "^8.18.0"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"flowviant": "bin/cli.mjs"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"node_modules/libsodium-sumo": {
|
|
23
|
+
"version": "0.8.4",
|
|
24
|
+
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.4.tgz",
|
|
25
|
+
"integrity": "sha512-TMtHShQfVVsaxDygyapvUC3o7YsPgXa/hRWeIgzyFz6w5k/1hirGptCxp1U7XwW3rCskaTTYKgV10v86UiGgNw==",
|
|
26
|
+
"license": "ISC"
|
|
27
|
+
},
|
|
28
|
+
"node_modules/libsodium-wrappers-sumo": {
|
|
29
|
+
"version": "0.8.4",
|
|
30
|
+
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.4.tgz",
|
|
31
|
+
"integrity": "sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==",
|
|
32
|
+
"license": "ISC",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"libsodium-sumo": "^0.8.0"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"node_modules/ws": {
|
|
38
|
+
"version": "8.21.1",
|
|
39
|
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
|
40
|
+
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=10.0.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"bufferutil": "^4.0.1",
|
|
47
|
+
"utf-8-validate": ">=5.0.2"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"bufferutil": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"utf-8-validate": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.117.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": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"!bin/lib/*.test.mjs",
|
|
12
12
|
"README.md",
|
|
13
|
-
"LICENSE"
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"npm-shrinkwrap.json"
|
|
14
15
|
],
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=20"
|
package/bin/lib/fleetJobs.mjs
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TWO ROSTER JOB LANES THAT ACT ON THIS CHECKOUT AND SETTLE BACK: patch
|
|
3
|
-
* reverts and task-restart cleanups.
|
|
4
|
-
*
|
|
5
|
-
* Split out of fleet.mjs (SOLID audit 2026-09-26, F038). Each lane dedups its
|
|
6
|
-
* in-flight job ids, does its work fire-and-forget, and ALWAYS reports the job
|
|
7
|
-
* done — the roster re-serves a job until it is reported, so a lane that could
|
|
8
|
-
* wedge on a failure would redo it every poll. The loop in fleet.mjs only
|
|
9
|
-
* hands each lane its roster array.
|
|
10
|
-
*
|
|
11
|
-
* Factories rather than module state because each lane is bound to the
|
|
12
|
-
* checkout this daemon serves (and cleanup to the live base ref, read at call
|
|
13
|
-
* time — the roster can move it mid-run).
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { execFileSync } from 'node:child_process';
|
|
17
|
-
import { FLEET_URL } from './config.mjs';
|
|
18
|
-
import { gitNetAsync, originSlug, isValidPrUrl, isValidBranch } from './git.mjs';
|
|
19
|
-
import { c, note, ok, warn } from './ui.mjs';
|
|
20
|
-
import { revertPatch, withPatchLock } from './patchRevert.mjs';
|
|
21
|
-
import { fleetEndpoint } from './fleetWire.mjs';
|
|
22
|
-
import { postToFleet } from './fleetPost.mjs';
|
|
23
|
-
|
|
24
|
-
export function createPatchRevertLane({ repoRoot }) {
|
|
25
|
-
// Patch reverts: a patch landed straight in this checkout, and a human took it
|
|
26
|
-
// back. The commits are HERE, not on the server, so the reverse-apply happens
|
|
27
|
-
// here too — a revert, never a reset, because the owner has almost certainly
|
|
28
|
-
// worked on top by now. Serialised through the same lock as applies.
|
|
29
|
-
const PATCH_REVERT_DONE_URL = fleetEndpoint('patch-revert-done', FLEET_URL);
|
|
30
|
-
const reverting = new Set();
|
|
31
|
-
const processPatchRevertJobs = (jobs) => {
|
|
32
|
-
for (const job of jobs ?? []) {
|
|
33
|
-
if (!job || typeof job.id !== 'string' || !Array.isArray(job.shas)) continue;
|
|
34
|
-
if (reverting.has(job.id)) continue;
|
|
35
|
-
reverting.add(job.id);
|
|
36
|
-
(async () => {
|
|
37
|
-
try {
|
|
38
|
-
note(`${c.cyan('revert')} ${c.dim(`— ${job.title}`)}`);
|
|
39
|
-
const res = await withPatchLock(() =>
|
|
40
|
-
Promise.resolve(revertPatch({ repoRoot, shas: job.shas }))
|
|
41
|
-
);
|
|
42
|
-
if (res.ok) ok(`${c.dim('reverted')} ${job.title}`);
|
|
43
|
-
else warn(`revert failed for "${job.title}": ${res.error}`);
|
|
44
|
-
// ALWAYS report, success or not. Without this the flag stays set, the
|
|
45
|
-
// roster re-serves the job every poll, and each pass reverts the
|
|
46
|
-
// revert — the change flapping in and out of the owner's tree forever.
|
|
47
|
-
await postToFleet(PATCH_REVERT_DONE_URL, {
|
|
48
|
-
taskId: job.id,
|
|
49
|
-
ok: res.ok,
|
|
50
|
-
error: res.ok ? undefined : String(res.error ?? 'revert failed'),
|
|
51
|
-
});
|
|
52
|
-
} finally {
|
|
53
|
-
reverting.delete(job.id);
|
|
54
|
-
}
|
|
55
|
-
})();
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
return processPatchRevertJobs;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function createCleanupLane({ repoRoot, getBaseRef }) {
|
|
62
|
-
// Cleanup jobs (task restarts): close the abandoned PR + delete its remote
|
|
63
|
-
// branch on the user's own gh, so a restart doesn't litter the repo.
|
|
64
|
-
const CLEANUP_DONE_URL = fleetEndpoint('cleanup-done', FLEET_URL);
|
|
65
|
-
const cleaning = new Set();
|
|
66
|
-
const processCleanupJobs = (jobs) => {
|
|
67
|
-
for (const job of jobs ?? []) {
|
|
68
|
-
if (!job || typeof job.id !== 'string') continue; // a null element would wedge the loop
|
|
69
|
-
if (cleaning.has(job.id)) continue;
|
|
70
|
-
cleaning.add(job.id);
|
|
71
|
-
(async () => {
|
|
72
|
-
try {
|
|
73
|
-
note(`${c.cyan('cleanup')} ${c.dim(`— ${job.title} (restarted)`)}`);
|
|
74
|
-
// Same guards as merge: only close a PR in THIS repo, only delete a
|
|
75
|
-
// well-formed non-base branch. A bad server must not close a stranger's
|
|
76
|
-
// PR or delete `main` (`--delete` with `main`) via a cleanup job.
|
|
77
|
-
if (job.prUrl && isValidPrUrl(job.prUrl, originSlug(repoRoot))) {
|
|
78
|
-
try {
|
|
79
|
-
execFileSync(
|
|
80
|
-
'gh',
|
|
81
|
-
[
|
|
82
|
-
'pr',
|
|
83
|
-
'close',
|
|
84
|
-
job.prUrl,
|
|
85
|
-
'--comment',
|
|
86
|
-
'Task restarted in Flowviant — this attempt was discarded.',
|
|
87
|
-
'--delete-branch',
|
|
88
|
-
],
|
|
89
|
-
{ cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000 }
|
|
90
|
-
);
|
|
91
|
-
} catch (e) {
|
|
92
|
-
// Already closed/merged/missing = fine; anything else we still
|
|
93
|
-
// report done — a restart must never wedge on stale remotes.
|
|
94
|
-
const err = e.stderr?.toString?.() || e.message || '';
|
|
95
|
-
warn(`cleanup for "${job.title}": ${err.split('\n')[0] || 'gh pr close failed'}`);
|
|
96
|
-
}
|
|
97
|
-
} else if (job.branch && isValidBranch(job.branch, repoRoot, getBaseRef())) {
|
|
98
|
-
try {
|
|
99
|
-
// Explicit refspec form so a leading '-' can't be a git flag.
|
|
100
|
-
// NETWORK call, so timed and non-interactive (2026-09-24, the
|
|
101
|
-
// audit): a bare execFileSync here had no timeout and could
|
|
102
|
-
// prompt on /dev/tty, freezing every poll and lease on the
|
|
103
|
-
// machine behind this cleanup's own async loop.
|
|
104
|
-
await gitNetAsync(['push', 'origin', `:refs/heads/${job.branch}`], repoRoot);
|
|
105
|
-
} catch {
|
|
106
|
-
/* branch already gone — fine */
|
|
107
|
-
}
|
|
108
|
-
} else if (job.prUrl || job.branch) {
|
|
109
|
-
warn(`cleanup REFUSED for "${job.title}": untrusted PR/branch value`);
|
|
110
|
-
}
|
|
111
|
-
await postToFleet(CLEANUP_DONE_URL, { taskId: job.id });
|
|
112
|
-
ok(`${c.cyan('cleaned')} ${c.dim(`— ${job.title}`)}`);
|
|
113
|
-
} finally {
|
|
114
|
-
cleaning.delete(job.id);
|
|
115
|
-
}
|
|
116
|
-
})();
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
return processCleanupJobs;
|
|
120
|
-
}
|
package/bin/lib/patchRevert.mjs
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TAKING A LANDED PATCH BACK — the one live half of what used to be
|
|
3
|
-
* `patch.mjs`.
|
|
4
|
-
*
|
|
5
|
-
* `patch.mjs` held two lifecycles: patch PLACEMENT (cherry-picking a
|
|
6
|
-
* teammate's agent commit into the owner's checkout after a dirty-path
|
|
7
|
-
* collision check, plus a commit-history reader for the retired
|
|
8
|
-
* `report_commits` path) and patch REVERT. Nothing in the daemon has called
|
|
9
|
-
* placement or the history reader since the dispatch lane was deleted — the
|
|
10
|
-
* roster still carries `patchRevertJobs` for a patch that landed before then,
|
|
11
|
-
* and fleetJobs.mjs is the only caller of what is left. So the revert moved here
|
|
12
|
-
* under its own name and the uncalled half was deleted rather than kept as a
|
|
13
|
-
* second, stale commit-record parser nobody tests (SOLID audit 2026-09-26,
|
|
14
|
-
* F167). Git history has the placement code if a new argument ever wants it.
|
|
15
|
-
*
|
|
16
|
-
* One revert at a time per daemon: `withPatchLock` serialises every write this
|
|
17
|
-
* lane makes to the owner's tree, so two jobs cannot race in one checkout.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { git, isValidSha } from './git.mjs';
|
|
21
|
-
|
|
22
|
-
/** Serialises reverts within this process. */
|
|
23
|
-
let patchChain = Promise.resolve();
|
|
24
|
-
|
|
25
|
-
export function withPatchLock(fn) {
|
|
26
|
-
const run = patchChain.then(fn, fn);
|
|
27
|
-
// Keep the chain alive regardless of outcome, but don't swallow the result.
|
|
28
|
-
patchChain = run.then(
|
|
29
|
-
() => {},
|
|
30
|
-
() => {}
|
|
31
|
-
);
|
|
32
|
-
return run;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Undo a landed patch. `git revert` rather than `reset` on purpose: the owner
|
|
37
|
-
* has almost certainly committed or edited on top by now, and rewriting their
|
|
38
|
-
* history to take something back would be far worse than the patch was.
|
|
39
|
-
*/
|
|
40
|
-
export function revertPatch({ repoRoot, shas }) {
|
|
41
|
-
// These arrive over the roster and go straight into git argv. Anything that
|
|
42
|
-
// isn't a bare object id — a revision range, a leading-dash option — is
|
|
43
|
-
// refused here rather than trusted because the server said so.
|
|
44
|
-
const clean = (shas ?? []).filter(isValidSha);
|
|
45
|
-
if (clean.length === 0 || clean.length !== (shas ?? []).length) {
|
|
46
|
-
return { ok: false, error: 'refused: patch revert carried a non-sha value' };
|
|
47
|
-
}
|
|
48
|
-
const ordered = [...clean].reverse(); // newest first
|
|
49
|
-
try {
|
|
50
|
-
for (const sha of ordered) git(['revert', '--no-edit', sha], repoRoot);
|
|
51
|
-
return { ok: true };
|
|
52
|
-
} catch (e) {
|
|
53
|
-
try {
|
|
54
|
-
git(['revert', '--abort'], repoRoot);
|
|
55
|
-
} catch {
|
|
56
|
-
/* nothing in progress */
|
|
57
|
-
}
|
|
58
|
-
return { ok: false, error: e?.message ?? String(e) };
|
|
59
|
-
}
|
|
60
|
-
}
|