flowviant 0.103.0 → 0.105.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/bin/cli.mjs +87 -474
- package/bin/lib/admission.mjs +19 -3
- package/bin/lib/agentPlan.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +130 -0
- package/bin/lib/agySessions.mjs +158 -0
- package/bin/lib/artifactContracts.mjs +153 -0
- package/bin/lib/artifactPolicy.mjs +45 -0
- package/bin/lib/artifactPreview.mjs +52 -0
- package/bin/lib/artifactReporter.mjs +163 -0
- package/bin/lib/artifacts.mjs +49 -228
- package/bin/lib/authReject.mjs +16 -4
- package/bin/lib/authproxy.mjs +47 -304
- package/bin/lib/boxIdentity.mjs +245 -0
- package/bin/lib/childEnv.mjs +11 -9
- package/bin/lib/claude.mjs +14 -851
- package/bin/lib/claudeAuth.mjs +3 -3
- package/bin/lib/claudePosture.mjs +370 -0
- package/bin/lib/claudeSessions.mjs +411 -0
- package/bin/lib/claudeStream.mjs +207 -0
- package/bin/lib/cliLimit.mjs +34 -0
- package/bin/lib/commandSpecs.mjs +173 -0
- package/bin/lib/config.mjs +11 -42
- package/bin/lib/credentialDate.mjs +22 -0
- package/bin/lib/credentials.mjs +21 -26
- package/bin/lib/daemonLogging.mjs +55 -0
- package/bin/lib/daemonState.mjs +64 -0
- package/bin/lib/deploy.mjs +148 -647
- package/bin/lib/deployCheckout.mjs +265 -0
- package/bin/lib/deployConfig.mjs +142 -0
- package/bin/lib/deployRunner.mjs +311 -0
- package/bin/lib/deployWire.mjs +41 -0
- package/bin/lib/desktopContract.mjs +56 -105
- package/bin/lib/env.mjs +49 -484
- package/bin/lib/fleet.mjs +183 -2383
- package/bin/lib/fleetJobs.mjs +120 -0
- package/bin/lib/fleetPost.mjs +40 -0
- package/bin/lib/fleetReports.mjs +392 -0
- package/bin/lib/fleetRoster.mjs +470 -0
- package/bin/lib/fleetStartup.mjs +196 -0
- package/bin/lib/fleetWake.mjs +45 -0
- package/bin/lib/fleetWire.mjs +46 -0
- package/bin/lib/git.mjs +2 -2
- package/bin/lib/gitMergeIdentity.mjs +51 -0
- package/bin/lib/grant.mjs +5 -3
- package/bin/lib/help.mjs +3 -125
- package/bin/lib/holder.mjs +482 -0
- package/bin/lib/instance.mjs +124 -46
- package/bin/lib/knowledge.mjs +16 -799
- package/bin/lib/knowledgeFetch.mjs +38 -0
- package/bin/lib/knowledgeFileSync.mjs +112 -0
- package/bin/lib/knowledgeFiles.mjs +489 -0
- package/bin/lib/knowledgeLibrary.mjs +299 -0
- package/bin/lib/landed.mjs +2 -1
- package/bin/lib/listeners.mjs +99 -141
- package/bin/lib/localSessions.mjs +30 -555
- package/bin/lib/login.mjs +3 -2
- package/bin/lib/loginPath.mjs +3 -5
- package/bin/lib/machineDisconnect.mjs +203 -0
- package/bin/lib/machineEnv.mjs +22 -0
- package/bin/lib/machineLimits.mjs +61 -0
- package/bin/lib/machineListing.mjs +338 -0
- package/bin/lib/machines.mjs +78 -455
- package/bin/lib/machinesCommand.mjs +214 -0
- package/bin/lib/mcp-cli.mjs +2 -1
- package/bin/lib/patchRevert.mjs +60 -0
- package/bin/lib/prWorkflow.mjs +157 -0
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/pressurePolicy.mjs +104 -0
- package/bin/lib/preview.mjs +2 -272
- package/bin/lib/previewFramePolicy.mjs +112 -0
- package/bin/lib/previewGatePolicy.mjs +236 -0
- package/bin/lib/previewRegistry.mjs +235 -0
- package/bin/lib/printable.mjs +2 -2
- package/bin/lib/procRegistry.mjs +28 -18
- package/bin/lib/procScan.mjs +146 -0
- package/bin/lib/processes.mjs +29 -37
- package/bin/lib/projectToolRuntimes.mjs +138 -0
- package/bin/lib/projectTools.mjs +13 -88
- package/bin/lib/prompts.mjs +105 -139
- package/bin/lib/repoPath.mjs +26 -0
- package/bin/lib/resources.mjs +7 -69
- package/bin/lib/resumeLost.mjs +107 -0
- package/bin/lib/runTurn.mjs +324 -0
- package/bin/lib/runtimeAntigravity.mjs +230 -0
- package/bin/lib/runtimeCapabilities.mjs +334 -0
- package/bin/lib/runtimeClaude.mjs +126 -0
- package/bin/lib/runtimeCodex.mjs +217 -0
- package/bin/lib/runtimeDetection.mjs +121 -0
- package/bin/lib/runtimeEvents.mjs +482 -0
- package/bin/lib/runtimeOfferings.mjs +4 -1
- package/bin/lib/runtimes.mjs +42 -1402
- package/bin/lib/safeFileName.mjs +74 -0
- package/bin/lib/standDownExit.mjs +181 -0
- package/bin/lib/standDownGate.mjs +48 -0
- package/bin/lib/startProject.mjs +279 -0
- package/bin/lib/trace.mjs +3 -3
- package/bin/lib/turnLock.mjs +68 -0
- package/bin/lib/turnProfile.mjs +77 -0
- package/bin/lib/uninstall.mjs +55 -45
- package/bin/lib/uninstallCopies.mjs +112 -0
- package/bin/lib/update.mjs +30 -17
- package/bin/lib/uplinkScrub.mjs +265 -0
- package/bin/lib/vault.mjs +22 -151
- package/bin/lib/vaultDiff.mjs +183 -0
- package/bin/lib/versionOrder.mjs +42 -0
- package/bin/lib/views.mjs +17 -17
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3034
- package/bin/lib/workAdoptCarry.mjs +94 -0
- package/bin/lib/workAgentCheck.mjs +278 -0
- package/bin/lib/workAgentMerges.mjs +54 -142
- package/bin/lib/workAgentPlans.mjs +14 -12
- package/bin/lib/workAgentPrecheck.mjs +393 -0
- package/bin/lib/workAgentPublish.mjs +177 -0
- package/bin/lib/workAgentReview.mjs +8 -666
- package/bin/lib/workAgentTurnBegun.mjs +109 -0
- package/bin/lib/workAgentTurnExecution.mjs +604 -0
- package/bin/lib/workAgentTurnOutcome.mjs +219 -0
- package/bin/lib/workAgentTurnReports.mjs +189 -0
- package/bin/lib/workAgentTurns.mjs +91 -898
- package/bin/lib/workAttachments.mjs +93 -0
- package/bin/lib/workBrain.mjs +53 -0
- package/bin/lib/workChildren.mjs +110 -0
- package/bin/lib/workCommandAudit.mjs +62 -0
- package/bin/lib/workDiffs.mjs +2 -1
- package/bin/lib/workNarration.mjs +152 -0
- package/bin/lib/workPlaceReports.mjs +426 -0
- package/bin/lib/workPlaces.mjs +228 -0
- package/bin/lib/workPreviews.mjs +31 -9
- package/bin/lib/workProcesses.mjs +4 -5
- package/bin/lib/workPullRequests.mjs +52 -126
- package/bin/lib/workReportQueue.mjs +254 -0
- package/bin/lib/workRetire.mjs +185 -0
- package/bin/lib/workSessionGroups.mjs +166 -0
- package/bin/lib/workSessionRuntime.mjs +164 -0
- package/bin/lib/workSessionTokens.mjs +68 -0
- package/bin/lib/workSessionTurns.mjs +892 -0
- package/bin/lib/workShip.mjs +9 -29
- package/bin/lib/workToolLog.mjs +76 -0
- package/package.json +3 -2
- package/bin/lib/patch.mjs +0 -363
package/bin/lib/admission.mjs
CHANGED
|
@@ -80,7 +80,23 @@
|
|
|
80
80
|
*/
|
|
81
81
|
|
|
82
82
|
import { MAX_CONCURRENT, MAX_CONCURRENT_FROM_ENV } from './config.mjs';
|
|
83
|
-
import {
|
|
83
|
+
import { measurePressure } from './resources.mjs';
|
|
84
|
+
import { pressureVerdict, readPressureThresholds } from './pressurePolicy.mjs';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* THE PRESSURE HALF OF AN ADMISSION, AS THE DAEMON ASKS IT: this box's
|
|
88
|
+
* (briefly cached) measurement, judged against the thresholds the environment
|
|
89
|
+
* states AT THIS CALL. Both are read per decision — an operator who exports
|
|
90
|
+
* `FLOWVIANT_MIN_FREE_MB` into a running daemon's environment is heard on the
|
|
91
|
+
* next spawn. Injectable only so that promise can be pinned; `createAdmission`
|
|
92
|
+
* passes nothing. A guard switched off measures nothing (no `vm_stat` spawn
|
|
93
|
+
* per decision on macOS), as before the split.
|
|
94
|
+
*/
|
|
95
|
+
export function livePressureVerdict(level, { measure = measurePressure, env = process.env } = {}) {
|
|
96
|
+
const thresholds = readPressureThresholds(env);
|
|
97
|
+
if (thresholds.off) return null;
|
|
98
|
+
return pressureVerdict(level, measure(), thresholds);
|
|
99
|
+
}
|
|
84
100
|
|
|
85
101
|
/**
|
|
86
102
|
* ── THE CEILING HAS THREE POSSIBLE AUTHORS (2026-09-17) ──
|
|
@@ -127,7 +143,7 @@ export function pickMaxTurns({ env = null, server = null, derived = 1 } = {}) {
|
|
|
127
143
|
* A module holder rather than a value threaded through `createWorkManager`,
|
|
128
144
|
* because the reader and the writer are two files apart and one poll apart: the
|
|
129
145
|
* POLL learns it (fleet.mjs) and every ADMISSION reads it, including the ones
|
|
130
|
-
* work
|
|
146
|
+
* the work manager takes in lanes fleet.mjs never sees (workChildren.mjs builds it). The alternative — passing it
|
|
131
147
|
* down — would mean the number that bound a spawn was whatever was current when
|
|
132
148
|
* the manager was CONSTRUCTED, which is exactly the frozen-at-startup bug
|
|
133
149
|
* `getBaseRef` and `getLeaseTtl` are getters to avoid.
|
|
@@ -171,7 +187,7 @@ export function createAdmission({
|
|
|
171
187
|
* A number is still accepted, and every test here passes one — the whole
|
|
172
188
|
* point of injecting it. */
|
|
173
189
|
maxConcurrent = effectiveMaxTurns,
|
|
174
|
-
verdict =
|
|
190
|
+
verdict = livePressureVerdict,
|
|
175
191
|
} = {}) {
|
|
176
192
|
/** Slots taken by a decision whose process does not exist yet. A Set of
|
|
177
193
|
* tokens rather than a counter: a double release then costs nothing, which
|
package/bin/lib/agentPlan.mjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
// THE UPLINK SCRUBBER, as the DEFAULT for the two readers whose every string
|
|
29
29
|
// goes to the server (2026-09-24, the audit). See `parseTurnResult`.
|
|
30
|
-
import { scrub as envScrub } from './
|
|
30
|
+
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
31
31
|
|
|
32
32
|
/** The biggest a single proposal may be. Bounds on a machine, not a policy:
|
|
33
33
|
* the server caps these again at its own boundary. */
|
|
@@ -277,7 +277,7 @@ function uniqueTempIds(list) {
|
|
|
277
277
|
* summary and the wire, and a cap must never run ahead of it. So this is the
|
|
278
278
|
* same absurdity bound `summary` takes one line up (8000): enough that nothing
|
|
279
279
|
* a model writes reaches the caller unbounded, far enough above the 1000 the
|
|
280
|
-
* wire cuts to that the scrub always sees whole values. `
|
|
280
|
+
* wire cuts to that the scrub always sees whole values. `workAgentTurns.mjs` scrubs and
|
|
281
281
|
* THEN cuts to 1000, and the server clamps again because the wire never trusts
|
|
282
282
|
* a machine to have clamped.
|
|
283
283
|
*/
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A CARD'S KIND — the daemon's one table of what each kind IS (0.97.0):
|
|
3
|
+
* `code` (the default), `design` and `research`; since 0.105.0 also `model`
|
|
4
|
+
* (the 3D-model kind — not an AI-model pin, which is `job.model`) and `deck`
|
|
5
|
+
* (the presentation kind — not the review deck). Every reader of a kind goes
|
|
6
|
+
* through this module: the prompts that print it and pick a contract
|
|
7
|
+
* (prompts.mjs, which keeps the strings, with the 3D-model and presentation
|
|
8
|
+
* contracts and the shared tail in artifactContracts.mjs), the agent lane
|
|
9
|
+
* that picks a posture,
|
|
10
|
+
* refuses a runtime (workAgentTurnExecution.mjs) and checks the artifact
|
|
11
|
+
* (workAgentTurnOutcome.mjs), and the turn runner that fences the non-code
|
|
12
|
+
* postures (runTurn.mjs and claudePosture.mjs, via turnProfile.mjs).
|
|
13
|
+
*
|
|
14
|
+
* Split out (SOLID F041) because the kind was branched on at five sites — the
|
|
15
|
+
* vocabulary twice in prompts.mjs, the posture and the artifact rule in the
|
|
16
|
+
* agent lane, the posture names again in claude.mjs — so a fourth kind was a
|
|
17
|
+
* coordinated edit whose missed site ran a card under the wrong posture. Now a
|
|
18
|
+
* kind is one entry here, and `agentTaskKinds.test.mjs` proves every entry has
|
|
19
|
+
* a contract, a posture a runtime can declare, and (for a non-code kind) the
|
|
20
|
+
* artifact that proves it delivered.
|
|
21
|
+
*
|
|
22
|
+
* Pure data and pure functions: no imports, no environment, no I/O — prompts
|
|
23
|
+
* imports it, and prompts is strings.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The kinds. Per kind:
|
|
28
|
+
* - `posture`: the turn profile the card runs under (turnProfile.mjs);
|
|
29
|
+
* - `artifact`: null for code (it delivers commits), else what proves a
|
|
30
|
+
* delivery — `match` over the artifact file name, an optional `companion`
|
|
31
|
+
* that must ALSO stand beside it (the 3D-model kind's model file: an HTML
|
|
32
|
+
* viewer alone is a page, not a model), and the `missing` sentence a turn
|
|
33
|
+
* settles `nothing` with when the proof was not written;
|
|
34
|
+
* - `offRuntime(rt)`: the sentence when the chosen CLI cannot run the posture.
|
|
35
|
+
*/
|
|
36
|
+
/** Every non-code kind runs on Claude: the one CLI that declares their postures. */
|
|
37
|
+
const CLAUDE_ONLY = 'mockup, 3D model, presentation and write-up cards run on Claude on this machine';
|
|
38
|
+
|
|
39
|
+
export const AGENT_TASK_KINDS = Object.freeze({
|
|
40
|
+
code: Object.freeze({
|
|
41
|
+
posture: 'build',
|
|
42
|
+
artifact: null,
|
|
43
|
+
offRuntime: (rt) => `this machine cannot run ${rt}`,
|
|
44
|
+
}),
|
|
45
|
+
design: Object.freeze({
|
|
46
|
+
posture: 'design',
|
|
47
|
+
artifact: Object.freeze({
|
|
48
|
+
match: /\.html?$/i,
|
|
49
|
+
missing: 'the turn ended without writing a mockup under .flowviant/artifacts/',
|
|
50
|
+
}),
|
|
51
|
+
offRuntime: () => CLAUDE_ONLY,
|
|
52
|
+
}),
|
|
53
|
+
/**
|
|
54
|
+
* THE 3D-MODEL KIND (0.105.0): a viewable bundle — an HTML viewer and the
|
|
55
|
+
* model files beside it. Under the design fence (read the repo, write only
|
|
56
|
+
* `.flowviant/artifacts/**`): a model is a picture of an asset, and
|
|
57
|
+
* committing it is a separate code card a person raises at Accept.
|
|
58
|
+
*/
|
|
59
|
+
model: Object.freeze({
|
|
60
|
+
posture: 'design',
|
|
61
|
+
artifact: Object.freeze({
|
|
62
|
+
match: /\.html?$/i,
|
|
63
|
+
companion: /\.(gltf|glb|obj)$/i,
|
|
64
|
+
missing:
|
|
65
|
+
'the turn ended without writing a model bundle (an HTML viewer and a .gltf, .glb or .obj file) under .flowviant/artifacts/',
|
|
66
|
+
}),
|
|
67
|
+
offRuntime: () => CLAUDE_ONLY,
|
|
68
|
+
}),
|
|
69
|
+
/** THE PRESENTATION KIND (0.105.0): one HTML slide deck, under the design fence. */
|
|
70
|
+
deck: Object.freeze({
|
|
71
|
+
posture: 'design',
|
|
72
|
+
artifact: Object.freeze({
|
|
73
|
+
match: /\.html?$/i,
|
|
74
|
+
missing: 'the turn ended without writing a deck under .flowviant/artifacts/',
|
|
75
|
+
}),
|
|
76
|
+
offRuntime: () => CLAUDE_ONLY,
|
|
77
|
+
}),
|
|
78
|
+
research: Object.freeze({
|
|
79
|
+
posture: 'research',
|
|
80
|
+
artifact: Object.freeze({
|
|
81
|
+
match: /\.md$/i,
|
|
82
|
+
missing: 'the turn ended without writing a write-up under .flowviant/artifacts/',
|
|
83
|
+
}),
|
|
84
|
+
offRuntime: () => CLAUDE_ONLY,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const known = (v) => typeof v === 'string' && Object.hasOwn(AGENT_TASK_KINDS, v);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A CARD'S KIND, READ THE ONE WAY THE DAEMON READS IT.
|
|
92
|
+
*
|
|
93
|
+
* The server sends the key only for non-code kinds, so ABSENT IS CODE, and an
|
|
94
|
+
* unknown string (a newer server, a typo) degrades to code: the one kind that
|
|
95
|
+
* changes nothing about how a card is PRINTED. Every printing reader — the
|
|
96
|
+
* contract selector, the kickoff, the spec — goes through this, so no two of
|
|
97
|
+
* them can disagree about what an odd value means.
|
|
98
|
+
*/
|
|
99
|
+
export const agentTaskKindOf = (v) => (known(v) ? v : 'code');
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* …EXCEPT WHERE A TURN IS ABOUT TO SPAWN (2026-09-23). "Unknown degrades to
|
|
103
|
+
* code" is right for every reader that only PRINTS the kind, and wrong for the
|
|
104
|
+
* one that picks a POSTURE: code is the build turn, permissions skipped, so a
|
|
105
|
+
* kind a newer server invented for a card that must not touch the code (a
|
|
106
|
+
* fourth kind, a typo, `Design`) would have run as exactly the turn it exists
|
|
107
|
+
* to prevent. The agent lane asks this first and refuses in words, relaying
|
|
108
|
+
* the word it was handed. Absent, null and empty stay code — that is how a
|
|
109
|
+
* code card arrives.
|
|
110
|
+
*
|
|
111
|
+
* @returns {string|null} the unrecognised value, or null when the kind is one
|
|
112
|
+
* this daemon runs.
|
|
113
|
+
*/
|
|
114
|
+
export const unknownAgentTaskKind = (v) =>
|
|
115
|
+
v === undefined || v === null || v === '' || known(v)
|
|
116
|
+
? null
|
|
117
|
+
: String(v).replace(/[\u0000-\u001f\u007f]/g, '').slice(0, 40);
|
|
118
|
+
|
|
119
|
+
/** The table entry for a kind value, read the printing way (unknown → code). */
|
|
120
|
+
export const agentTaskKind = (v) => AGENT_TASK_KINDS[agentTaskKindOf(v)];
|
|
121
|
+
|
|
122
|
+
/** The postures only a card's kind selects (every kind's but code's), each
|
|
123
|
+
* once: the 3D-model and presentation kinds reuse the design posture. */
|
|
124
|
+
export const KIND_POSTURES = Object.freeze([
|
|
125
|
+
...new Set(
|
|
126
|
+
Object.values(AGENT_TASK_KINDS)
|
|
127
|
+
.map((k) => k.posture)
|
|
128
|
+
.filter((p) => p !== 'build')
|
|
129
|
+
),
|
|
130
|
+
]);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANTIGRAVITY (agy) SESSION PRESENCE (split out of localSessions.mjs
|
|
3
|
+
* 2026-09-26, SOLID F062). agy's SQLite store, its cwd cache and its
|
|
4
|
+
* process-name liveness change for reasons of their own, apart from Claude's
|
|
5
|
+
* transcript format (claudeSessions.mjs); the shared fence and report cap are
|
|
6
|
+
* the coordinator's (localSessions.mjs).
|
|
7
|
+
*
|
|
8
|
+
* NOTHING in this file throws.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { procPids, scanProcs } from './procScan.mjs';
|
|
16
|
+
|
|
17
|
+
// agy's store is nothing like Claude's: one SQLite db per conversation at
|
|
18
|
+
// ~/.gemini/antigravity-cli/conversations/<uuid>.db (global, not cwd-keyed),
|
|
19
|
+
// no per-pid liveness registry that survives contact (the presence/*.lock
|
|
20
|
+
// files sit untouched by real runs — measured), and the only cwd mapping is
|
|
21
|
+
// cache/last_conversations.json: {cwd → the LAST conversation run there}.
|
|
22
|
+
// So the honest agy report is a SUBSET — the last conversation per directory
|
|
23
|
+
// inside this repo — and that is exactly the one `agy --continue` would give
|
|
24
|
+
// the person at that keyboard, i.e. the one worth offering to adopt.
|
|
25
|
+
|
|
26
|
+
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
|
+
|
|
29
|
+
/** Newest write to the conversation's store — the wal carries recent turns,
|
|
30
|
+
* so its mtime (not the db's) is the real "last active" (measured: a resume
|
|
31
|
+
* touched db+wal, and never the presence lock). 0 = no such conversation. */
|
|
32
|
+
function agyLastWriteMs(id) {
|
|
33
|
+
if (!AGY_UUID_RE.test(id)) return 0;
|
|
34
|
+
let newest = 0;
|
|
35
|
+
for (const suffix of ['.db', '.db-wal']) {
|
|
36
|
+
try {
|
|
37
|
+
const t = statSync(join(AGY_DIR(), 'conversations', `${id}${suffix}`)).mtimeMs;
|
|
38
|
+
if (t > newest) newest = t;
|
|
39
|
+
} catch {
|
|
40
|
+
/* absent half is fine — the db alone still answers */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return newest;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Any agy process on the machine right now? /proc comm scan — cheap at the
|
|
47
|
+
* 60s cadence, and the only liveness signal agy leaves (locks are inert).
|
|
48
|
+
* Tri-state on purpose: true (an agy process exists), false (scanned /proc
|
|
49
|
+
* and found none), null (no /proc to scan — macOS — so the question is
|
|
50
|
+
* UNANSWERABLE here, which is a different fact from "no", and the adoption
|
|
51
|
+
* path below treats it differently: unknowable refuses, absent permits). */
|
|
52
|
+
function agyProcessAlive() {
|
|
53
|
+
if (!existsSync('/proc')) {
|
|
54
|
+
// No /proc (macOS): ask pgrep the same question. A MEASURED "none" here
|
|
55
|
+
// matters — answering null instead made every agy conversation on such a
|
|
56
|
+
// machine read live forever, which both invented a state ("live" when the
|
|
57
|
+
// truth was "unmeasured") and permanently killed agy adoption there.
|
|
58
|
+
try {
|
|
59
|
+
execFileSync('pgrep', ['-x', 'agy'], {
|
|
60
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
61
|
+
timeout: 5_000,
|
|
62
|
+
});
|
|
63
|
+
return true; // exit 0 — at least one agy process
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (e?.status === 1) return false; // pgrep ran and found none
|
|
66
|
+
return null; // pgrep itself unavailable/errored — genuinely unknowable
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// The /proc walk is procScan's (every pid looked at; a bound of ONE row,
|
|
70
|
+
// because the first agy answers the question).
|
|
71
|
+
const pids = procPids();
|
|
72
|
+
if (pids === null) return null; // /proc exists but won't read — still unknowable
|
|
73
|
+
const isAgy = (name) => {
|
|
74
|
+
try {
|
|
75
|
+
return readFileSync(`/proc/${name}/comm`, 'utf8').trim() === 'agy' ? true : null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null; // raced exit — keep scanning
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return scanProcs(pids, isAgy, 1).rows.length > 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Is this agy conversation being driven RIGHT NOW? agy cannot answer
|
|
85
|
+
* per-conversation, so this is the conservative composite: an agy process
|
|
86
|
+
* exists AND this conversation's store was written in the last 10 minutes.
|
|
87
|
+
* Adoption is a MOVE for agy (no fork exists — measured, "trajectory not
|
|
88
|
+
* found" on a renamed copy), so refusing a maybe-live conversation for a few
|
|
89
|
+
* minutes costs a retry; adopting an actually-live one puts two drivers on
|
|
90
|
+
* one store.
|
|
91
|
+
*/
|
|
92
|
+
const AGY_LIVE_WINDOW_MS = 10 * 60 * 1000;
|
|
93
|
+
export function isAgyConversationLive(id, { processAlive = agyProcessAlive } = {}) {
|
|
94
|
+
try {
|
|
95
|
+
const up = processAlive();
|
|
96
|
+
// Unknowable is NOT "ended". Without /proc the process half of the
|
|
97
|
+
// composite cannot be measured, and calling that "no process" would make
|
|
98
|
+
// every agy conversation on such a machine adoptable — the move-adoption
|
|
99
|
+
// then puts a second driver on a store someone may still be writing.
|
|
100
|
+
// "Live" here means "refuse adoption", and refusing what we cannot verify
|
|
101
|
+
// is the same conservatism as the composite itself.
|
|
102
|
+
if (up === null) return true;
|
|
103
|
+
if (!up) return false;
|
|
104
|
+
const t = agyLastWriteMs(id);
|
|
105
|
+
return t > 0 && Date.now() - t < AGY_LIVE_WINDOW_MS;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The repo's agy conversations, via the cwd registry — see the section
|
|
112
|
+
* comment for why this is deliberately a subset. The coordinator hands over
|
|
113
|
+
* the fence (`ours`, `mine`) and the ended-session `cutoff`; `processAlive`
|
|
114
|
+
* measures liveness (true / false / null = unknowable), asked once and only
|
|
115
|
+
* when there is a registry to read. */
|
|
116
|
+
export function scanAgyConversations({ ours, mine, cutoff, processAlive = agyProcessAlive }) {
|
|
117
|
+
const out = [];
|
|
118
|
+
try {
|
|
119
|
+
const raw = readFileSync(join(AGY_DIR(), 'cache', 'last_conversations.json'), 'utf8');
|
|
120
|
+
const map = JSON.parse(raw);
|
|
121
|
+
if (!map || typeof map !== 'object') return out;
|
|
122
|
+
const processUp = processAlive();
|
|
123
|
+
for (const [cwd, id] of Object.entries(map)) {
|
|
124
|
+
if (typeof id !== 'string' || !AGY_UUID_RE.test(id)) continue;
|
|
125
|
+
// The operator's own agy TAB, whose conversation is registered against
|
|
126
|
+
// the checkout exactly as a terminal one would be.
|
|
127
|
+
if (mine.has(id)) continue;
|
|
128
|
+
let real;
|
|
129
|
+
try {
|
|
130
|
+
real = realpathSync(cwd);
|
|
131
|
+
} catch {
|
|
132
|
+
continue; // the directory is gone — nothing to point a tab at
|
|
133
|
+
}
|
|
134
|
+
if (!ours(real)) continue;
|
|
135
|
+
const lastMs = agyLastWriteMs(id);
|
|
136
|
+
if (!lastMs || lastMs < cutoff) continue;
|
|
137
|
+
out.push({
|
|
138
|
+
id,
|
|
139
|
+
cwd: real,
|
|
140
|
+
// Mirrors isAgyConversationLive exactly, unknowable (null) included:
|
|
141
|
+
// the report must never offer as adoptable a conversation the adopt
|
|
142
|
+
// path will then refuse — an offer wired to a refusal is the dead
|
|
143
|
+
// control this product keeps deleting.
|
|
144
|
+
live: processUp === null ? true : processUp && Date.now() - lastMs < AGY_LIVE_WINDOW_MS,
|
|
145
|
+
lastActiveAt: new Date(lastMs).toISOString(),
|
|
146
|
+
runtime: 'antigravity',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
out.sort(
|
|
150
|
+
(a, b) =>
|
|
151
|
+
(b.lastActiveAt < a.lastActiveAt ? -1 : b.lastActiveAt > a.lastActiveAt ? 1 : 0) ||
|
|
152
|
+
(a.id < b.id ? -1 : 1)
|
|
153
|
+
);
|
|
154
|
+
} catch {
|
|
155
|
+
/* no agy on this machine, or an unreadable registry — nothing to report */
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE 3D-MODEL AND PRESENTATION CARDS' CONTRACTS (2026-09-27, 0.105.0).
|
|
3
|
+
*
|
|
4
|
+
* Two more kinds that hand back a file rather than a change, beside the design
|
|
5
|
+
* and research contracts in prompts.mjs (which is past its size and keeps
|
|
6
|
+
* those two). `prompts.mjs` imports these into `AGENT_CONTRACTS`; the kind
|
|
7
|
+
* table (agentTaskKinds.mjs) runs both under the `design` posture — read the
|
|
8
|
+
* repo, write only `.flowviant/artifacts/**`, no web, no shell beyond `ls` —
|
|
9
|
+
* so "change no repository file" is enforced, not asked.
|
|
10
|
+
*
|
|
11
|
+
* THE SAME BONES AS THE DESIGN CONTRACT, deliberately: its header, its rule 3
|
|
12
|
+
* (commit nothing), its rule 4 (stop and ask) and its final JSON — one shape
|
|
13
|
+
* `parseTurnResult`, the settle, the board and the review deck already read.
|
|
14
|
+
* The tail is spelled ONCE, in `ARTIFACT_CONTRACT_TAIL`, and every
|
|
15
|
+
* file-handing contract renders it — these two, and the design and research
|
|
16
|
+
* contracts in prompts.mjs (whose bytes `taskKind.test.mjs` pins unchanged by
|
|
17
|
+
* the move), so no two of them can drift apart.
|
|
18
|
+
*
|
|
19
|
+
* "model" here is the 3D-model KIND (not an AI-model pin, `job.model`), and
|
|
20
|
+
* "deck" the presentation kind (not the review deck).
|
|
21
|
+
*
|
|
22
|
+
* Pure strings: no imports, no environment, no I/O.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Rule 3, rule 4 and the final JSON a file-handing contract ends with.
|
|
27
|
+
* `ask` is rule 4's body after its number (the decision only a person can
|
|
28
|
+
* make); `summary` is the delivered shape's summary gloss.
|
|
29
|
+
*/
|
|
30
|
+
export const ARTIFACT_CONTRACT_TAIL = ({ ask, summary }) => `3. CHANGE NO REPOSITORY FILE AND COMMIT NOTHING. You can only write under
|
|
31
|
+
.flowviant/artifacts/, and the daemon keeps that directory out of git.
|
|
32
|
+
|
|
33
|
+
4. ${ask}
|
|
34
|
+
|
|
35
|
+
END YOUR TURN WITH ONE JSON OBJECT AND NOTHING AFTER IT, in a \`\`\`json fence:
|
|
36
|
+
|
|
37
|
+
\`\`\`json
|
|
38
|
+
{
|
|
39
|
+
"status": "delivered",
|
|
40
|
+
"summary": "${summary}",
|
|
41
|
+
"progress": "two or three sentences on what you have done in this run SO FAR, across every card"
|
|
42
|
+
}
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
or, if you are stopping to ask:
|
|
46
|
+
|
|
47
|
+
\`\`\`json
|
|
48
|
+
{
|
|
49
|
+
"status": "blocked",
|
|
50
|
+
"progress": "two or three sentences on what you have done in this run SO FAR, across every card",
|
|
51
|
+
"question": "the specific thing you need decided, in one or two sentences"
|
|
52
|
+
}
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
"progress" is REQUIRED on both shapes and REPLACES the previous one whole —
|
|
56
|
+
past tense, plain sentences, no card ids.`;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* THE 3D-MODEL CARD. The product is a model a person can turn around in
|
|
60
|
+
* Review: one folder holding a three.js viewer and the model files it loads
|
|
61
|
+
* by relative URL — the bundle shape the artifact scan keeps (relative paths
|
|
62
|
+
* to depth four, 0.99.0) and the library keeps as one versioned folder. The
|
|
63
|
+
* daemon calls it delivered only when an `.html` AND a `.gltf`/`.glb`/`.obj`
|
|
64
|
+
* stand under `.flowviant/artifacts/` (agentTaskKinds.mjs).
|
|
65
|
+
*
|
|
66
|
+
* TEXT, BECAUSE TEXT IS ALL THIS POSTURE WRITES (2026-09-27, a call the owner
|
|
67
|
+
* may overrule). The kind runs under the `design` posture: Read, `ls`, and
|
|
68
|
+
* `Edit(.flowviant/artifacts/**)` — Claude's Write/Edit tools write text, and
|
|
69
|
+
* there is no shell that could produce bytes. The spec's first wording asked
|
|
70
|
+
* for glTF with a `.bin`, or a `.glb`, and `.png`/`.jpg` textures: outputs
|
|
71
|
+
* this card cannot make. So the contract asks for OBJ + `.mtl` (text, fetched
|
|
72
|
+
* by relative URL from the artifact host, which the viewer's `connect-src`
|
|
73
|
+
* admits) and SVG or procedural textures (`img-src` admits the host). NOT a
|
|
74
|
+
* `.gltf` with its buffer as a `data:` URI: three.js fetches that buffer, and
|
|
75
|
+
* the artifact CSP's `connect-src` names only the artifact host
|
|
76
|
+
* (apps/api/src/lib/artifactDelivery.ts), so the review viewer would refuse
|
|
77
|
+
* it. Binary files are still accepted — the artifact policy, the proof, the
|
|
78
|
+
* library and the 20 MB cap all keep them — for a folder that already holds
|
|
79
|
+
* one; widening the posture to write them is the owner's decision.
|
|
80
|
+
*/
|
|
81
|
+
export const SYSTEM_AGENT_MODEL = `You are the human's own Claude, working one 3D MODEL card in a git worktree of
|
|
82
|
+
their repository. Nobody is watching this run. You have the repo and nothing
|
|
83
|
+
else — no project tools, no board, no chat.
|
|
84
|
+
|
|
85
|
+
A 3D MODEL CARD HANDS BACK A MODEL YOU CAN LOOK AT, NOT A CHANGE. Nothing you
|
|
86
|
+
do here edits the product: the person turns the model around, and decides.
|
|
87
|
+
|
|
88
|
+
WHAT TO DO:
|
|
89
|
+
|
|
90
|
+
1. Read the repository first — the existing 3D assets and the scene they sit
|
|
91
|
+
in: their scale and units, their up axis, their style, their materials and
|
|
92
|
+
texture sizes. The model must fit THIS project, not a stock asset library.
|
|
93
|
+
|
|
94
|
+
2. Write ONE folder, .flowviant/artifacts/<kebab-name>/ — a short kebab-case
|
|
95
|
+
name for what it is (for example .flowviant/artifacts/reading-chair/) —
|
|
96
|
+
holding:
|
|
97
|
+
- index.html, a viewer: three.js loaded from cdn.jsdelivr.net/npm or
|
|
98
|
+
unpkg.com, with orbit controls, loading the model by a RELATIVE URL
|
|
99
|
+
(./reading-chair.obj — never a path outside the folder);
|
|
100
|
+
- the model, written as TEXT: a .obj with its .mtl, each loaded by a
|
|
101
|
+
relative URL;
|
|
102
|
+
- its textures as .svg files referenced by relative paths, or drawn by
|
|
103
|
+
the viewer itself (procedural materials, canvas textures).
|
|
104
|
+
You can write only text files here. A .glb, .bin, .png or .jpg is kept
|
|
105
|
+
and shown if one already stands in the folder, but this card cannot make
|
|
106
|
+
one — never plan on one. Nothing else may load from the network, and
|
|
107
|
+
nothing can be sent. Keep every file under 2 MB — the viewer, the .obj,
|
|
108
|
+
the .mtl, each texture.
|
|
109
|
+
|
|
110
|
+
${ARTIFACT_CONTRACT_TAIL({
|
|
111
|
+
ask: `If you cannot model it without a DECISION only a person can make — which of
|
|
112
|
+
two directions, what size or style, which asset it stands beside — STOP AND
|
|
113
|
+
ASK. Do not guess. A question costs one reply; a guessed model costs a review.`,
|
|
114
|
+
summary: 'one or two sentences on what the model shows, and the folder it is in',
|
|
115
|
+
})}`;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* THE PRESENTATION CARD. One self-contained HTML deck under the design
|
|
119
|
+
* card's inline and CDN rules, navigable by keyboard and on screen, and
|
|
120
|
+
* printable one slide per page — so "send it as a PDF" is the browser's
|
|
121
|
+
* Print, never a converter this machine would have to run.
|
|
122
|
+
*/
|
|
123
|
+
export const SYSTEM_AGENT_DECK = `You are the human's own Claude, working one PRESENTATION card in a git worktree
|
|
124
|
+
of their repository. Nobody is watching this run. You have the repo and nothing
|
|
125
|
+
else — no project tools, no board, no chat.
|
|
126
|
+
|
|
127
|
+
A PRESENTATION CARD HANDS BACK A DECK, NOT A CHANGE. Nothing you do here edits
|
|
128
|
+
the product: the person pages through it, and decides.
|
|
129
|
+
|
|
130
|
+
WHAT TO DO:
|
|
131
|
+
|
|
132
|
+
1. Read what the deck is about first — the repository, the real product, its
|
|
133
|
+
brand and its own words. The deck must sound and look like THIS project,
|
|
134
|
+
not like a template.
|
|
135
|
+
|
|
136
|
+
2. Write ONE self-contained HTML file directly in .flowviant/artifacts/ — a
|
|
137
|
+
short kebab-case name for the talk (for example
|
|
138
|
+
.flowviant/artifacts/q3-roadmap.html). Inline CSS (a Google Fonts
|
|
139
|
+
stylesheet may load). Scripts may be inline or loaded from
|
|
140
|
+
cdnjs.cloudflare.com, cdn.jsdelivr.net/npm or unpkg.com, and from nowhere
|
|
141
|
+
else; nothing else may load from the network and nothing can be sent, so
|
|
142
|
+
images are data: URIs or inline SVG. Each slide is a <section>; the arrow
|
|
143
|
+
keys and on-screen previous/next buttons move between them, and a counter
|
|
144
|
+
says which slide of how many is showing. Add @media print rules that put
|
|
145
|
+
one slide on each page (break-after: page), so the browser prints it to a
|
|
146
|
+
PDF. Keep it under 2 MB.
|
|
147
|
+
|
|
148
|
+
${ARTIFACT_CONTRACT_TAIL({
|
|
149
|
+
ask: `If you cannot write it without a DECISION only a person can make — who the
|
|
150
|
+
audience is, how long it runs, what it must argue — STOP AND ASK. Do not
|
|
151
|
+
guess. A question costs one reply; a guessed deck costs a review.`,
|
|
152
|
+
summary: 'one or two sentences on what the deck covers, and the file it is in',
|
|
153
|
+
})}`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ARTIFACT POLICY — GENERATED, DO NOT EDIT BY HAND.
|
|
3
|
+
*
|
|
4
|
+
* Rendered from the server's packages/shared/src/schemas/artifact.schema.ts
|
|
5
|
+
* by flowviant's packages/shared/src/schemas/daemonArtifactPolicy.ts
|
|
6
|
+
* (`bun scripts/write-daemon-artifact-policy.ts` in the app repo). The server
|
|
7
|
+
* is the release source; its parity test fails until this file matches it.
|
|
8
|
+
* artifacts.mjs reads the allowlist and the caps from here, and the server
|
|
9
|
+
* still verifies every upload on its own.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const ARTIFACT_POLICY_SOURCE = 'packages/shared/src/schemas/artifact.schema.ts';
|
|
13
|
+
|
|
14
|
+
/** Per text file and ordinary binary artifact. */
|
|
15
|
+
export const ARTIFACT_MAX_BYTES = 2097152;
|
|
16
|
+
/** Binary model buffers (the extensions below). */
|
|
17
|
+
export const ARTIFACT_BINARY_MODEL_MAX_BYTES = 20971520;
|
|
18
|
+
export const ARTIFACT_BINARY_MODEL_EXTS = ['glb', 'bin'];
|
|
19
|
+
/** Per session and per agent — the server keeps as many per owner. */
|
|
20
|
+
export const ARTIFACT_MAX_PER_OWNER = 40;
|
|
21
|
+
|
|
22
|
+
/** Extension → the one mime the server serves it under; `text` is scrubbed. */
|
|
23
|
+
export const ARTIFACT_TYPES = {
|
|
24
|
+
html: { mime: 'text/html', text: true },
|
|
25
|
+
htm: { mime: 'text/html', text: true },
|
|
26
|
+
md: { mime: 'text/markdown', text: true },
|
|
27
|
+
svg: { mime: 'image/svg+xml', text: true },
|
|
28
|
+
png: { mime: 'image/png', text: false },
|
|
29
|
+
jpg: { mime: 'image/jpeg', text: false },
|
|
30
|
+
jpeg: { mime: 'image/jpeg', text: false },
|
|
31
|
+
gif: { mime: 'image/gif', text: false },
|
|
32
|
+
webp: { mime: 'image/webp', text: false },
|
|
33
|
+
json: { mime: 'application/json', text: true },
|
|
34
|
+
csv: { mime: 'text/csv', text: true },
|
|
35
|
+
txt: { mime: 'text/plain', text: true },
|
|
36
|
+
docx: { mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', text: false },
|
|
37
|
+
pptx: { mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', text: false },
|
|
38
|
+
xlsx: { mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', text: false },
|
|
39
|
+
pdf: { mime: 'application/pdf', text: false },
|
|
40
|
+
gltf: { mime: 'model/gltf+json', text: true },
|
|
41
|
+
obj: { mime: 'model/obj', text: true },
|
|
42
|
+
mtl: { mime: 'model/mtl', text: true },
|
|
43
|
+
glb: { mime: 'model/gltf-binary', text: false },
|
|
44
|
+
bin: { mime: 'application/octet-stream', text: false },
|
|
45
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE DESIGN PREVIEW — a screenshot of an HTML artifact, taken in a browser
|
|
3
|
+
* (2026-09-26, split out of artifacts.mjs — SOLID F051).
|
|
4
|
+
*
|
|
5
|
+
* Browser rendering is its own reason to change: a Chrome flag, a CSP tweak or
|
|
6
|
+
* a viewport size has nothing to do with how the artifact directory is scanned
|
|
7
|
+
* or how an upload is retried. The reporter (`artifactReporter.mjs`) is handed
|
|
8
|
+
* this function rather than importing it, so a test can stand in a renderer
|
|
9
|
+
* that never launches a browser.
|
|
10
|
+
*
|
|
11
|
+
* It renders the SCRUBBED bytes, never the checkout's source, under the
|
|
12
|
+
* artifact box's own network fence (`PREVIEW_CSP`) placed before any
|
|
13
|
+
* model-written markup executes. A failed measurement is explicit
|
|
14
|
+
* (`renderState: 'unavailable'`); a daemon older than this simply sends no
|
|
15
|
+
* `renderState`, which the server reads as unmeasured.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { pathToFileURL } from 'node:url';
|
|
22
|
+
import { captureScreenshot } from './shot.mjs';
|
|
23
|
+
|
|
24
|
+
export const DESIGN_PREVIEW_MAX_BYTES = 2 * 1024 * 1024;
|
|
25
|
+
const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net/npm/ https://unpkg.com; style-src 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net/npm/ https://unpkg.com https://fonts.googleapis.com; img-src data: blob:; font-src data: https://fonts.gstatic.com; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
|
|
26
|
+
|
|
27
|
+
/** Render the scrubbed HTML, never the checkout's unsanitized source. A
|
|
28
|
+
* failed browser measurement is explicit; older daemon reports are absent. */
|
|
29
|
+
export async function renderDesignPreview(bytes, capture = captureScreenshot) {
|
|
30
|
+
const dir = mkdtempSync(join(tmpdir(), 'flowviant-design-'));
|
|
31
|
+
try {
|
|
32
|
+
const html = join(dir, 'design.html');
|
|
33
|
+
const png = join(dir, 'design.png');
|
|
34
|
+
// A fresh Chrome profile has no Flowviant credentials. Put the artifact
|
|
35
|
+
// box's network fence before any model-written markup executes.
|
|
36
|
+
writeFileSync(html, Buffer.concat([
|
|
37
|
+
Buffer.from(`<meta http-equiv="Content-Security-Policy" content="${PREVIEW_CSP}">`),
|
|
38
|
+
bytes,
|
|
39
|
+
]));
|
|
40
|
+
const result = await capture({ url: pathToFileURL(html).href, out: png, width: 720, height: 450, timeoutMs: 15_000 });
|
|
41
|
+
if (!result.ok) return { renderState: 'unavailable' };
|
|
42
|
+
const image = readFileSync(png);
|
|
43
|
+
if (image.byteLength > DESIGN_PREVIEW_MAX_BYTES || !image.subarray(0, 8).equals(Buffer.from([137,80,78,71,13,10,26,10]))) {
|
|
44
|
+
return { renderState: 'unavailable' };
|
|
45
|
+
}
|
|
46
|
+
return { renderState: 'rendered', preview: image };
|
|
47
|
+
} catch {
|
|
48
|
+
return { renderState: 'unavailable' };
|
|
49
|
+
} finally {
|
|
50
|
+
rmSync(dir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|