flowviant 0.125.29 → 0.125.31

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 (72) hide show
  1. package/README.md +364 -0
  2. package/bin/cli.mjs +23 -0
  3. package/bin/lib/agentPlan.mjs +8 -0
  4. package/bin/lib/agentQueueContext.mjs +6 -0
  5. package/bin/lib/agentSplit.mjs +90 -0
  6. package/bin/lib/architectureContract.mjs +57 -0
  7. package/bin/lib/architectureReport.mjs +38 -0
  8. package/bin/lib/artifactContracts.mjs +21 -2
  9. package/bin/lib/claudeInput.mjs +1 -1
  10. package/bin/lib/claudeSubagents.mjs +46 -6
  11. package/bin/lib/claudeWorkflows.mjs +94 -0
  12. package/bin/lib/cliLimit.mjs +5 -0
  13. package/bin/lib/codexInput.mjs +191 -0
  14. package/bin/lib/codexSubagentEvents.mjs +3 -1
  15. package/bin/lib/codexSubagents.mjs +34 -9
  16. package/bin/lib/conversationOutput.mjs +14 -0
  17. package/bin/lib/crossCliChild.mjs +327 -0
  18. package/bin/lib/crossCliLaunch.mjs +92 -0
  19. package/bin/lib/crossCliPosture.mjs +55 -0
  20. package/bin/lib/crossCliResearch.mjs +19 -0
  21. package/bin/lib/environmentProcess.mjs +44 -0
  22. package/bin/lib/fixtures/workflow-ultracode.jsonl +22 -0
  23. package/bin/lib/fleet.mjs +2 -0
  24. package/bin/lib/linuxDesktopBridge.mjs +62 -0
  25. package/bin/lib/managedBrowser.mjs +50 -10
  26. package/bin/lib/mergeBatch.mjs +1 -1
  27. package/bin/lib/nativeAgentTools.mjs +12 -5
  28. package/bin/lib/noWindowEnv.mjs +4 -2
  29. package/bin/lib/orchestrator.mjs +24 -7
  30. package/bin/lib/orchestratorClient.mjs +8 -2
  31. package/bin/lib/orchestratorPrompt.mjs +13 -5
  32. package/bin/lib/orchestratorProtocol.mjs +18 -0
  33. package/bin/lib/outsideCli.mjs +124 -0
  34. package/bin/lib/projectToolRuntimes.mjs +4 -2
  35. package/bin/lib/projectTools.mjs +2 -2
  36. package/bin/lib/prompts.mjs +7 -5
  37. package/bin/lib/repositoryPlaces.mjs +1 -1
  38. package/bin/lib/repositorySetup.mjs +15 -24
  39. package/bin/lib/reviewBridge.mjs +110 -0
  40. package/bin/lib/reviewDatabase.mjs +112 -0
  41. package/bin/lib/reviewDesktop.mjs +153 -0
  42. package/bin/lib/reviewEnvironment.mjs +286 -0
  43. package/bin/lib/reviewEnvironmentConfig.mjs +104 -0
  44. package/bin/lib/reviewEnvironmentMain.mjs +38 -0
  45. package/bin/lib/reviewImage.mjs +58 -0
  46. package/bin/lib/reviewMcpClient.mjs +69 -0
  47. package/bin/lib/reviewProcess.mjs +51 -0
  48. package/bin/lib/reviewTargets.mjs +50 -0
  49. package/bin/lib/robloxReview.mjs +234 -0
  50. package/bin/lib/robloxStudioBridge.mjs +68 -0
  51. package/bin/lib/runTurn.mjs +30 -8
  52. package/bin/lib/runtimeClaude.mjs +7 -3
  53. package/bin/lib/runtimeCodex.mjs +12 -6
  54. package/bin/lib/runtimeLimits.mjs +59 -1
  55. package/bin/lib/runtimeReview.mjs +185 -0
  56. package/bin/lib/splitGate.mjs +115 -0
  57. package/bin/lib/subagentBounds.mjs +26 -0
  58. package/bin/lib/subagentLedger.mjs +110 -0
  59. package/bin/lib/trace.mjs +134 -6
  60. package/bin/lib/turnDeclaration.mjs +4 -4
  61. package/bin/lib/turnGaps.mjs +6 -2
  62. package/bin/lib/turnSubagents.mjs +23 -9
  63. package/bin/lib/uiReview.mjs +5 -2
  64. package/bin/lib/wikiDraft.mjs +9 -0
  65. package/bin/lib/work.mjs +22 -1
  66. package/bin/lib/workAgentCheck.mjs +24 -1
  67. package/bin/lib/workAgentResume.mjs +11 -2
  68. package/bin/lib/workAgentTools.mjs +6 -4
  69. package/bin/lib/workAgentTurnExecution.mjs +77 -9
  70. package/bin/lib/workAgentTurnOutcome.mjs +11 -0
  71. package/bin/lib/workAgentTurnReports.mjs +5 -2
  72. package/package.json +1 -1
package/README.md CHANGED
@@ -200,6 +200,370 @@ On Claude Code and Codex build turns, `card_ui_review.inspect_screenshot` can su
200
200
 
201
201
  The agent can fix in-scope findings and rerun up to three attempts per turn, each capped at two minutes including startup. Every attempt stops its owned browser and preview, including on cancellation; teardown also tracks their process groups. Reports and screenshots use `.flowviant/artifacts/`, and the existing delivery Verification retains routes, viewports, actions/outcomes, scrubbed diagnostics and source identity. Tracked/nonignored file bytes and HEAD are remeasured at delivery; later changes mark evidence stale. Ignored build/runtime files are outside that source measurement. The project check, fresh read-only pre-review, human approval and merge remain separate.
202
202
 
203
+ ## Isolated review environments
204
+
205
+ Code agents can provision disposable databases natively. This is opt-in: commit
206
+ `.flowviant/environment.json` on the reviewed base branch. Projects without a
207
+ recipe keep their existing setup. This layer works independently of the model
208
+ and whether the project has a browser preview.
209
+
210
+ For PostgreSQL, use a **dedicated development/review server** with `psql` on the
211
+ daemon's PATH. The administrative role needs permission to create/drop databases,
212
+ create restricted login roles and assign database ownership. Flowviant creates
213
+ an empty database from `template0` and a distinct role for each worktree. It
214
+ does not clone the contents of a shared development or production database.
215
+
216
+ ```json
217
+ {
218
+ "version": 1,
219
+ "database": {
220
+ "provider": "postgres",
221
+ "adminUrlEnv": "REVIEW_DATABASE_ADMIN_URL",
222
+ "bindings": ["DATABASE_URL", "DIRECT_DATABASE_URL"]
223
+ },
224
+ "migrate": ["npm", "run", "db:migrate"],
225
+ "seed": ["npm", "run", "db:seed"],
226
+ "envFiles": [".env.local"],
227
+ "env": {
228
+ "NODE_ENV": "development",
229
+ "CACHE_PREFIX": "review_${ENVIRONMENT_ID}"
230
+ },
231
+ "connectors": ["database"],
232
+ "ttlHours": 24
233
+ }
234
+ ```
235
+
236
+ Use this project's actual migration/fixture scripts and connector names. Commands
237
+ are argv arrays, run without a shell. The migration runner and fixture seeder
238
+ must tolerate retries after partial failure. Each listed project MCP connector
239
+ must reference one of the database bindings, for example `${DATABASE_URL}` in
240
+ its environment configuration. Flowviant expands it from the same scoped values
241
+ the app receives. Unlisted/personal connectors are not rewritten or isolated.
242
+
243
+ Machine-local values belong in the gitignored
244
+ `.flowviant/environment.local.json`, with mode `0600`, or the admin variable can
245
+ be supplied by the daemon's environment. For example:
246
+
247
+ ```json
248
+ {
249
+ "env": {
250
+ "REVIEW_DATABASE_ADMIN_URL": "postgresql://review_admin:LOCAL_PASSWORD@127.0.0.1:5432/postgres",
251
+ "PAYMENTS_API_KEY": "LOCAL_SANDBOX_KEY"
252
+ }
253
+ }
254
+ ```
255
+
256
+ This local file stays in the checkout. Its selected runtime values, excluding
257
+ provider administrator credentials, join the isolated database bindings. Generated
258
+ `.env`/`.dev.vars` files and local receipts are owner-only and excluded from Git.
259
+ Existing/tracked/user-edited env files and symlink destinations are preserved;
260
+ choose another path if one conflicts. Use `envFiles: []` for projects that do not
261
+ load dotenv files; the provided command wrapper injects values directly, including
262
+ complex multiline credentials. A recipe disables automatic copying of the
263
+ checkout's ignored `.env*`, including production profiles.
264
+
265
+ The first configured build allocates the database, runs the existing setup hook
266
+ or lockfile install once, migrates and seeds. Later cards reuse that worktree's
267
+ database, rerun migrations and retain successfully seeded data. The agent gets
268
+ an exact private helper command for `run -- <argv>` and `refresh`. The wrapper
269
+ selects the repository's own bindings even when a multi-repository place has
270
+ several environments. `refresh --seed` explicitly reruns fixtures. The local UI
271
+ self-review and final/combined-merge checks use scoped values too; combined
272
+ worktrees have their own databases and migrate their integrated source before
273
+ validation. These setup receipts do not count as passing application tests.
274
+
275
+ The daemon keeps local allocation intent before contacting a provider, so retries
276
+ reuse one environment id. Active turns/checks hold a heartbeat lease. Its sweep
277
+ releases inactive environments after `ttlHours` (1–168, default 24) or worktree
278
+ retirement; at most two receipts per repository per sweep. Shutdown retains them
279
+ for restart. Cleanup checks database/role ownership before deleting anything;
280
+ failures retain the receipt for retry. A reviewed recipe change rebuilds disposable
281
+ data after the previous environment is released. Moving a worktree retains its id.
282
+ The daemon must be running for this sweep; a remote provider adapter should also
283
+ set/renew its own expiration as protection while the machine is offline.
284
+
285
+ To use a database provider's branches, replace the `database` object with:
286
+
287
+ ```json
288
+ {
289
+ "provider": "command",
290
+ "command": ["node", ".flowviant/database.mjs"],
291
+ "credentials": ["DATABASE_PROVIDER_API_KEY"],
292
+ "source": "SANITIZED_DEVELOPMENT_BRANCH_ID",
293
+ "bindings": ["DATABASE_URL", "DIRECT_DATABASE_URL"]
294
+ }
295
+ ```
296
+
297
+ This is the native provider-adapter lifecycle, not an AI-generated provisioner
298
+ on every card. The reviewed adapter receives `allocate` or `release` as its last
299
+ argument, with the environment id, original test-data source, known resource id
300
+ and expiration in `FLOWVIANT_ENVIRONMENT_ID`, `FLOWVIANT_ENVIRONMENT_SOURCE`,
301
+ `FLOWVIANT_ENVIRONMENT_RESOURCE_ID` and `FLOWVIANT_ENVIRONMENT_EXPIRES_AT`.
302
+ Stdin also carries JSON `{id, resourceId, source}`. Only the named provider
303
+ credentials and minimal toolchain environment are passed. Allocate must look up
304
+ the original id before creating, renew its expiration and verify ownership;
305
+ a create-success/lost-response retry must never create another branch. It returns
306
+ JSON `{owner, resourceId, connectionString}`. Release must verify ownership and
307
+ reconcile by id even if allocation never returned a resource id, then return
308
+ `{owner}`. Both operations must be idempotent. Output is private and never included
309
+ in error messages. Repository adapter files named with `./` or `.flowviant/`
310
+ must still match the saved reviewed blobs, including during cleanup; keep that
311
+ adapter available until its resources are released. No commercial branch SDK
312
+ or provider credentials are installed automatically.
313
+
314
+ An allocate response may also return `bindings`, keyed only by declared database
315
+ variable names, when the provider needs distinct pooled and direct URLs for the
316
+ same branch. Other bindings use `connectionString`. The adapter must verify that
317
+ all of these connections target its owned environment.
318
+
319
+ Database isolation does not configure auth tenants, Redis, queues, object storage,
320
+ email or payment sandboxes. Select their development credentials explicitly and
321
+ namespace them with the environment id where supported. Apps and connector
322
+ processes still run as the user's UID: this is data separation, not a security
323
+ sandbox against repository code reading other local files. An inherited role's
324
+ access to unrelated databases is the PostgreSQL server's policy; use a dedicated
325
+ review server. See PostgreSQL's [database creation documentation](https://www.postgresql.org/docs/current/sql-createdatabase.html).
326
+
327
+ ## Runtime review adapters
328
+
329
+ Commit `.flowviant/review.json` on the project's reviewed base branch to expose
330
+ repeatable review workflows to code agents. One `card_runtime` interface supports
331
+ command checks, browser apps, Linux desktops, Roblox Studio and native/engine bridges. Projects
332
+ without targets mount no additional tools or processes. The agent's existing
333
+ shell and browser workflows remain available.
334
+
335
+ ```json
336
+ {
337
+ "version": 1,
338
+ "targets": {
339
+ "checks": {
340
+ "adapter": "command",
341
+ "command": ["npm", "test"],
342
+ "timeoutSeconds": 120
343
+ },
344
+ "web": {
345
+ "adapter": "browser",
346
+ "command": ["npm", "run", "dev", "--", "--port", "${PORT}"],
347
+ "port": 0,
348
+ "fixture": "Disposable seeded review database and test account"
349
+ },
350
+ "desktop": {
351
+ "adapter": "desktop",
352
+ "platforms": ["linux"],
353
+ "command": ["npm", "run", "desktop:dev"],
354
+ "fixture": "Disposable local workspace and sandbox service bindings"
355
+ }
356
+ }
357
+ }
358
+ ```
359
+
360
+ Use the repository's real commands. A Roblox repository can declare command
361
+ targets for its configured Luau analyzer, standalone logic tests or Rojo build;
362
+ these results do not claim a Studio playtest. The adapters do not install tools
363
+ or infer gameplay from a sync server's listening port. `cwd` defaults to the
364
+ repository root and must resolve inside that worktree. Commands are argv arrays,
365
+ without shell expansion. Timeouts are 1–600 seconds. Browser `port: 0` allocates
366
+ a loopback port and substitutes only `${PORT}`; a fixed port is also supported.
367
+ The listener is attributed to the target worktree before Chrome opens it.
368
+ The runtime MCP server gives Codex a longer tool-call timeout in its private
369
+ build config so the CLI's default does not truncate a configured command.
370
+
371
+ `discover` reports targets and actual prerequisites without launching apps.
372
+ `run` executes a named target; command results retain exit status, bounded logs
373
+ and source identity. A large log stays in the evidence artifact while only its
374
+ tail enters the model context. Browser/desktop/engine targets use the existing
375
+ Live app frame, private sign-in and take/return/stop control. `observe` retains
376
+ the actual screenshot, `act` supplies bounded input, and `handoff` holds the turn
377
+ without model polling. Screenshots and interactions are observations, not passing
378
+ tests. Source changes invalidate earlier command evidence. The shell helper
379
+ supplied to runners without MCP omits image base64 from text output; the model
380
+ opens the retained screenshot with its own image tool if available.
381
+
382
+ Only an interactive target launch creates a browser/display or stream. One
383
+ interactive surface occupies the frame per held turn. Native screenshots run at
384
+ a bounded low cadence, with fresh captures after input; the stream invokes no
385
+ model per frame. Adapter-owned app/process groups stop on explicit runtime Stop,
386
+ target replacement, timeout, abort and turn teardown. The browser frame's Stop
387
+ closes its browser; its supervised server also closes when the runtime tool or
388
+ turn closes. A person's Stop prevents reopening an interactive surface in that
389
+ turn. Commands can run alongside an existing surface without replacing its
390
+ owned server. Artifacts use the existing delivery/storage path.
391
+
392
+ Command checks inherit the normal unattended build environment without the
393
+ machine credential. Interactive apps/helpers start from the minimal toolchain
394
+ environment plus scoped review-environment bindings. A target's optional `env`
395
+ array selects additional variables by name, e.g. `["SANDBOX_API_KEY"]`; values
396
+ stay local and scoped database bindings take precedence. Provider administrator
397
+ credentials remain excluded. Personal/unlisted connectors and other external
398
+ services keep their explicit configuration.
399
+
400
+ ### Linux virtual desktops
401
+
402
+ The first native backend requires Xvfb, xauth, Openbox, xdotool and ImageMagick's
403
+ `import` executable. On Debian/Ubuntu these are supplied by the `xvfb`, `xauth`,
404
+ `openbox`, `xdotool` and `imagemagick` packages. Install them in the daemon's VM
405
+ image; missing tools are reported on the target. The runner never installs them
406
+ automatically or falls back to the user's display. Chrome headless itself does
407
+ not need these desktop packages or a visible desktop session.
408
+
409
+ The backend atomically allocates a private X display, disables its TCP listener
410
+ and creates an owner-only Xauthority cookie. It starts a window manager without
411
+ session autostart, then the declared app, waits for a visible client window and
412
+ focuses it. The app has a fresh home/config/runtime directory and private X11
413
+ display; inherited Wayland/WSLg and personal D-Bus sessions are not used. If an
414
+ app needs D-Bus, use a repository-supported `dbus-run-session -- ...` command.
415
+ Capture/input helpers receive only that display's authority. Handoff drains
416
+ capture work before resuming public frames. Natural app exit ends the surface;
417
+ crashes/timeouts report their actual failure. Teardown kills owned groups and
418
+ removes the temporary home and authority files.
419
+
420
+ The Linux backend runs behind the same stdio bridge used by other host helpers.
421
+ It owns a separate process group and watches its parent pipe: a daemon killed
422
+ without graceful shutdown closes that pipe and the helper reaps its native
423
+ descendants. Normal bridge close first allows cleanup on EOF, with a bounded
424
+ force-stop grace for a stalled helper.
425
+
426
+ V1 supports left clicks, text, the shared bounded navigation keys and vertical
427
+ scrolling at 1440×900. It does not yet provide drag, key holds, sound or native
428
+ accessibility inspection. Apps needing Wayland-only APIs or GPU acceleration
429
+ need an appropriate host/backend; an Xvfb image is not proof those capabilities
430
+ exist. The session isolates display/cookies and avoids accidental personal
431
+ desktop access, but still runs as the operator's UID. Use a dedicated VM/container
432
+ for execution containment and scoped service credentials for data isolation.
433
+
434
+ ### Native and engine bridges
435
+
436
+ A target with `adapter: "bridge"` uses the same UI and control ownership. Its
437
+ `command` starts a reviewed host helper over private newline-delimited JSON-RPC
438
+ 2.0 stdio; `platforms` can restrict where it runs. `configuration` is a bounded
439
+ object passed to the helper alongside the actual `worktree`. Repository helper
440
+ paths spelled `./...` or `.flowviant/...` must match their reviewed base blobs;
441
+ the helper launches from the checkout, with the target worktree in its scope.
442
+
443
+ The protocol methods are `initialize`, `open`, `snapshot`, `act` and `privacy`.
444
+ Initialize receives `{protocol:1, worktree, width:1440, height:900, configuration}`
445
+ and returns `{protocol:1, isolation:"owned-session", privateHandoff:true}` or
446
+ `isolation:"consented-window"`. It must bind that owned session or an explicitly
447
+ consented window. Open receives `{epoch:0}`. Snapshot receives the current epoch
448
+ and returns `{epoch,width:1440,height:900,data:"JPEG_BASE64",observation:"..."}`.
449
+ Act receives `{epoch,input}` with the shared bounded input verbs. Frame
450
+ notifications use method `frame` and params `{epoch,width,height,data}`. Frames
451
+ are bounded to one million base64 characters; no debugging socket is exposed.
452
+ An `ended` notification with the current epoch reports normal application exit;
453
+ `failure` with `{epoch,message}` reports an actual backend error. Helpers must
454
+ release their owned children when stdin closes and when terminated.
455
+ Privacy receives `{private,epoch}` and must drain old captures before replying
456
+ `{epoch,drained:true}`. The broker discards every old-epoch frame. Wrong ownership,
457
+ missing private fences, malformed messages/images and timeouts close the helper.
458
+
459
+ This contract is the extension point for Windows/macOS desktop capture and specialized engines;
460
+ general Windows/macOS desktop capture helpers are not included in this release. Windows capture would
461
+ use an explicitly selected window through Windows Graphics Capture; macOS would
462
+ use ScreenCaptureKit and native input/accessibility permissions. Linux Wayland
463
+ hosts would use their consented portal/PipeWire path. Roblox Studio requires a
464
+ Windows or Mac host; Linux can still edit and check its filesystem sources.
465
+ Engine-specific play/start/assert tools can use project MCP or command targets;
466
+ declaring a bridge alone does not implement those engine semantics. The built-in
467
+ Roblox adapter below uses Studio's own MCP rather than OS desktop capture.
468
+
469
+ ### Roblox Studio through Windows + WSL
470
+
471
+ Use `adapter: "roblox"` for a Windows Studio fixture reached through a reviewed
472
+ stdio launcher in WSL. Studio must already be installed, signed in, and have its
473
+ built-in MCP server enabled. The daemon needs native Linux headless Chromium and
474
+ `flock`; it creates no personal browser window or Windows daemon. No model service
475
+ is added. [Roblox's MCP guide](https://create.roblox.com/docs/studio/mcp) describes
476
+ the built-in server and its available tools.
477
+
478
+ ```json
479
+ {
480
+ "version": 1,
481
+ "targets": {
482
+ "studio": {
483
+ "adapter": "roblox",
484
+ "command": ["./scripts/roblox-studio-mcp"],
485
+ "env": ["ROBLOX_STUDIO_MCP", "WSL_INTEROP", "WSL_DISTRO_NAME"],
486
+ "fixture": "Disposable local place with test services and worktree scripts",
487
+ "configuration": {
488
+ "studioName": "${WORKTREE}.rbxl",
489
+ "startPlay": true,
490
+ "scripts": [
491
+ {
492
+ "file": "src/server/Main.server.luau",
493
+ "path": ["ServerScriptService", "Main"]
494
+ }
495
+ ]
496
+ }
497
+ }
498
+ }
499
+ }
500
+ ```
501
+
502
+ Commit the executable launcher on the reviewed base. For an existing Windows
503
+ StudioMCP executable, a small launcher can use the daemon's selected
504
+ `ROBLOX_STUDIO_MCP` environment variable (a WSL `/mnt/c/.../StudioMCP.exe` path):
505
+
506
+ ```bash
507
+ #!/usr/bin/env bash
508
+ set -euo pipefail
509
+ exec "${ROBLOX_STUDIO_MCP:?Set the installed StudioMCP.exe path on this machine}"
510
+ ```
511
+
512
+ Reuse a working launcher if already configured. Keep stdin/stdout attached to the
513
+ MCP process, without shell banners or `start`; do not launch Studio itself from
514
+ this wrapper. The adapter validates the reviewed launcher bytes before running
515
+ it from the checkout. Machine-specific paths stay in the selected environment.
516
+
517
+ Build/sync a disposable place from the task worktree using the project's actual
518
+ Rojo or Studio workflow, then open it in Windows Studio. `studioName` must equal
519
+ the **exact name returned by `list_roblox_studios`**; `${WORKTREE}` expands to the
520
+ task worktree's folder name. No first-instance or place-id fallback is used.
521
+ In that fixture set the `FlowviantReviewWorktree` attribute on ReplicatedStorage
522
+ to the same folder name, for example in Studio's Command Bar:
523
+
524
+ ```lua
525
+ game:GetService("ReplicatedStorage"):SetAttribute("FlowviantReviewWorktree", "a-TASK-WORKTREE-ID")
526
+ ```
527
+
528
+ The source mappings use worktree-relative Lua/Luau filenames and explicit Studio
529
+ instance path segments. At most forty scripts, each at most 128 KiB, are compared
530
+ before input or play starts; CRLF/LF differences are normalized. Missing,
531
+ ambiguous, escaping or stale mappings and a wrong worktree marker refuse that
532
+ review. Unmapped scripts, assets and other fixtures remain unverified. A marker
533
+ is an explicit fixture binding, not a sandbox or proof of automatic Rojo sync.
534
+
535
+ `run` requires an unambiguous Edit state before starting play and waits for a
536
+ Client player/camera. A pre-existing play session is left alone; `startPlay:false`
537
+ attaches to an already running fixture and never stops that session. A local OS
538
+ lock prevents simultaneous turns using the same fixture name in that daemon
539
+ environment. This does not coordinate other WSL distros or independent MCP clients.
540
+ Owned play receives a temporary `FlowviantReviewSession` Client attribute; the
541
+ adapter rechecks it before input and cleanup, leaving a replacement play session
542
+ alone. This marker is runtime state, not a saved/published place edit.
543
+
544
+ Viewport captures, click/text/navigation-key input, Take control and Return use
545
+ the existing Live app frame. PNG/JPEG images are normalized in a network-disabled
546
+ headless canvas, letterboxed to 1440×900, with click coordinates mapped to the
547
+ measured player viewport. Mismatched aspect ratios are refused. The two-second
548
+ capture cadence makes no model calls. Screenshots and input are observations;
549
+ they do not assert gameplay passed. The first version uses the shared bounded
550
+ input vocabulary, without WASD/key holds, mouse drag or full Studio editor control.
551
+
552
+ Use privately fences old captures; console history is omitted after any private
553
+ control. Studio's Windows login/MFA dialogs are outside the viewport and must be
554
+ completed on the Windows host. Stop review leaves the Studio editor open. The
555
+ helper watches parent EOF and on normal Stop, abort or daemon death attempts to
556
+ stop only play it started, with bounded cleanup time before its processes are
557
+ forced down. A disconnected/unresponsive MCP server can prevent that final stop;
558
+ the editor and any remaining play session stay on the Windows host.
559
+
560
+ Studio must already have disposable/namespaced DataStores and sandbox service
561
+ bindings. Flowviant's database environment does not rewrite Roblox DataStore,
562
+ Open Cloud, HTTP or game-universe configuration. This adapter does not install
563
+ Studio, publish a place, upload assets, invoke Roblox's AI subagents, import
564
+ personal auth or silently select a live game. Without Studio MCP, Lua changes
565
+ still produce the usual code diff and configured command-check evidence.
566
+
203
567
  ## Modes
204
568
 
205
569
  | Env | What runs |
package/bin/cli.mjs CHANGED
@@ -80,6 +80,12 @@
80
80
  // answered here before anything else loads: no config, no update check, no
81
81
  // output but the hook's. An unknown hook exits 2, which Claude Code reads as a
82
82
  // refusal — the read guard's own rule for a hook that cannot judge.
83
+ if (process.argv[2] === '__environment') {
84
+ await (await import('./lib/reviewEnvironmentMain.mjs')).reviewEnvironmentMain();
85
+ await new Promise(resolve => process.stdout.write('', resolve));
86
+ process.exit(process.exitCode ?? 0);
87
+ }
88
+
83
89
  if (process.argv[2] === '__capture-mcp') {
84
90
  await (await import('./lib/captureAuthority.mjs')).captureMcpMain();
85
91
  process.exit(process.exitCode ?? 0);
@@ -98,6 +104,23 @@ if (process.argv[2] === '__managed-browser-mcp') {
98
104
  process.exit(0);
99
105
  }
100
106
 
107
+ if (['__runtime-review', '__runtime-review-mcp'].includes(process.argv[2])) {
108
+ await (await import('./lib/runtimeReview.mjs')).runtimeReviewMain(process.argv[2] === '__runtime-review-mcp');
109
+ await new Promise(resolve => process.stdout.write('', resolve));
110
+ process.exit(process.exitCode ?? 0);
111
+ }
112
+
113
+ if (process.argv[2] === '__linux-desktop-bridge') {
114
+ await (await import('./lib/linuxDesktopBridge.mjs')).linuxDesktopBridgeMain();
115
+ process.exit(process.exitCode ?? 0);
116
+ }
117
+
118
+ if (process.argv[2] === '__roblox-studio-bridge') {
119
+ await (await import('./lib/robloxStudioBridge.mjs')).robloxStudioBridgeMain();
120
+ await new Promise(resolve => process.stdout.write('', resolve));
121
+ process.exit(process.exitCode ?? 0);
122
+ }
123
+
101
124
  if (process.argv[2] === '__video-render-mcp') {
102
125
  await (await import('./lib/videoRender.mjs')).videoRenderMain();
103
126
  process.exit(process.exitCode ?? 0);
@@ -33,6 +33,7 @@ import { scrub as envScrub, scrubCut } from './uplinkScrub.mjs';
33
33
  import { groupDependencies } from './planDependencies.mjs';
34
34
  import { AGENT_TASK_KINDS } from './agentTaskKinds.mjs';
35
35
  import { blockedAnswer } from './agentAsk.mjs';
36
+ import { splitAnswer } from './agentSplit.mjs';
36
37
  import { isResearchUse } from './researchUse.mjs';
37
38
  import { candidateObjects } from './modelJson.mjs';
38
39
  import { parseGaps } from './turnGaps.mjs';
@@ -258,6 +259,13 @@ export function parseTurnResult(text, scrub = envScrub) {
258
259
  // as plain text.
259
260
  return { outcome: 'question', answer: blockedAnswer(scrub(question), v, scrub), ...progressOf(v, scrub) };
260
261
  }
262
+ if (v.status === 'split') {
263
+ // A split proposal (2026-11-06, agentSplit.mjs) is a question: the agent
264
+ // freezes until the person answers Use N subagents or Do it yourself.
265
+ const answer = splitAnswer(v, scrub);
266
+ if (!answer) continue;
267
+ return { outcome: 'question', answer, ...progressOf(v, scrub) };
268
+ }
261
269
  if (v.status === 'delivered') {
262
270
  return {
263
271
  outcome: 'delivered',
@@ -17,6 +17,12 @@ requirements. The brief and criteria marked full are supplied without clipping.
17
17
  sourceChats are the person's exact words in the New task chat these cards were
18
18
  planned in, up to each card's landing; a brief is the planner's paraphrase of
19
19
  them, and links, references and file lists there are part of the request.
20
+ ${job.gapContinuationAccepted === true ? 'This server supports disposition=required: unfinished required work continues in this card before Review.' : 'This server has not advertised automatic gap continuation. Finish required work within this turn; if it remains unfinished, ask through a blocked reply instead of declaring delivery.'}
21
+ relatedWork names sibling cards from the same request across agents, including
22
+ completed cards. Outside this queue does not mean unowned. Link the actual
23
+ ownerTaskId instead of proposing or implementing a sibling's work again.
24
+ Only a card's observed completion is recorded; verify a reported recurrence
25
+ instead of assuming an earlier Done card solved the current failure.
20
26
  Human messages are exact recorded words across this agent, with the preceding
21
27
  question when present. taskId on a message is where it was recorded, not an
22
28
  implicit scope restriction. target.kind=agent explicitly addresses the whole
@@ -0,0 +1,90 @@
1
+ /**
2
+ * CLAUDE SUGGESTS SUBAGENTS (2026-11-06, D28–D31, epic E5): the split a card
3
+ * agent proposes instead of starting children that edit files, read off its
4
+ * final JSON and handed on as a QUESTION.
5
+ *
6
+ * {"status": "split", "split": {"reason": "…", "parts": [
7
+ * {"title": "Port gizmo shaders", "touches": ["src/gizmo.ts"], "edits": true},
8
+ * {"title": "Audit textures", "touches": ["assets/**"], "edits": false}]},
9
+ * "progress": "…"}
10
+ *
11
+ * The agent freezes on it as a native CLI freezes on its ask tool (D31): the
12
+ * turn settles `question`, the board says Needs you, and the person answers
13
+ * through the existing answer lane. So the answer is built as every blocked
14
+ * question's is (agentAsk.mjs): prose, then ONE ```flowviant-ask fence — here
15
+ * with the two options the server and the page key on, `Use N subagents` and
16
+ * `Do it yourself` (the shared `splitUseLabel` / `SPLIT_SOLO_LABEL`), and the
17
+ * parts under an extra `split` key the ask reader ignores. A screen that
18
+ * knows nothing of splits still draws an answerable question.
19
+ *
20
+ * BOUNDED, AND NEVER LOST. `SPLIT_BOUNDS` is the shared one, key for key. A
21
+ * proposal past any bound, or with a hostile character, loses its fence whole
22
+ * and goes on as a plain-text question listing what could be read, exactly as
23
+ * malformed options do: the person can still answer it in words.
24
+ */
25
+ import { ASK_HOSTILE } from './agentAsk.mjs';
26
+
27
+ /** The shared `SPLIT_BOUNDS` (packages/shared/src/schemas/subagentSplit.ts). */
28
+ export const SPLIT_BOUNDS = Object.freeze({ parts: 12, title: 120, touches: 8, touch: 200, reason: 600 });
29
+ export const SPLIT_SOLO_LABEL = 'Do it yourself';
30
+ export const splitUseLabel = n => `Use ${n} subagent${n === 1 ? '' : 's'}`;
31
+ export const SPLIT_QUESTION = 'Use subagents?';
32
+
33
+ const one = (v, max, scrub) => {
34
+ if (typeof v !== 'string') return null;
35
+ const t = scrub(v).trim();
36
+ return t && t.length <= max && !ASK_HOSTILE.test(t) ? t : null;
37
+ };
38
+
39
+ /** The declared split → `{ parts, reason }`, or null when any part breaks a
40
+ * bound. `scrub` runs before each field is measured. */
41
+ export function declaredSplit(raw, scrub = s => s) {
42
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
43
+ if (!Array.isArray(raw.parts) || !raw.parts.length || raw.parts.length > SPLIT_BOUNDS.parts) return null;
44
+ const parts = [];
45
+ for (const p of raw.parts) {
46
+ if (!p || typeof p !== 'object' || Array.isArray(p) || typeof p.edits !== 'boolean') return null;
47
+ const title = one(p.title, SPLIT_BOUNDS.title, scrub);
48
+ const touches = p.touches === undefined ? [] : p.touches;
49
+ if (!title || !Array.isArray(touches) || touches.length > SPLIT_BOUNDS.touches) return null;
50
+ const kept = [];
51
+ for (const t of touches) { const k = one(t, SPLIT_BOUNDS.touch, scrub); if (!k) return null; kept.push(k); }
52
+ parts.push({ title, touches: kept, edits: p.edits });
53
+ }
54
+ let reason = null;
55
+ if (raw.reason !== undefined && raw.reason !== null) {
56
+ if (typeof raw.reason !== 'string') return null;
57
+ const r = scrub(raw.reason).trim();
58
+ if (r.length > SPLIT_BOUNDS.reason || ASK_HOSTILE.test(r.replace(/\n/g, ' '))) return null;
59
+ reason = r || null;
60
+ }
61
+ return { parts, reason };
62
+ }
63
+
64
+ const partLine = (p, i) => `${i + 1}. ${p.title}${p.touches.length ? ` — ${p.touches.join(', ')}` : ''} · ${p.edits ? 'edits' : 'read-only'}`;
65
+
66
+ /**
67
+ * THE ANSWER A SPLIT TURN HANDS ON, or null when it declared no parts at all
68
+ * (then it is no answer, as a blocked turn with no question is not). The
69
+ * prose names the parts, so a surface that draws no fence still shows them.
70
+ */
71
+ export function splitAnswer(v, scrub = s => s) {
72
+ const split = declaredSplit(v?.split, scrub);
73
+ if (!split) {
74
+ // Readable parts, past a bound: a plain question, answerable in words.
75
+ const titles = Array.isArray(v?.split?.parts) ? v.split.parts.map(p => typeof p?.title === 'string' ? scrub(p.title).trim().slice(0, SPLIT_BOUNDS.title) : '').filter(Boolean) : [];
76
+ if (!titles.length) return null;
77
+ return `${SPLIT_QUESTION} I would split this card into ${titles.length} parts:\n\n${titles.map((t, i) => `${i + 1}. ${t}`).join('\n')}`;
78
+ }
79
+ const n = split.parts.length;
80
+ const prose = `${split.reason ? `${split.reason}\n\n` : ''}${split.parts.map(partLine).join('\n')}\n\n${SPLIT_QUESTION}`;
81
+ const ask = {
82
+ question: SPLIT_QUESTION, header: 'Subagents',
83
+ options: [
84
+ { label: splitUseLabel(n), description: n === 1 ? 'Run this part as a subagent.' : 'Run the parts side by side.' },
85
+ { label: SPLIT_SOLO_LABEL, description: 'Claude does every part itself, one after another.' },
86
+ ],
87
+ split,
88
+ };
89
+ return `${prose}\n\n\`\`\`flowviant-ask\n${JSON.stringify(ask)}\n\`\`\``;
90
+ }
@@ -0,0 +1,57 @@
1
+ /** Generated by the existing vault-scoped reader; no new infrastructure access. */
2
+ export const ARCHITECTURE_EXAMPLE = {
3
+ version: 1,
4
+ summary: 'A browser application calls an API. Production deployment is unknown.',
5
+ components: [
6
+ { id: 'web', name: 'Web app', kind: 'application', description: 'The browser interface.', evidence: 'confirmed', sources: [{ path: 'web/package.json', reason: 'Declares the browser application.' }] },
7
+ { id: 'api', name: 'API', kind: 'service', description: 'Handles application requests.', evidence: 'confirmed', sources: [{ path: 'api/server.ts', line: 1, reason: 'Defines the HTTP entry point.' }] },
8
+ ],
9
+ connections: [{ from: 'web', to: 'api', label: 'HTTP requests', evidence: 'confirmed', sources: [{ path: 'web/client.ts', reason: 'Calls the API base URL.' }] }],
10
+ unknowns: ['The repository does not establish which services are deployed.'],
11
+ };
12
+
13
+ export const SYSTEM_ARCHITECTURE_CONTRACT = `
14
+ UNDERSTAND MY SYSTEM — docs/01-architecture.md also supplies the Architecture
15
+ view. Full sweeps generate the report. Incremental re-grounds maintain it only
16
+ when architecture-affecting changes are in scope; never expand an unrelated
17
+ incremental pass into a full scan just to add this report. If no report exists
18
+ and this pass cannot inspect the system, leave it absent. Inspect real entry
19
+ points, workspace/package manifests, API clients,
20
+ database/storage/queue adapters and deployment configuration before writing it.
21
+ Map major applications, services, data stores, queues and external systems;
22
+ do not turn every file, dependency or function into a node.
23
+
24
+ Include exactly ONE fenced block tagged flowviant-architecture containing JSON
25
+ in this version-1 format (this example is FORMAT ONLY, never project facts):
26
+ \n\`\`\`flowviant-architecture
27
+ ${JSON.stringify(ARCHITECTURE_EXAMPLE, null, 2)}
28
+ \`\`\`
29
+
30
+ Kinds: application, service, database, queue, storage, external. Component ids
31
+ are unique ASCII letters/digits/underscore/hyphen, starting with a letter or
32
+ digit, at most 80 characters. Connections reference those ids. Every component
33
+ and connection needs at least one source: a real repository-relative path you
34
+ opened, an optional positive line number and a reason explaining the evidence.
35
+ Never emit absolute paths, traversal or URLs as source paths. Confirmed means
36
+ source explicitly establishes the claim; inferred means an interpretation, and
37
+ the source reason must explain it. Neither means running, healthy or deployed.
38
+
39
+ State scope in the summary: only this checked-out repository is inspected;
40
+ other repositories, deployment state and external settings remain unknown unless
41
+ actually inspected. Never imply project-wide or production completeness. Put
42
+ unanswered questions and uninspected areas in unknowns. An empty/new repository
43
+ has components: [], connections: [] and an explanation in unknowns; do not invent
44
+ a proposed architecture. Never expose credentials, environment values, private
45
+ hosts or secret material. Do not contact infrastructure, provision anything,
46
+ install dependencies or run the application to construct this map.
47
+
48
+ At most 80 components, 200 connections, 40 sources per finding and 80 unknowns.
49
+ Names at most 160 characters, connection labels 200, source paths 1000 and
50
+ summary/descriptions/reasons/questions 4000. Keep the chapter's readable prose,
51
+ Mermaid overview, files: frontmatter and page-local provenance beside the report.
52
+ This is source analysis by the coding CLI, not independent verification by
53
+ Flowviant. Re-ground it when changed files affect system boundaries, config,
54
+ entry points or connections, including additions/removals outside its old files:
55
+ list. Re-read affected sources; retain unrelated findings without asserting a
56
+ new verified commit for unchecked claims. Respect human/reference ownership.
57
+ `;
@@ -0,0 +1,38 @@
1
+ /** Standalone daemon counterpart of shared/systemArchitecture.ts. The release
2
+ * parity gate checks the same accepted/rejected reports on both readers. */
3
+ export const SYSTEM_ARCHITECTURE_PATH = 'docs/01-architecture.md';
4
+ export const SYSTEM_ARCHITECTURE_FENCE = 'flowviant-architecture';
5
+ export const ARCHITECTURE_REPORT_DAEMON_VERSION = '0.125.31';
6
+
7
+ const text = (value, max = 4000) => typeof value === 'string' && value.trim().length > 0 && value.trim().length <= max;
8
+ const id = (value) => typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$/.test(value);
9
+ const source = (value) => value && typeof value.path === 'string' && value.path.length > 0 && value.path.length <= 1000 &&
10
+ !/^[\/\\]|[\\\x00-\x1f\x7f:#?]/.test(value.path) && value.path.split('/').every((part) => part && part !== '.' && part !== '..') &&
11
+ (value.line === undefined || (Number.isInteger(value.line) && value.line > 0)) && text(value.reason);
12
+ const finding = (value) => value && ['confirmed', 'inferred'].includes(value.evidence) &&
13
+ Array.isArray(value.sources) && value.sources.length > 0 && value.sources.length <= 40 && value.sources.every(source);
14
+
15
+ export function validArchitectureReport(value) {
16
+ if (!value || value.version !== 1 || !text(value.summary) || !Array.isArray(value.components) || value.components.length > 80 ||
17
+ !Array.isArray(value.connections) || value.connections.length > 200 || !Array.isArray(value.unknowns) || value.unknowns.length > 80 || !value.unknowns.every((question) => text(question))) return false;
18
+ const ids = new Set();
19
+ for (const component of value.components) {
20
+ if (!component || !id(component.id) || ids.has(component.id) || !text(component.name, 160) || !text(component.description) ||
21
+ !['application', 'service', 'database', 'queue', 'storage', 'external'].includes(component.kind) || !finding(component)) return false;
22
+ ids.add(component.id);
23
+ }
24
+ return (ids.size > 0 || value.unknowns.length > 0) && value.connections.every((connection) =>
25
+ connection && ids.has(connection.from) && ids.has(connection.to) && text(connection.label, 200) && finding(connection));
26
+ }
27
+
28
+ export function readSystemArchitecture(markdown) {
29
+ const blocks = [...markdown.matchAll(/^```flowviant-architecture[^\S\r\n]*\r?\n([\s\S]*?)^```[^\S\r\n]*\r?$/gm)];
30
+ if (blocks.length === 0) return { status: markdown.includes('```flowviant-architecture') ? 'invalid' : 'missing' };
31
+ if (blocks.length !== 1) return { status: 'invalid' };
32
+ try {
33
+ const report = JSON.parse(blocks[0][1]);
34
+ return validArchitectureReport(report) ? { status: 'available', report } : { status: 'invalid' };
35
+ } catch {
36
+ return { status: 'invalid' };
37
+ }
38
+ }