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.
- package/README.md +364 -0
- package/bin/cli.mjs +23 -0
- package/bin/lib/agentPlan.mjs +8 -0
- package/bin/lib/agentQueueContext.mjs +6 -0
- package/bin/lib/agentSplit.mjs +90 -0
- package/bin/lib/architectureContract.mjs +57 -0
- package/bin/lib/architectureReport.mjs +38 -0
- package/bin/lib/artifactContracts.mjs +21 -2
- package/bin/lib/claudeInput.mjs +1 -1
- package/bin/lib/claudeSubagents.mjs +46 -6
- package/bin/lib/claudeWorkflows.mjs +94 -0
- package/bin/lib/cliLimit.mjs +5 -0
- package/bin/lib/codexInput.mjs +191 -0
- package/bin/lib/codexSubagentEvents.mjs +3 -1
- package/bin/lib/codexSubagents.mjs +34 -9
- package/bin/lib/conversationOutput.mjs +14 -0
- package/bin/lib/crossCliChild.mjs +327 -0
- package/bin/lib/crossCliLaunch.mjs +92 -0
- package/bin/lib/crossCliPosture.mjs +55 -0
- package/bin/lib/crossCliResearch.mjs +19 -0
- package/bin/lib/environmentProcess.mjs +44 -0
- package/bin/lib/fixtures/workflow-ultracode.jsonl +22 -0
- package/bin/lib/fleet.mjs +2 -0
- package/bin/lib/linuxDesktopBridge.mjs +62 -0
- package/bin/lib/managedBrowser.mjs +50 -10
- package/bin/lib/mergeBatch.mjs +1 -1
- package/bin/lib/nativeAgentTools.mjs +12 -5
- package/bin/lib/noWindowEnv.mjs +4 -2
- package/bin/lib/orchestrator.mjs +24 -7
- package/bin/lib/orchestratorClient.mjs +8 -2
- package/bin/lib/orchestratorPrompt.mjs +13 -5
- package/bin/lib/orchestratorProtocol.mjs +18 -0
- package/bin/lib/outsideCli.mjs +124 -0
- package/bin/lib/projectToolRuntimes.mjs +4 -2
- package/bin/lib/projectTools.mjs +2 -2
- package/bin/lib/prompts.mjs +7 -5
- package/bin/lib/repositoryPlaces.mjs +1 -1
- package/bin/lib/repositorySetup.mjs +15 -24
- package/bin/lib/reviewBridge.mjs +110 -0
- package/bin/lib/reviewDatabase.mjs +112 -0
- package/bin/lib/reviewDesktop.mjs +153 -0
- package/bin/lib/reviewEnvironment.mjs +286 -0
- package/bin/lib/reviewEnvironmentConfig.mjs +104 -0
- package/bin/lib/reviewEnvironmentMain.mjs +38 -0
- package/bin/lib/reviewImage.mjs +58 -0
- package/bin/lib/reviewMcpClient.mjs +69 -0
- package/bin/lib/reviewProcess.mjs +51 -0
- package/bin/lib/reviewTargets.mjs +50 -0
- package/bin/lib/robloxReview.mjs +234 -0
- package/bin/lib/robloxStudioBridge.mjs +68 -0
- package/bin/lib/runTurn.mjs +30 -8
- package/bin/lib/runtimeClaude.mjs +7 -3
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeLimits.mjs +59 -1
- package/bin/lib/runtimeReview.mjs +185 -0
- package/bin/lib/splitGate.mjs +115 -0
- package/bin/lib/subagentBounds.mjs +26 -0
- package/bin/lib/subagentLedger.mjs +110 -0
- package/bin/lib/trace.mjs +134 -6
- package/bin/lib/turnDeclaration.mjs +4 -4
- package/bin/lib/turnGaps.mjs +6 -2
- package/bin/lib/turnSubagents.mjs +23 -9
- package/bin/lib/uiReview.mjs +5 -2
- package/bin/lib/wikiDraft.mjs +9 -0
- package/bin/lib/work.mjs +22 -1
- package/bin/lib/workAgentCheck.mjs +24 -1
- package/bin/lib/workAgentResume.mjs +11 -2
- package/bin/lib/workAgentTools.mjs +6 -4
- package/bin/lib/workAgentTurnExecution.mjs +77 -9
- package/bin/lib/workAgentTurnOutcome.mjs +11 -0
- package/bin/lib/workAgentTurnReports.mjs +5 -2
- 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);
|
package/bin/lib/agentPlan.mjs
CHANGED
|
@@ -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
|
+
}
|