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.
Files changed (135) hide show
  1. package/README.md +31 -12
  2. package/bin/cli.mjs +13 -17
  3. package/bin/lib/admission.mjs +7 -14
  4. package/bin/lib/agentInternalWait.mjs +83 -0
  5. package/bin/lib/agentPlan.mjs +56 -24
  6. package/bin/lib/agentPublish.mjs +12 -13
  7. package/bin/lib/agentTaskKinds.mjs +4 -7
  8. package/bin/lib/agySessions.mjs +3 -3
  9. package/bin/lib/artifactPreview.mjs +92 -18
  10. package/bin/lib/artifactReporter.mjs +16 -8
  11. package/bin/lib/artifacts.mjs +64 -60
  12. package/bin/lib/authproxy.mjs +2 -1
  13. package/bin/lib/childEnv.mjs +9 -4
  14. package/bin/lib/claude.mjs +10 -20
  15. package/bin/lib/claudePersonal.mjs +71 -6
  16. package/bin/lib/claudePosture.mjs +119 -42
  17. package/bin/lib/claudeSessions.mjs +7 -7
  18. package/bin/lib/claudeStream.mjs +7 -11
  19. package/bin/lib/cliLimit.mjs +2 -2
  20. package/bin/lib/codexHome.mjs +17 -0
  21. package/bin/lib/codexPersonal.mjs +44 -3
  22. package/bin/lib/codexRelay.mjs +21 -5
  23. package/bin/lib/codexResearch.mjs +180 -0
  24. package/bin/lib/config.mjs +37 -24
  25. package/bin/lib/conversationIds.mjs +51 -0
  26. package/bin/lib/credentials.mjs +2 -7
  27. package/bin/lib/daemonLocks.mjs +88 -0
  28. package/bin/lib/daemonState.mjs +5 -5
  29. package/bin/lib/deploy.mjs +6 -9
  30. package/bin/lib/deployCheckout.mjs +26 -22
  31. package/bin/lib/deployRunner.mjs +54 -18
  32. package/bin/lib/deployWire.mjs +32 -23
  33. package/bin/lib/desktopContract.mjs +3 -5
  34. package/bin/lib/env.mjs +104 -35
  35. package/bin/lib/fleet.mjs +7 -9
  36. package/bin/lib/fleetPost.mjs +172 -25
  37. package/bin/lib/fleetReports.mjs +130 -170
  38. package/bin/lib/fleetStartup.mjs +6 -6
  39. package/bin/lib/fleetWire.mjs +42 -0
  40. package/bin/lib/git.mjs +111 -61
  41. package/bin/lib/gitMergeIdentity.mjs +2 -8
  42. package/bin/lib/grant.mjs +23 -7
  43. package/bin/lib/holder.mjs +108 -81
  44. package/bin/lib/hooks/readGuard.mjs +41 -7
  45. package/bin/lib/install.mjs +80 -33
  46. package/bin/lib/instance.mjs +36 -83
  47. package/bin/lib/intakeDraft.mjs +4 -1
  48. package/bin/lib/knowledgeFileSync.mjs +31 -0
  49. package/bin/lib/knowledgeFiles.mjs +28 -83
  50. package/bin/lib/landed.mjs +22 -29
  51. package/bin/lib/launchCommand.mjs +91 -0
  52. package/bin/lib/listeners.mjs +0 -5
  53. package/bin/lib/localSessions.mjs +52 -39
  54. package/bin/lib/login.mjs +22 -12
  55. package/bin/lib/loginPath.mjs +22 -17
  56. package/bin/lib/machineListing.mjs +6 -3
  57. package/bin/lib/mcp-cli.mjs +9 -10
  58. package/bin/lib/noWindowEnv.mjs +23 -3
  59. package/bin/lib/ownedDirs.mjs +99 -0
  60. package/bin/lib/pinnedCapture.mjs +383 -0
  61. package/bin/lib/prWorkflow.mjs +94 -17
  62. package/bin/lib/preflight.mjs +8 -25
  63. package/bin/lib/preview.mjs +19 -25
  64. package/bin/lib/previewGatePolicy.mjs +34 -9
  65. package/bin/lib/previewRegistry.mjs +4 -11
  66. package/bin/lib/printable.mjs +70 -7
  67. package/bin/lib/procRegistry.mjs +33 -13
  68. package/bin/lib/procScan.mjs +2 -3
  69. package/bin/lib/procStat.mjs +76 -0
  70. package/bin/lib/processGroup.mjs +48 -0
  71. package/bin/lib/processes.mjs +10 -24
  72. package/bin/lib/projectToolRuntimes.mjs +101 -16
  73. package/bin/lib/projectTools.mjs +50 -32
  74. package/bin/lib/prompts.mjs +41 -19
  75. package/bin/lib/repoPath.mjs +19 -0
  76. package/bin/lib/repoState.mjs +17 -19
  77. package/bin/lib/runTurn.mjs +268 -210
  78. package/bin/lib/runtimeAntigravity.mjs +16 -5
  79. package/bin/lib/runtimeCapabilities.mjs +140 -20
  80. package/bin/lib/runtimeClaude.mjs +55 -5
  81. package/bin/lib/runtimeCodex.mjs +245 -205
  82. package/bin/lib/runtimeDetection.mjs +2 -2
  83. package/bin/lib/runtimeEvents.mjs +16 -10
  84. package/bin/lib/runtimeLimits.mjs +5 -6
  85. package/bin/lib/runtimeOfferings.mjs +24 -11
  86. package/bin/lib/runtimes.mjs +10 -0
  87. package/bin/lib/shipSweep.mjs +5 -3
  88. package/bin/lib/shot.mjs +91 -17
  89. package/bin/lib/sleep.mjs +11 -0
  90. package/bin/lib/startProject.mjs +5 -5
  91. package/bin/lib/statePaths.mjs +40 -0
  92. package/bin/lib/tty.mjs +25 -15
  93. package/bin/lib/turnLock.mjs +7 -14
  94. package/bin/lib/turnProfile.mjs +1 -1
  95. package/bin/lib/ui.mjs +0 -3
  96. package/bin/lib/uninstall.mjs +30 -26
  97. package/bin/lib/update.mjs +79 -27
  98. package/bin/lib/uplinkScrub.mjs +296 -31
  99. package/bin/lib/vault.mjs +78 -10
  100. package/bin/lib/vaultDiff.mjs +5 -2
  101. package/bin/lib/views.mjs +37 -12
  102. package/bin/lib/wikiRunner.mjs +40 -61
  103. package/bin/lib/work.mjs +17 -7
  104. package/bin/lib/workAdoptCarry.mjs +2 -4
  105. package/bin/lib/workAgentCheck.mjs +104 -57
  106. package/bin/lib/workAgentMerges.mjs +62 -57
  107. package/bin/lib/workAgentPlans.mjs +99 -145
  108. package/bin/lib/workAgentPrecheck.mjs +71 -138
  109. package/bin/lib/workAgentPublish.mjs +6 -8
  110. package/bin/lib/workAgentTurnBegun.mjs +15 -11
  111. package/bin/lib/workAgentTurnExecution.mjs +46 -53
  112. package/bin/lib/workAgentTurnReports.mjs +9 -37
  113. package/bin/lib/workAgentTurns.mjs +4 -3
  114. package/bin/lib/workAttachments.mjs +66 -11
  115. package/bin/lib/workBrain.mjs +9 -4
  116. package/bin/lib/workCappedTurn.mjs +109 -0
  117. package/bin/lib/workChildren.mjs +5 -5
  118. package/bin/lib/workCommandAudit.mjs +11 -2
  119. package/bin/lib/workDiffs.mjs +52 -6
  120. package/bin/lib/workIntake.mjs +35 -60
  121. package/bin/lib/workPlaceReports.mjs +7 -4
  122. package/bin/lib/workPlaces.mjs +96 -30
  123. package/bin/lib/workPreviews.mjs +47 -112
  124. package/bin/lib/workProcesses.mjs +8 -35
  125. package/bin/lib/workPullRequests.mjs +37 -56
  126. package/bin/lib/workReportQueue.mjs +100 -148
  127. package/bin/lib/workRetire.mjs +2 -1
  128. package/bin/lib/workSessionRuntime.mjs +38 -37
  129. package/bin/lib/workSessionTurns.mjs +111 -189
  130. package/bin/lib/workShip.mjs +43 -23
  131. package/bin/lib/workTurnShape.mjs +163 -0
  132. package/npm-shrinkwrap.json +59 -0
  133. package/package.json +3 -2
  134. package/bin/lib/fleetJobs.mjs +0 -120
  135. package/bin/lib/patchRevert.mjs +0 -60
@@ -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(`— "${job.sessionName || job.sessionId}"`)}`);
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 = shipPlace === REPO_PLACE ? repoRoot : join(baseDir, 'sessions', shipPlace);
107
- let branch = `session/${job.sessionId}`;
108
- let detached = false;
109
- if (existsSync(wt)) {
110
- try {
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: job.sessionName || job.sessionId.slice(0, 8),
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
- workingTree: placeWtFor(shipPlace)?.wt ?? null,
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
- String(detail)
250
- .split('\n')
251
- .find((l) => l.trim()) ?? 'git merge failed'
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 "${job.sessionName || job.sessionId}" — ship waits for it`
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
- String(detail)
317
- .split('\n')
318
- .find((l) => l.trim()) ?? 'git merge failed'
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(String(e?.message ?? 'the merge failed')).slice(0, 500),
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.115.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"
@@ -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
- }
@@ -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
- }