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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# flowviant
|
|
2
2
|
|
|
3
|
-
Run your own coding CLIs as build agents for [Flowviant](https://flowviant.com) — [Claude Code](https://claude.com/claude-code), Codex or Antigravity, on your own credentials. This daemon holds your sessions, keeps a worktree per tab, and ships branches on your word.
|
|
3
|
+
Run your own coding CLIs as build agents for [Flowviant](https://flowviant.com) — [Claude Code](https://claude.com/claude-code), Codex or Antigravity, on your own credentials. This daemon holds your sessions, keeps a worktree per tab, and ships branches on your word. The daemon does not send your Claude, Codex or GitHub logins to Flowviant; it shells out to CLIs you signed in yourself.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
curl -fsSL https://api.flowviant.com/install.sh | sh
|
|
@@ -15,7 +15,7 @@ A Flowviant project has ONE machine, and this is it: one box, running one Claude
|
|
|
15
15
|
|
|
16
16
|
You work in the **Workbench**, where your sessions are TABS — each one a held context plus a persistent git worktree on its own `session/<id>` branch. The browser is a terminal projected onto this machine, so you reach the same session from any device. When you say ship, the tab merges its own branch into base with `--no-ff` — no squash, so the commit shas the work reported still exist on main.
|
|
17
17
|
|
|
18
|
-
Because it drives the CLIs you're already logged into, **the cost is yours** (your Claude subscription, your GitHub) and **the daemon
|
|
18
|
+
Because it drives the CLIs you're already logged into, **the cost is yours** (your Claude subscription, your GitHub) and **the daemon does not send your logins** — it shells out to tools you authenticated yourself, and reads Claude Code's credential file only for its expiry and plan type. What a turn prints is relayed as printed, after redaction (see [Security posture](#security-posture)).
|
|
19
19
|
|
|
20
20
|
## Requirements
|
|
21
21
|
|
|
@@ -102,7 +102,7 @@ From **0.86.0**, a project whose owner turns on **Publish agent branches** has e
|
|
|
102
102
|
|
|
103
103
|
## Sharing a preview
|
|
104
104
|
|
|
105
|
-
You run your dev server yourself, in the session's own worktree, exactly as you would in any terminal. The daemon NOTICES the listening port; ask for a share in the app and it puts a [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) quick tunnel in front of it (auto-fetched if missing, pinned and checksummed) behind
|
|
105
|
+
You run your dev server yourself, in the session's own worktree, exactly as you would in any terminal. The daemon NOTICES the listening port; ask for a share in the app and it puts a [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) quick tunnel in front of it (auto-fetched if missing, pinned and checksummed), behind the daemon's own gate. Viewers reach the share through Flowviant's relay at `<name>.flowviant.dev`, never the tunnel's own address, and the gate admits only the share's password or a short-lived grant signed with the share's key. Flowviant stores that key and the password with the share, and its relay mints grants for signed-in project members — or for anyone with the link, if the share's owner opens it to everyone. Since 0.72.0 the gate also lets the Workbench embed the share in a frame: it rewrites the response's frame policy to permit exactly the app's origin, and a framed sign-in gets a partitioned cookie so it works where third-party cookies are blocked.
|
|
106
106
|
|
|
107
107
|
The daemon never executes anything the repository declares. An earlier version read a `.flowviant/preview.json` from the branch and spawned the command it named — that start path was removed in 0.53.0 and is not coming back; see the header of `bin/lib/preview.mjs` for exactly what it did, so nobody rebuilds it.
|
|
108
108
|
|
|
@@ -111,7 +111,7 @@ The daemon never executes anything the repository declares. An earlier version r
|
|
|
111
111
|
| Env | What runs |
|
|
112
112
|
| --- | --- |
|
|
113
113
|
| _(stored login)_ or `FLOWVIANT_MACHINE_TOKEN` (`FLOWVIANT_FLEET` still read) | **the daemon** — the project's machine, serving its sessions |
|
|
114
|
-
| `FLOWVIANT_SAFE=1` |
|
|
114
|
+
| `FLOWVIANT_SAFE=1` | a permission list instead of running unattended (not a sandbox — see below) |
|
|
115
115
|
|
|
116
116
|
## Not freezing the box
|
|
117
117
|
|
|
@@ -133,7 +133,7 @@ Workbench tab executes a coding agent with the daemon's own OS permissions.
|
|
|
133
133
|
Membership is the consent boundary, the same trust plane as the shared
|
|
134
134
|
repository: invite people you would give a shell to.
|
|
135
135
|
|
|
136
|
-
Two knobs
|
|
136
|
+
Two knobs are worth setting on a shared box, and only the first is a boundary:
|
|
137
137
|
|
|
138
138
|
- **Run the daemon under a dedicated OS user** that owns only the repository
|
|
139
139
|
checkout and `~/.flowviant`. This is the single biggest hardening available
|
|
@@ -141,19 +141,38 @@ Two knobs bound the blast radius, and both are worth setting on a shared box:
|
|
|
141
141
|
home directory, or the rest of the machine. A plain separate account works;
|
|
142
142
|
a systemd unit with `ProtectHome=read-only` and `ReadWritePaths=` works
|
|
143
143
|
better.
|
|
144
|
-
- **`FLOWVIANT_SAFE=1`**
|
|
145
|
-
(edit/read/search plus `git
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
- **`FLOWVIANT_SAFE=1`** replaces unattended mode with a permission list for
|
|
145
|
+
Claude (edit/read/search plus `git`, `gh`, `npm` and `bun`) and a
|
|
146
|
+
workspace-write sandbox for Codex. Those programs can run arbitrary code
|
|
147
|
+
themselves (scripts, hooks, `bun -e`), so this narrows what a confused agent
|
|
148
|
+
reaches for; it is not a sandbox. The dedicated OS user is the boundary.
|
|
149
|
+
Antigravity has no per-invocation narrowing — its permission engine is
|
|
150
|
+
machine-wide — which is surfaced in the app rather than papered over.
|
|
151
|
+
|
|
152
|
+
What leaves the box is redacted. Before it posts what a turn
|
|
153
|
+
printed (answers, narration, tool calls, commands, the trace, artifacts, wiki
|
|
154
|
+
notes, deploy and check output, commit diffs), the daemon replaces the values in
|
|
155
|
+
your checkout's root `.env*` files and in the env files a deploy copies, the
|
|
156
|
+
deploy and CLI credentials in its own environment, your Codex provider's
|
|
157
|
+
secrets, its own Flowviant credential, and well-known token shapes (Anthropic,
|
|
158
|
+
OpenAI, GitHub, AWS, Slack, OAuth token fields, PEM private keys) with
|
|
159
|
+
`[REDACTED:NAME]`. A value that does not look generated (a short one, a word, a
|
|
160
|
+
hostname, a lowercase hex string) is left as it is, and a secret in none of
|
|
161
|
+
those is relayed as printed. What your CLI writes through Flowviant's own tools
|
|
162
|
+
(such as a tab's progress messages and purpose line, or the cards a New task
|
|
163
|
+
chat stages) goes from the CLI to Flowviant, not through the daemon, as written.
|
|
164
|
+
The full list, and the exact test for what looks generated, are in
|
|
165
|
+
[SECURITY.md](./SECURITY.md#what-is-redacted).
|
|
149
166
|
|
|
150
167
|
Fenced turns — design, 3D-model, deck and research cards, the pre-review,
|
|
151
168
|
the planner, intake, the wiki and the New task chat — read none of your Claude
|
|
152
169
|
Code settings files: their permissions and hooks are Flowviant's posture
|
|
153
170
|
alone, and of `~/.claude/settings.json` they keep only how Claude signs in
|
|
154
|
-
(`apiKeyHelper`, `env` and the like) and your model defaults
|
|
171
|
+
(`apiKeyHelper`, `env` and the like) and your model defaults — handed to Claude
|
|
172
|
+
in a file only your user can read (`~/.flowviant/run/`, removed when the turn
|
|
173
|
+
ends), never on the command line, where every local user could read it. Code agents and
|
|
155
174
|
Terminal tabs still read yours, so your own allow rules and `defaultMode` add
|
|
156
|
-
to `FLOWVIANT_SAFE=1`'s
|
|
175
|
+
to `FLOWVIANT_SAFE=1`'s permission list there. Codex's fenced turns run with
|
|
157
176
|
`--ignore-user-config` the same way.
|
|
158
177
|
|
|
159
178
|
The posture is reported on every poll and shown in the project's
|
package/bin/cli.mjs
CHANGED
|
@@ -104,7 +104,7 @@ if (dirAt >= 0 && commandUsesCheckout(process.argv[2])) {
|
|
|
104
104
|
const { CREDENTIAL, VERSION } = await import('./lib/config.mjs');
|
|
105
105
|
const { runFleetDaemon } = await import('./lib/fleet.mjs');
|
|
106
106
|
const { runLogin } = await import('./lib/login.mjs');
|
|
107
|
-
const { launchCommand, terminalCommand } = await import('./lib/launchCommand.mjs');
|
|
107
|
+
const { launchCommand, relaunchSelf, selfArgv, terminalCommand } = await import('./lib/launchCommand.mjs');
|
|
108
108
|
|
|
109
109
|
if (process.argv.includes('--remote') && !(process.argv[2] === 'status' && process.argv.includes('--json'))) {
|
|
110
110
|
console.error('error: --remote requires status --json');
|
|
@@ -159,19 +159,14 @@ async function loginCommand() {
|
|
|
159
159
|
// Re-exec as a plain `flowviant` rather than falling through. config.mjs reads
|
|
160
160
|
// the credential at IMPORT time — which was before the login we just did — so
|
|
161
161
|
// this process still has an empty FLEET_TOKEN and would exit with "no
|
|
162
|
-
// credential found" seconds after saving one.
|
|
163
|
-
//
|
|
164
|
-
// process.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
});
|
|
171
|
-
// AWAIT it. Registering an exit handler and falling through would run the rest
|
|
172
|
-
// of this file in the parent — which has no credential — and print "no
|
|
173
|
-
// credential found" over the daemon that just started in the child.
|
|
174
|
-
process.exit(await new Promise((resolve) => child.on('exit', (code) => resolve(code ?? 0))));
|
|
162
|
+
// credential found" seconds after saving one. The one relaunch proxy
|
|
163
|
+
// (launchCommand.mjs) keeps this process alive as the child's stand-in, so the
|
|
164
|
+
// user's shell keeps one foreground process.
|
|
165
|
+
// AWAIT it: it ends this process with the child's exit code, so the await
|
|
166
|
+
// never returns. Falling through would run the rest of this file in the
|
|
167
|
+
// parent — which has no credential — and print "no credential found" over
|
|
168
|
+
// the daemon that just started in the child.
|
|
169
|
+
await relaunchSelf({ argv: selfArgv(json ? ['--json-events'] : []), env: process.env });
|
|
175
170
|
}
|
|
176
171
|
|
|
177
172
|
// `flowviant status` — each project connected here. `--json` is the tray's
|
|
@@ -242,13 +237,14 @@ async function ghAuthCommand() {
|
|
|
242
237
|
// blocks re-adding a path.
|
|
243
238
|
async function cleanCommand() {
|
|
244
239
|
const { rmSync, existsSync } = await import('node:fs');
|
|
245
|
-
const { join } = await import('node:path');
|
|
246
|
-
const { homedir } = await import('node:os');
|
|
247
240
|
const { execFileSync } = await import('node:child_process');
|
|
241
|
+
// The root the daemon keys every checkout's worktree home under — the one
|
|
242
|
+
// derivation (statePaths.mjs), so this sweeps exactly the daemon's directory.
|
|
243
|
+
const { worktreesRoot } = await import('./lib/statePaths.mjs');
|
|
248
244
|
// Also reap any preview dev-server/tunnel groups a crashed daemon left running.
|
|
249
245
|
const { reapOrphanPreviews } = await import('./lib/previewRegistry.mjs');
|
|
250
246
|
reapOrphanPreviews((m) => console.log(m));
|
|
251
|
-
const dir =
|
|
247
|
+
const dir = worktreesRoot();
|
|
252
248
|
if (!existsSync(dir)) {
|
|
253
249
|
console.log('nothing to clean — no worktrees at ~/.flowviant/worktrees.');
|
|
254
250
|
process.exit(0);
|
package/bin/lib/admission.mjs
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
* idempotent so doing both is the normal case rather than a bug.
|
|
80
80
|
*/
|
|
81
81
|
|
|
82
|
-
import { MAX_CONCURRENT, MAX_CONCURRENT_FROM_ENV } from './config.mjs';
|
|
82
|
+
import { MAX_CONCURRENT, MAX_CONCURRENT_FROM_ENV, clampTurnCeiling } from './config.mjs';
|
|
83
83
|
import { measurePressure } from './resources.mjs';
|
|
84
84
|
import { pressureVerdict, readPressureThresholds } from './pressurePolicy.mjs';
|
|
85
85
|
|
|
@@ -126,15 +126,11 @@ export function livePressureVerdict(level, { measure = measurePressure, env = pr
|
|
|
126
126
|
* sentinel number that could be mistaken for a bound.
|
|
127
127
|
*
|
|
128
128
|
* Pure, and exported, so the order can be proved without a server, a poll or a
|
|
129
|
-
* box of any particular size.
|
|
129
|
+
* box of any particular size. Each author is read through config.mjs's one
|
|
130
|
+
* `clampTurnCeiling` (2026-10-10, audit F070).
|
|
130
131
|
*/
|
|
131
132
|
export function pickMaxTurns({ env = null, server = null, derived = 1 } = {}) {
|
|
132
|
-
|
|
133
|
-
const v = Number(n);
|
|
134
|
-
if (!Number.isFinite(v) || v < 1) return null;
|
|
135
|
-
return Math.min(Math.floor(v), 32);
|
|
136
|
-
};
|
|
137
|
-
return clamp(env) ?? clamp(server) ?? clamp(derived) ?? 1;
|
|
133
|
+
return clampTurnCeiling(env) ?? clampTurnCeiling(server) ?? clampTurnCeiling(derived) ?? 1;
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
/**
|
|
@@ -164,8 +160,7 @@ let serverMaxTurns = null;
|
|
|
164
160
|
* dial back to Auto would be unspellable.
|
|
165
161
|
*/
|
|
166
162
|
export function setServerMaxTurns(raw) {
|
|
167
|
-
|
|
168
|
-
serverMaxTurns = Number.isFinite(v) && v >= 1 ? Math.min(Math.floor(v), 32) : null;
|
|
163
|
+
serverMaxTurns = clampTurnCeiling(raw);
|
|
169
164
|
return serverMaxTurns;
|
|
170
165
|
}
|
|
171
166
|
|
|
@@ -206,10 +201,8 @@ export function createAdmission({
|
|
|
206
201
|
* unguarded spawn loop that froze somebody's computer. The local derivation
|
|
207
202
|
* is the one value in this process that cannot be garbage.
|
|
208
203
|
*/
|
|
209
|
-
const asked =
|
|
210
|
-
|
|
211
|
-
const ceiling =
|
|
212
|
-
Number.isFinite(asked) && asked >= 1 ? Math.min(Math.floor(asked), 32) : MAX_CONCURRENT;
|
|
204
|
+
const asked = typeof maxConcurrent === 'function' ? maxConcurrent() : maxConcurrent;
|
|
205
|
+
const ceiling = clampTurnCeiling(asked) ?? MAX_CONCURRENT;
|
|
213
206
|
if (live >= ceiling)
|
|
214
207
|
return {
|
|
215
208
|
reason: `the machine is already running ${live} CLI turn${live === 1 ? '' : 's'}`,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Recover a CLI that yielded to its own background work, inside the original
|
|
2
|
+
* turn and place writer lock. Tool requests establish intent; prose is never
|
|
3
|
+
* classified. No new server turn is created and no intermediate settle is sent.
|
|
4
|
+
* The bounded recovery either finishes, asks a real question, or throws a
|
|
5
|
+
* measured failure for the lane's existing one-shot settlement. */
|
|
6
|
+
import { parseTurnResult } from './agentPlan.mjs';
|
|
7
|
+
import { limitLine } from './cliLimit.mjs';
|
|
8
|
+
|
|
9
|
+
export const INTERNAL_WAIT_RECOVERIES = 2;
|
|
10
|
+
export const INTERNAL_WAIT_CONTRACT = `Your own command or test still running is internal work, never a human blocker.
|
|
11
|
+
Wait using the command's completion/read tools, read its result, finish the card
|
|
12
|
+
and commit before returning the final JSON. A progress update is not a final
|
|
13
|
+
answer. Do not end the CLI to await a background notification or ScheduleWakeup:
|
|
14
|
+
this unattended runner does not deliver those after the CLI exits. Only a real
|
|
15
|
+
person-only decision or unavailable input belongs in blocked.question.`;
|
|
16
|
+
|
|
17
|
+
export async function runAgentWithInternalWaits(run, options) {
|
|
18
|
+
let thread = options.resumeThreadId;
|
|
19
|
+
let total = null;
|
|
20
|
+
for (let attempt = 0; ; attempt++) {
|
|
21
|
+
let requestedWait = false;
|
|
22
|
+
let exitCode = null;
|
|
23
|
+
let exitSignal = null;
|
|
24
|
+
let cliFailed = false;
|
|
25
|
+
let lastAnswer = null;
|
|
26
|
+
let currentUsage = null;
|
|
27
|
+
const combine = () => {
|
|
28
|
+
if (!currentUsage) return total;
|
|
29
|
+
return Object.fromEntries(['input', 'output', 'cacheCreate', 'cacheRead'].map((k) =>
|
|
30
|
+
[k, (total?.[k] ?? 0) + (currentUsage[k] ?? 0)]));
|
|
31
|
+
};
|
|
32
|
+
const out = await run({
|
|
33
|
+
...options,
|
|
34
|
+
...(attempt ? {
|
|
35
|
+
resume: Boolean(thread),
|
|
36
|
+
resumeThreadId: thread,
|
|
37
|
+
prompt: `${options.prompt ?? ''}\n\nContinue THIS SAME CARD after yielding to internal work. Inspect the existing\nbackground command/task and read its result; do not restart a still-running test suite or\nstart a duplicate command. If the CLI killed the background job, confirm that\nfrom its task status/output before rerunning the required check. Use completion/read tools until it finishes. Then\nfinish the work, check the result and commit. ${INTERNAL_WAIT_CONTRACT}`,
|
|
38
|
+
} : {}),
|
|
39
|
+
onCliFailure: () => {
|
|
40
|
+
cliFailed = true;
|
|
41
|
+
options.onCliFailure?.();
|
|
42
|
+
},
|
|
43
|
+
onExit: (code, signal) => {
|
|
44
|
+
exitCode = code;
|
|
45
|
+
exitSignal = signal;
|
|
46
|
+
options.onExit?.(code, signal);
|
|
47
|
+
},
|
|
48
|
+
onInit: (init) => {
|
|
49
|
+
if (typeof init.sessionId === 'string') thread = init.sessionId;
|
|
50
|
+
options.onInit?.(init);
|
|
51
|
+
},
|
|
52
|
+
onThreadId: (id) => {
|
|
53
|
+
thread = id;
|
|
54
|
+
options.onThreadId?.(id);
|
|
55
|
+
},
|
|
56
|
+
onAnswer: (text) => {
|
|
57
|
+
lastAnswer = text;
|
|
58
|
+
options.onAnswer?.(text);
|
|
59
|
+
},
|
|
60
|
+
onUsage: (usage) => {
|
|
61
|
+
currentUsage = usage;
|
|
62
|
+
options.onUsage?.(combine());
|
|
63
|
+
},
|
|
64
|
+
onToolEvent: (name, input) => {
|
|
65
|
+
// The actual incident was ScheduleWakeup(noop:true) after Bash returned
|
|
66
|
+
// a background task id. No tool-name guesses or sentence matching.
|
|
67
|
+
if ((name === 'ScheduleWakeup' && input?.noop === true) ||
|
|
68
|
+
(name === 'Bash' && input?.run_in_background === true) ||
|
|
69
|
+
(name === 'TaskOutput' && input?.block === true)) requestedWait = true;
|
|
70
|
+
options.onToolEvent?.(name, input);
|
|
71
|
+
},
|
|
72
|
+
}, attempt > 0);
|
|
73
|
+
total = combine();
|
|
74
|
+
// A declared question/delivery always wins. Missing JSON alone never
|
|
75
|
+
// retries. A crash/signal/limit cannot be disguised by an earlier wait.
|
|
76
|
+
if (parseTurnResult(lastAnswer ?? out) || !requestedWait || exitCode !== 0 ||
|
|
77
|
+
exitSignal || cliFailed || limitLine(out)) return out;
|
|
78
|
+
if (attempt >= INTERNAL_WAIT_RECOVERIES)
|
|
79
|
+
throw new Error('internal wait recovery exhausted: the CLI exited without a result after two continuations on the same card');
|
|
80
|
+
if (!thread) throw new Error('internal wait recovery could not resume: the CLI reported no session id');
|
|
81
|
+
options.onContinuation?.(attempt + 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/bin/lib/agentPlan.mjs
CHANGED
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
* Run: node --test bin/lib/agentPlan.test.mjs
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
// THE UPLINK SCRUBBER, as the DEFAULT for the
|
|
29
|
-
// goes to the server (2026-09-24, the audit).
|
|
30
|
-
|
|
28
|
+
// THE UPLINK SCRUBBER, as the DEFAULT for the three readers whose every string
|
|
29
|
+
// goes to the server (2026-09-24, the audit; the precheck joined 2026-10-10).
|
|
30
|
+
// See `parseTurnResult`. `scrubCut` is the scrub-then-cut order, spelled once.
|
|
31
|
+
import { scrub as envScrub, scrubCut } from './uplinkScrub.mjs';
|
|
31
32
|
|
|
32
33
|
/** The biggest a single proposal may be. Bounds on a machine, not a policy:
|
|
33
34
|
* the server caps these again at its own boundary. */
|
|
@@ -68,7 +69,8 @@ const MAX_NOTE = 1000;
|
|
|
68
69
|
* none of which a real answer comes near:
|
|
69
70
|
*
|
|
70
71
|
* · only the LAST `MAX_SCAN_CHARS` are scanned — every contract here puts the
|
|
71
|
-
* object LAST, and the fenced block is still looked for in the whole text
|
|
72
|
+
* object LAST, and the fenced block is still looked for in the whole text,
|
|
73
|
+
* by a LINEAR scan (`fencedBody`) since 2026-10-10;
|
|
72
74
|
* · each `{`'s closing brace is found ONCE (see `closers`), so the scan is a
|
|
73
75
|
* single right-to-left pass instead of one walk per brace;
|
|
74
76
|
* · only spans that can open an object (`{` then a key or `}`) are handed to
|
|
@@ -90,10 +92,10 @@ const MAX_NESTED_PARSES = 200;
|
|
|
90
92
|
const MAX_NESTED_CHARS = 4 * 1024 * 1024;
|
|
91
93
|
|
|
92
94
|
export function candidateObjects(text) {
|
|
93
|
-
const fenced = text
|
|
95
|
+
const fenced = fencedBody(text);
|
|
94
96
|
const raw = text.length > MAX_SCAN_CHARS ? text.slice(text.length - MAX_SCAN_CHARS) : text;
|
|
95
97
|
const bodies = [];
|
|
96
|
-
if (fenced) bodies.push(fenced
|
|
98
|
+
if (fenced !== null) bodies.push(fenced);
|
|
97
99
|
const close = closers(raw);
|
|
98
100
|
let topEnd = -1;
|
|
99
101
|
let nestedParses = 0;
|
|
@@ -125,6 +127,35 @@ export function candidateObjects(text) {
|
|
|
125
127
|
return out;
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
/**
|
|
131
|
+
* THE FIRST FENCED BLOCK'S BODY, or null — what
|
|
132
|
+
* `text.match(/```(?:json)?\s*([\s\S]*?)```/)[1]` answered, found in ONE
|
|
133
|
+
* forward pass (2026-10-10, the audit's F079).
|
|
134
|
+
*
|
|
135
|
+
* That regex ran over the WHOLE text — the only search the `MAX_SCAN_CHARS`
|
|
136
|
+
* bound above skipped — and it backtracks quadratically on an opening fence
|
|
137
|
+
* followed by a long whitespace run with no closing fence: the greedy `\s*` and
|
|
138
|
+
* the lazy body re-scan the run from every split. Measured: 80 KB took 0.6 s,
|
|
139
|
+
* 320 KB nine seconds, of synchronous CPU in which no poll, lease or other
|
|
140
|
+
* lane moved — and a turn's stdout is model-written and uncapped. Same
|
|
141
|
+
* answer, by `indexOf`: the first ``` anywhere, an optional `json`, the
|
|
142
|
+
* whitespace after it skipped, and the body runs to the next ```. If that
|
|
143
|
+
* opening has no closer, no later opening can have one either, so there is no
|
|
144
|
+
* fence. The search stays over the whole text, as the bound above promises.
|
|
145
|
+
*/
|
|
146
|
+
const FENCE_SPACE = /\s*/y;
|
|
147
|
+
function fencedBody(text) {
|
|
148
|
+
const open = text.indexOf('```');
|
|
149
|
+
if (open < 0) return null;
|
|
150
|
+
let at = open + 3;
|
|
151
|
+
if (text.startsWith('json', at)) at += 4;
|
|
152
|
+
FENCE_SPACE.lastIndex = at;
|
|
153
|
+
FENCE_SPACE.exec(text);
|
|
154
|
+
at = FENCE_SPACE.lastIndex;
|
|
155
|
+
const close = text.indexOf('```', at);
|
|
156
|
+
return close < 0 ? null : text.slice(at, close);
|
|
157
|
+
}
|
|
158
|
+
|
|
128
159
|
/**
|
|
129
160
|
* For every `{`, the index of the `}` that closes it when read as the START of
|
|
130
161
|
* an object (outside any string), or -1 — the answer `balanced` gives, for all
|
|
@@ -186,7 +217,7 @@ export function parseProposal(text, scrub = envScrub) {
|
|
|
186
217
|
if (taskIds.length === 0) continue;
|
|
187
218
|
agents.push({
|
|
188
219
|
tempId: String(g.tempId || `a${i + 1}`).slice(0, 64),
|
|
189
|
-
name:
|
|
220
|
+
name: scrubCut(g.name, MAX_NAME, scrub),
|
|
190
221
|
taskIds,
|
|
191
222
|
...(Number.isFinite(g.pointsBudget) && g.pointsBudget > 0
|
|
192
223
|
? { pointsBudget: Math.min(Math.round(g.pointsBudget), 100_000) }
|
|
@@ -221,7 +252,7 @@ export function parseProposal(text, scrub = envScrub) {
|
|
|
221
252
|
return {
|
|
222
253
|
agents: uniqueTempIds(agents),
|
|
223
254
|
...(typeof parsed.note === 'string' && parsed.note.trim()
|
|
224
|
-
? { note:
|
|
255
|
+
? { note: scrubCut(parsed.note, MAX_NOTE, scrub) }
|
|
225
256
|
: {}),
|
|
226
257
|
};
|
|
227
258
|
}
|
|
@@ -284,7 +315,7 @@ function uniqueTempIds(list) {
|
|
|
284
315
|
const MAX_PROGRESS = 8000;
|
|
285
316
|
const progressOf = (v, scrub) => {
|
|
286
317
|
const t = typeof v.progress === 'string' ? v.progress.trim() : '';
|
|
287
|
-
return t ? { progress:
|
|
318
|
+
return t ? { progress: scrubCut(t, MAX_PROGRESS, scrub) } : {};
|
|
288
319
|
};
|
|
289
320
|
|
|
290
321
|
/**
|
|
@@ -337,14 +368,14 @@ const progressOf = (v, scrub) => {
|
|
|
337
368
|
* its prefix. So the scrub rides in, as `parsePrecheck`'s does, and runs over
|
|
338
369
|
* each whole field first.
|
|
339
370
|
*
|
|
340
|
-
* DEFAULTED TO THE REAL SCRUBBER,
|
|
341
|
-
* production callers pass nothing, and a redaction that
|
|
342
|
-
* remembering is the one that is missing the day it
|
|
343
|
-
* (the caller still scrubs `answer`) is harmless — a
|
|
344
|
-
* no secret to match.
|
|
371
|
+
* DEFAULTED TO THE REAL SCRUBBER, like `parseProposal` and (since 2026-10-10)
|
|
372
|
+
* `parsePrecheck`: the production callers pass nothing, and a redaction that
|
|
373
|
+
* depends on each caller remembering is the one that is missing the day it
|
|
374
|
+
* matters. Scrubbing twice (the caller still scrubs `answer`) is harmless — a
|
|
375
|
+
* redaction marker contains no secret to match.
|
|
345
376
|
*/
|
|
346
377
|
export function parseTurnResult(text, scrub = envScrub) {
|
|
347
|
-
const clean = (s, cap) =>
|
|
378
|
+
const clean = (s, cap) => scrubCut(s, cap, scrub);
|
|
348
379
|
for (const v of candidateObjects(String(text ?? ''))) {
|
|
349
380
|
if (v.status === 'blocked') {
|
|
350
381
|
const question = typeof v.question === 'string' ? v.question.trim() : '';
|
|
@@ -416,11 +447,15 @@ const MAX_PRECHECK_OVERALL = 1200;
|
|
|
416
447
|
* line is the ordinary way to reach it. The fix is `toolEventOf`'s: the scrub
|
|
417
448
|
* rides INTO the builder and runs over the whole field, before the cap.
|
|
418
449
|
*
|
|
419
|
-
* DEFAULTED TO
|
|
420
|
-
*
|
|
421
|
-
*
|
|
450
|
+
* DEFAULTED TO THE REAL SCRUBBER (2026-10-10, the audit's F072), like its two
|
|
451
|
+
* neighbours. It used to default to identity "so a caller that forgets loses
|
|
452
|
+
* redaction rather than the whole reading" — the opposite stance from
|
|
453
|
+
* `parseTurnResult`'s for the same class of model-written text, and a
|
|
454
|
+
* redaction that depends on each caller remembering is the one that is missing
|
|
455
|
+
* the day it matters. Tests that want the raw reading pass identity; the one
|
|
456
|
+
* production caller passes `envScrub`, and `work.test.mjs` pins the order.
|
|
422
457
|
*/
|
|
423
|
-
export function parsePrecheck(text, scrub =
|
|
458
|
+
export function parsePrecheck(text, scrub = envScrub) {
|
|
424
459
|
const parsed = candidateObjects(String(text ?? '')).find((v) => Array.isArray(v.cards));
|
|
425
460
|
if (!parsed) return null;
|
|
426
461
|
|
|
@@ -434,14 +469,11 @@ export function parsePrecheck(text, scrub = (s) => s) {
|
|
|
434
469
|
seen.add(taskId);
|
|
435
470
|
// SCRUB, THEN CUT — see the docblock. The whole field is in hand here, so an
|
|
436
471
|
// exact-value match still finds a secret that spans the cap.
|
|
437
|
-
const note =
|
|
438
|
-
typeof c.note === 'string' ? scrub(c.note.trim()).slice(0, MAX_PRECHECK_NOTE) : '';
|
|
472
|
+
const note = typeof c.note === 'string' ? scrubCut(c.note.trim(), MAX_PRECHECK_NOTE, scrub) : '';
|
|
439
473
|
cards.push({ taskId, verdict: c.verdict, ...(note ? { note } : {}) });
|
|
440
474
|
}
|
|
441
475
|
const overall =
|
|
442
|
-
typeof parsed.overall === 'string'
|
|
443
|
-
? scrub(parsed.overall.trim()).slice(0, MAX_PRECHECK_OVERALL)
|
|
444
|
-
: '';
|
|
476
|
+
typeof parsed.overall === 'string' ? scrubCut(parsed.overall.trim(), MAX_PRECHECK_OVERALL, scrub) : '';
|
|
445
477
|
/**
|
|
446
478
|
* AN ANSWER THAT SAYS NOTHING IS NOT AN ANSWER. No readable card verdict and
|
|
447
479
|
* no overall means the model produced the right punctuation and no content —
|
package/bin/lib/agentPublish.mjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* SHAPE, never a path, never a value that reaches argv unread.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import {
|
|
30
|
+
import { isValidSha, sessionBranchRef, stripUrlUserinfo } from './git.mjs';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* THE SHAPE A PUBLISHED REF MAY HAVE — the server's own `PUBLISH_REF_RE`,
|
|
@@ -52,19 +52,19 @@ export function isPublishRef(ref) {
|
|
|
52
52
|
return typeof ref === 'string' && PUBLISH_REF_RE.test(ref);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** The local branch an agent works on. One definition
|
|
56
|
-
* the
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
/** The local branch an agent works on. One definition — git.mjs's
|
|
56
|
+
* `sessionBranchRef`, the same name the cutter and the begun-guard read — so
|
|
57
|
+
* a push, a fetch and the guard's own `rev-parse` can never name three
|
|
58
|
+
* different branches. */
|
|
59
|
+
export const agentBranchRef = sessionBranchRef;
|
|
60
60
|
|
|
61
61
|
/** A sha this machine MEASURED, in the one form `rev-parse` prints. The lease
|
|
62
62
|
* below is built from nothing else: an unrecognised value is treated as no
|
|
63
|
-
* observation at all rather than interpolated into argv.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
* observation at all rather than interpolated into argv. git.mjs's
|
|
64
|
+
* `isValidSha`, re-exported under the name this module's readers use — it
|
|
65
|
+
* was a verbatim copy of that regex, so a tightening could land in one and
|
|
66
|
+
* not the other (audit 2026-10-10, F749). */
|
|
67
|
+
export const isSha = isValidSha;
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* PUSH THIS AGENT'S BRANCH TO ITS PUBLISHED NAME.
|
|
@@ -153,8 +153,7 @@ export function publishDeleteArgs(ref) {
|
|
|
153
153
|
*/
|
|
154
154
|
const DIAGNOSTIC_RE = /^(fatal:|error:|remote:|!)|\b(denied|rejected|refused)\b/i;
|
|
155
155
|
export function publishErrorText(raw) {
|
|
156
|
-
const lines =
|
|
157
|
-
.replace(/\/\/[^/@\s]+@/g, '//')
|
|
156
|
+
const lines = stripUrlUserinfo(raw)
|
|
158
157
|
.split('\n')
|
|
159
158
|
.map((l) => l.trim())
|
|
160
159
|
.filter(Boolean);
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
* which Claude and — since 0.115.0 — Codex declare (2026-09-29, measured:
|
|
38
38
|
* runtimeCodex.mjs `codexDesignFence`). */
|
|
39
39
|
const DESIGN_CLIS = 'mockup, 3D model and presentation cards run on Claude or Codex on this machine';
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
const
|
|
40
|
+
/** Write-ups run on Claude or isolated Codex (0.117.0); the copied input view
|
|
41
|
+
* keeps host credentials outside the web-enabled tool fence. */
|
|
42
|
+
const RESEARCH_CLIS = 'write-up cards run on Claude or Codex on this machine';
|
|
43
43
|
/** The image kind runs on Codex: the one CLI that declares its posture, because
|
|
44
44
|
* the pictures come from Codex's own image tool on its own login. */
|
|
45
45
|
const CODEX_ONLY = 'image cards run on Codex on this machine';
|
|
@@ -109,7 +109,7 @@ export const AGENT_TASK_KINDS = Object.freeze({
|
|
|
109
109
|
match: /\.md$/i,
|
|
110
110
|
missing: 'the turn ended without writing a write-up under .flowviant/artifacts/',
|
|
111
111
|
}),
|
|
112
|
-
offRuntime: () =>
|
|
112
|
+
offRuntime: () => RESEARCH_CLIS,
|
|
113
113
|
}),
|
|
114
114
|
});
|
|
115
115
|
|
|
@@ -144,9 +144,6 @@ export const unknownAgentTaskKind = (v) =>
|
|
|
144
144
|
? null
|
|
145
145
|
: String(v).replace(/[\u0000-\u001f\u007f]/g, '').slice(0, 40);
|
|
146
146
|
|
|
147
|
-
/** The table entry for a kind value, read the printing way (unknown → code). */
|
|
148
|
-
export const agentTaskKind = (v) => AGENT_TASK_KINDS[agentTaskKindOf(v)];
|
|
149
|
-
|
|
150
147
|
/** The postures only a card's kind selects (every kind's but code's), each
|
|
151
148
|
* once: the 3D-model and presentation kinds reuse the design posture; the
|
|
152
149
|
* image kind has its own. */
|
package/bin/lib/agySessions.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { homedir } from 'node:os';
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { execFileSync } from 'node:child_process';
|
|
15
15
|
import { procPids, scanProcs } from './procScan.mjs';
|
|
16
|
+
import { AGY_CONV_RE } from './conversationIds.mjs';
|
|
16
17
|
|
|
17
18
|
// agy's store is nothing like Claude's: one SQLite db per conversation at
|
|
18
19
|
// ~/.gemini/antigravity-cli/conversations/<uuid>.db (global, not cwd-keyed),
|
|
@@ -24,13 +25,12 @@ import { procPids, scanProcs } from './procScan.mjs';
|
|
|
24
25
|
// the person at that keyboard, i.e. the one worth offering to adopt.
|
|
25
26
|
|
|
26
27
|
const AGY_DIR = () => join(homedir(), '.gemini', 'antigravity-cli');
|
|
27
|
-
const AGY_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
28
28
|
|
|
29
29
|
/** Newest write to the conversation's store — the wal carries recent turns,
|
|
30
30
|
* so its mtime (not the db's) is the real "last active" (measured: a resume
|
|
31
31
|
* touched db+wal, and never the presence lock). 0 = no such conversation. */
|
|
32
32
|
function agyLastWriteMs(id) {
|
|
33
|
-
if (!
|
|
33
|
+
if (!AGY_CONV_RE.test(id)) return 0;
|
|
34
34
|
let newest = 0;
|
|
35
35
|
for (const suffix of ['.db', '.db-wal']) {
|
|
36
36
|
try {
|
|
@@ -121,7 +121,7 @@ export function scanAgyConversations({ ours, mine, cutoff, processAlive = agyPro
|
|
|
121
121
|
if (!map || typeof map !== 'object') return out;
|
|
122
122
|
const processUp = processAlive();
|
|
123
123
|
for (const [cwd, id] of Object.entries(map)) {
|
|
124
|
-
if (typeof id !== 'string' || !
|
|
124
|
+
if (typeof id !== 'string' || !AGY_CONV_RE.test(id)) continue;
|
|
125
125
|
// The operator's own agy TAB, whose conversation is registered against
|
|
126
126
|
// the checkout exactly as a terminal one would be.
|
|
127
127
|
if (mine.has(id)) continue;
|