flowviant 0.125.20 → 0.125.22
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 +11 -1
- package/bin/cli.mjs +13 -0
- package/bin/lib/agentCards.mjs +24 -1
- package/bin/lib/agentPlan.mjs +3 -0
- package/bin/lib/agentQueueContext.mjs +20 -1
- package/bin/lib/artifactContracts.mjs +132 -2
- package/bin/lib/artifactIteration.mjs +2 -1
- package/bin/lib/artifactReporter.mjs +2 -2
- package/bin/lib/artifacts.mjs +8 -1
- package/bin/lib/claudeStream.mjs +7 -0
- package/bin/lib/codexFencedMount.mjs +2 -2
- package/bin/lib/codexSubagents.mjs +6 -2
- package/bin/lib/knowledgeFetch.mjs +8 -3
- package/bin/lib/knowledgeFiles.mjs +4 -4
- package/bin/lib/orchestrator.mjs +40 -14
- package/bin/lib/orchestratorHooks.mjs +15 -2
- package/bin/lib/orchestratorNative.mjs +30 -9
- package/bin/lib/pinnedCapture.mjs +19 -8
- package/bin/lib/postureTools.mjs +2 -2
- package/bin/lib/prompts.mjs +25 -4
- package/bin/lib/publicReferenceBrowser.mjs +180 -0
- package/bin/lib/publicReferenceNetwork.mjs +131 -0
- package/bin/lib/publicReferenceProtocol.mjs +55 -0
- package/bin/lib/publicReferenceRuntime.mjs +65 -0
- package/bin/lib/publicReferenceTool.mjs +89 -0
- package/bin/lib/referenceCaptureProtocol.mjs +12 -0
- package/bin/lib/referenceCaptures.mjs +140 -0
- package/bin/lib/runTurn.mjs +8 -0
- package/bin/lib/runtimeCodex.mjs +1 -1
- package/bin/lib/runtimeEvents.mjs +24 -2
- package/bin/lib/turnGaps.mjs +47 -0
- package/bin/lib/uiReview.mjs +305 -0
- package/bin/lib/uiReviewBrowser.mjs +212 -0
- package/bin/lib/uiReviewContract.mjs +51 -0
- package/bin/lib/uiReviewTurn.mjs +15 -0
- package/bin/lib/websiteAccess.mjs +68 -0
- package/bin/lib/websiteTrace.mjs +36 -0
- package/bin/lib/work.mjs +1 -0
- package/bin/lib/workAgentPrecheck.mjs +4 -1
- package/bin/lib/workAgentTools.mjs +3 -2
- package/bin/lib/workAgentTurnExecution.mjs +49 -5
- package/bin/lib/workAgentTurnOutcome.mjs +13 -3
- package/bin/lib/workAgentTurns.mjs +2 -0
- package/bin/lib/workToolLog.mjs +24 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -188,7 +188,17 @@ From **0.86.0**, a project whose owner turns on **Publish agent branches** has e
|
|
|
188
188
|
|
|
189
189
|
You run your dev server yourself, in the session's own worktree, exactly as you would in any terminal. The daemon NOTICES the listening port; ask for a share in the app and it puts a [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) quick tunnel in front of it (auto-fetched if missing, pinned and checksummed), behind the daemon's own gate. Viewers reach the share through Flowviant's relay at `<name>.flowviant.dev`, never the tunnel's own address, and the gate admits only the share's password or a short-lived grant signed with the share's key. Flowviant stores that key and the password with the share, and its relay mints grants for signed-in project members — or for anyone with the link, if the share's owner opens it to everyone. Since 0.72.0 the gate also lets the Workbench embed the share in a frame: it rewrites the response's frame policy to permit exactly the app's origin, and a framed sign-in gets a partitioned cookie so it works where third-party cookies are blocked.
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
Sharing never starts a repository command. An earlier version read a `.flowviant/preview.json` from the branch and spawned the command it named — that sharing start path was removed in 0.53.0; see the header of `bin/lib/preview.mjs`.
|
|
192
|
+
|
|
193
|
+
## Local UI self-review in code agents
|
|
194
|
+
|
|
195
|
+
During a code build, the same agent can run a bounded local Chromium review before delivery. Its turn supplies the exact helper command and JSON-plan format. The agent chooses the repository-supported preview argv and dedicated port, names its script/configuration basis, and describes disposable fixtures. The runner starts that command inside the worktree with an isolated home and no inherited SDK credentials, measures its listener and process group, then checks HTTP readiness. It does not install a browser or dependencies, share a preview, or open a public tunnel.
|
|
196
|
+
|
|
197
|
+
Each plan contains local routes and ordered CSS-selector actions (`click`, `fill`, `submit`, `waitFor`, `assertText`, `assertUrl`). Chromium uses fresh contexts for 1440×900 desktop and 390×844 mobile captures. Click/submit mean DOM activation; assertions establish the resulting state. A preview-only proxy and request fence block other destinations, popups, downloads, frames, workers and sockets. Apps requiring those capabilities must report the affected checks skipped or use a disposable fixture that works within this boundary. Repository code still executes as the user's UID; the runner is not a sandbox for a malicious backend or a substitute for choosing safe fixtures.
|
|
198
|
+
|
|
199
|
+
On Claude Code and Codex build turns, `card_ui_review.inspect_screenshot` can supply an exact captured PNG as native image input. The agent must actually inspect the images and report what it saw, or report the runtime's image-input failure. A screenshot or image-input receipt alone never counts as a visual verdict. Other build CLIs can use their existing image tools if available and must name unavailable capabilities honestly.
|
|
200
|
+
|
|
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.
|
|
192
202
|
|
|
193
203
|
## Modes
|
|
194
204
|
|
package/bin/cli.mjs
CHANGED
|
@@ -85,6 +85,19 @@ if (process.argv[2] === '__capture-mcp') {
|
|
|
85
85
|
process.exit(process.exitCode ?? 0);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
if (process.argv[2] === '__reference-mcp') {
|
|
89
|
+
await (await import('./lib/publicReferenceTool.mjs')).referenceToolMain();
|
|
90
|
+
// A screenshot can exceed the pipe buffer. EOF on the client's input must
|
|
91
|
+
// not truncate a completed result while stdout is still draining.
|
|
92
|
+
await new Promise(resolve => process.stdout.write('', resolve));
|
|
93
|
+
process.exit(process.exitCode ?? 0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (['__ui-review', '__ui-review-mcp'].includes(process.argv[2])) {
|
|
97
|
+
await (await import('./lib/uiReview.mjs')).uiReviewMain(process.argv[2] === '__ui-review-mcp');
|
|
98
|
+
process.exit(process.exitCode ?? 0);
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
if (process.argv[2] === '__orchestrator-mcp') {
|
|
89
102
|
await (await import('./lib/orchestratorClient.mjs')).orchestratorMcpMain();
|
|
90
103
|
process.exit(process.exitCode ?? 0);
|
package/bin/lib/agentCards.mjs
CHANGED
|
@@ -70,6 +70,24 @@ export function stashCard(path, taskId, spec) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* WHAT THE AGENT SAID IT DID NOT DELIVER on a card (2026-10-25, turnGaps.mjs),
|
|
75
|
+
* kept beside the spec so the pre-review can read the request, the diff and
|
|
76
|
+
* the agent's own account of what is missing together. Same append rule; the
|
|
77
|
+
* reader keeps the last declaration per card.
|
|
78
|
+
*/
|
|
79
|
+
export function stashGaps(path, taskId, gaps) {
|
|
80
|
+
if (!path || !Array.isArray(gaps)) return false;
|
|
81
|
+
const id = String(taskId ?? '').trim();
|
|
82
|
+
if (!id) return false;
|
|
83
|
+
try {
|
|
84
|
+
appendFileSync(path, JSON.stringify({ taskId: id.slice(0, 64), gaps }) + '\n');
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
/**
|
|
74
92
|
* What this box holds, newest spec per card, in the order the cards were worked.
|
|
75
93
|
*
|
|
@@ -89,6 +107,7 @@ export function readStash(path) {
|
|
|
89
107
|
* order stays the order the cards were first handed out — which is the order
|
|
90
108
|
* the branch was built in, and the order a reviewer reads them in. */
|
|
91
109
|
const byId = new Map();
|
|
110
|
+
const gapsById = new Map();
|
|
92
111
|
for (const line of raw.split('\n')) {
|
|
93
112
|
if (!line.trim()) continue;
|
|
94
113
|
let v;
|
|
@@ -99,10 +118,14 @@ export function readStash(path) {
|
|
|
99
118
|
}
|
|
100
119
|
if (!v || typeof v !== 'object') continue;
|
|
101
120
|
const taskId = typeof v.taskId === 'string' ? v.taskId.trim() : '';
|
|
121
|
+
if (taskId && Array.isArray(v.gaps)) {
|
|
122
|
+
gapsById.set(taskId, v.gaps);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
102
125
|
const prompt = typeof v.prompt === 'string' ? v.prompt : '';
|
|
103
126
|
if (!taskId || !prompt.trim()) continue;
|
|
104
127
|
byId.set(taskId, prompt);
|
|
105
128
|
}
|
|
106
|
-
const all = [...byId].map(([taskId, prompt]) => ({ taskId, prompt }));
|
|
129
|
+
const all = [...byId].map(([taskId, prompt]) => ({ taskId, prompt, ...(gapsById.has(taskId) ? { gaps: gapsById.get(taskId) } : {}) }));
|
|
107
130
|
return all;
|
|
108
131
|
}
|
package/bin/lib/agentPlan.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import { AGENT_TASK_KINDS } from './agentTaskKinds.mjs';
|
|
|
35
35
|
import { blockedAnswer } from './agentAsk.mjs';
|
|
36
36
|
import { isResearchUse } from './researchUse.mjs';
|
|
37
37
|
import { candidateObjects } from './modelJson.mjs';
|
|
38
|
+
import { parseGaps } from './turnGaps.mjs';
|
|
38
39
|
|
|
39
40
|
/** The biggest a single proposal may be. Bounds on a machine, not a policy:
|
|
40
41
|
* the server caps these again at its own boundary. */
|
|
@@ -272,6 +273,8 @@ export function parseTurnResult(text, scrub = envScrub) {
|
|
|
272
273
|
...(typeof v.automation === 'string' && v.automation.trim()
|
|
273
274
|
? { automation: scrub(v.automation.trim()) } : {}),
|
|
274
275
|
...(parseTestFindings(v.testFindings, scrub) ? { testFindings: parseTestFindings(v.testFindings, scrub) } : {}),
|
|
276
|
+
// What it says it did not deliver (turnGaps.mjs): absent is unknown.
|
|
277
|
+
...(parseGaps(v.gaps, scrub) ? { gaps: parseGaps(v.gaps, scrub) } : {}),
|
|
275
278
|
...progressOf(v, scrub),
|
|
276
279
|
raised: Array.isArray(v.raised)
|
|
277
280
|
? v.raised
|
|
@@ -14,6 +14,9 @@ an order alone is not a declared dependency. requirements.status=missing is
|
|
|
14
14
|
unavailable, never an empty brief. Discussions carry their existing omission
|
|
15
15
|
counts and may be unavailable; do not treat a shortened discussion as full
|
|
16
16
|
requirements. The brief and criteria marked full are supplied without clipping.
|
|
17
|
+
sourceChats are the person's exact words in the New task chat these cards were
|
|
18
|
+
planned in, up to each card's landing; a brief is the planner's paraphrase of
|
|
19
|
+
them, and links, references and file lists there are part of the request.
|
|
17
20
|
Human messages are exact recorded words across this agent, with the preceding
|
|
18
21
|
question when present. taskId on a message is where it was recorded, not an
|
|
19
22
|
implicit scope restriction. target.kind=agent explicitly addresses the whole
|
|
@@ -32,7 +35,9 @@ relying on an excluded decision; do not recover it from older native memory.
|
|
|
32
35
|
Latest outcomes and progress are reported summaries, not complete specifications,
|
|
33
36
|
proof that checks passed, human acceptance or evidence that bytes exist locally.
|
|
34
37
|
Stored outputs name exact ids/turns/hashes, not a successful local read. Retained
|
|
35
|
-
result selections preserve their exact scope.
|
|
38
|
+
result selections preserve their exact scope. localInputs lists the retained
|
|
39
|
+
paths that exist in this worktree this turn; any other retained path named in
|
|
40
|
+
the snapshot does not. Use the active turn's measured
|
|
36
41
|
input manifest and prerequisite receipts before relying on files; never replace
|
|
37
42
|
or regenerate missing selected inputs or assume queue knowledge discovers them.
|
|
38
43
|
Only execute the active turn's assigned work. Upcoming cards are context, not
|
|
@@ -44,6 +49,20 @@ only relevant prior findings/decisions/input identities, not the entire queue.
|
|
|
44
49
|
${fence('AGENT QUEUE SNAPSHOT', context)}`;
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
/** THE CARD'S SOURCE CHAT, as words the pre-review reads beside its spec
|
|
53
|
+
* (2026-10-25): the stash is per card and the snapshot is gone by review
|
|
54
|
+
* entry, so the person's request is kept with the spec it shaped. Empty when
|
|
55
|
+
* the card landed from no chat (or the server sent no source chats). */
|
|
56
|
+
export function sourceChatText(context, taskId) {
|
|
57
|
+
const chats = (context?.sourceChats?.chats ?? []).filter((c) => Array.isArray(c.taskIds) && c.taskIds.includes(taskId));
|
|
58
|
+
if (!chats.length) return '';
|
|
59
|
+
const said = chats.flatMap((c) => [
|
|
60
|
+
...(c.messages ?? []).map((m) => `${m.answering ? `(answering: ${m.answering})\n` : ''}${m.body}`),
|
|
61
|
+
...(c.omitted ? [`(${c.omitted} more message${c.omitted === 1 ? '' : 's'} omitted)`] : []),
|
|
62
|
+
]);
|
|
63
|
+
return `\n\nTHE PERSON'S OWN WORDS (the New task chat this card was planned in; the brief above paraphrases them):\n${said.join('\n\n')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
/** The gate supplies active identity/fences, not upcoming work to a child.
|
|
48
67
|
* Relevant findings still travel with the root's focused assignment. */
|
|
49
68
|
export function agentChildScope(job) {
|
|
@@ -82,6 +82,49 @@ is missing, so you lead:
|
|
|
82
82
|
outside decision — stop and name exactly that dependency; a skip never
|
|
83
83
|
supplies it.`;
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* THE WHOLE REQUEST, NOT ONLY THE BRIEF (2026-10-25) — every agent contract
|
|
87
|
+
* closes on it, before HELP_THEM_DECIDE.
|
|
88
|
+
*
|
|
89
|
+
* Eighteen merged runs were audited and most delivered part of what was asked
|
|
90
|
+
* while every check passed: a Codex planner could not open a Claude Design
|
|
91
|
+
* file and wrote "use only the described controls as scope" into the brief;
|
|
92
|
+
* the Claude agent, which could open it, never tried; an agent recast the
|
|
93
|
+
* design states between its cards as "decisions"; another satisfied "measure
|
|
94
|
+
* the waits" with a timer it wrote. The brief is a paraphrase, the person's
|
|
95
|
+
* words now ride the queue snapshot (`sourceChats`), a reference the agent
|
|
96
|
+
* can open is the agent's to open, and the delivery says what is missing
|
|
97
|
+
* (`gaps`, turnGaps.mjs) instead of leaving the person to find it.
|
|
98
|
+
*/
|
|
99
|
+
export const WHOLE_REQUEST = `THE WHOLE REQUEST, NOT ONLY THE BRIEF:
|
|
100
|
+
- A brief is a planner's paraphrase. When the queue snapshot carries
|
|
101
|
+
"sourceChats", those are the person's exact words from the chat this card
|
|
102
|
+
was planned in: read them before you start. Links, design references, file
|
|
103
|
+
lists and pasted text there are part of the request.
|
|
104
|
+
- A REFERENCE YOU CAN OPEN, YOU OPEN. Read every design or reference that the
|
|
105
|
+
brief, the source chat, the card's discussion or the person's messages link
|
|
106
|
+
to before you build against it. A claude.ai/design link is read with a
|
|
107
|
+
DesignSync tool where your CLI offers one: load it (ToolSearch
|
|
108
|
+
"select:DesignSync"), then get_file the named file and its imports,
|
|
109
|
+
read-only, never its write methods. A brief that says a reference "could not be opened", "is
|
|
110
|
+
unavailable" or "use only X as scope" describes the planner's access, not
|
|
111
|
+
yours: try it yourself. If you still cannot read it, name it in "gaps" and
|
|
112
|
+
build nothing that depends on guessing what it contains.
|
|
113
|
+
- COVER THE REQUEST, THEN SAY WHAT IS MISSING. Before you deliver, walk the
|
|
114
|
+
request: the source chat's words, every frame, state and rule a reference
|
|
115
|
+
you read shows or states, the brief and each "done when". Add "gaps" to the
|
|
116
|
+
delivered JSON object, one entry per item this card was meant to deliver
|
|
117
|
+
that you did not build ("not built"), built in part ("partly") or did not
|
|
118
|
+
check against the real thing ("unverified"), and per part of the request
|
|
119
|
+
that NO card in this run owns ("unowned"), so the person can file it:
|
|
120
|
+
"gaps": [{ "item": "what is missing", "status": "not built | partly | unverified | unowned", "why": "one sentence" }]
|
|
121
|
+
"gaps": [] says you walked the request and nothing is missing. Never recast
|
|
122
|
+
a gap as a decision: a choice you made in place of the request is a gap
|
|
123
|
+
("partly") with its reason.
|
|
124
|
+
- A "reproduce", "measure" or "verify" step is done only against the real
|
|
125
|
+
thing. A mock, timer or fixture you wrote yourself does not satisfy it:
|
|
126
|
+
list it as "unverified".`;
|
|
127
|
+
|
|
85
128
|
/**
|
|
86
129
|
* Rule 3, rule 4 and the final JSON a file-handing contract ends with.
|
|
87
130
|
* `ask` is rule 4's body after its number (the decision only a person can
|
|
@@ -119,6 +162,8 @@ or, if you are stopping to ask:
|
|
|
119
162
|
"progress" is REQUIRED on both shapes and REPLACES the previous one whole —
|
|
120
163
|
past tense, plain sentences, no card ids.
|
|
121
164
|
|
|
165
|
+
${WHOLE_REQUEST}
|
|
166
|
+
|
|
122
167
|
${HELP_THEM_DECIDE}`;
|
|
123
168
|
|
|
124
169
|
/**
|
|
@@ -158,7 +203,54 @@ export const LOOK_HEADLESS = `${NO_WINDOW} To look at your page, screenshot it
|
|
|
158
203
|
* so it posts to `parent` with `'*'`; the app accepts only its own frame's
|
|
159
204
|
* messages. A page with no handshake (every 0.105.0 bundle) is still viewed;
|
|
160
205
|
* it is just never offered a Download .glb.
|
|
206
|
+
*
|
|
207
|
+
* THE MANIFEST AND THE POSTER (2026-10-25, the owner's `Output Viewer Frame`;
|
|
208
|
+
* decision record `2026-10-25-flowviant-draws-standard-outputs`). Flowviant
|
|
209
|
+
* draws the exported GLB in its own viewer, and every control over it, so the
|
|
210
|
+
* page becomes a full-bleed canvas and its folder gains `view.json`, the
|
|
211
|
+
* manifest the web reads when present (packages/shared/src/modelViewer.ts —
|
|
212
|
+
* its literal here is `MODEL_VIEW_MANIFEST`, held to that schema by
|
|
213
|
+
* agentTaskKinds.test.mjs). It uploads on the general rule — an allowlisted
|
|
214
|
+
* type the turn changed is prepared and sent (intermediateArtifacts.mjs) — so
|
|
215
|
+
* only these words had been keeping it out. It proves no delivery: the page
|
|
216
|
+
* and its measured export still do.
|
|
217
|
+
*
|
|
218
|
+
* THE POSTER IS THE DAEMON'S RENDER OF THE PAGE (0.125.21). These words first
|
|
219
|
+
* asked the agent to screenshot its own `poster.png`, the still a list or a
|
|
220
|
+
* nested kit shows without loading a GLB; but a Claude model card cannot run
|
|
221
|
+
* Chrome under its fenced shell, and the daemon already renders every HTML
|
|
222
|
+
* artifact it uploads (artifactPreview.mjs `renderDesignPreview`), which the
|
|
223
|
+
* server stores as that artifact's preview. So the page's default view IS the
|
|
224
|
+
* poster, and the contract asks for that view instead: the whole model,
|
|
225
|
+
* full-bleed, nothing over it, standing within the render's few seconds.
|
|
226
|
+
* "poster" stays optional in the shape: a poster.png an agent already has
|
|
227
|
+
* still ships on the same general rule.
|
|
161
228
|
*/
|
|
229
|
+
export const MODEL_VIEW_MANIFEST = `{
|
|
230
|
+
"kind": "map",
|
|
231
|
+
"title": "Street and metro map",
|
|
232
|
+
"entry": "street-metro-map/index.glb",
|
|
233
|
+
"poster": "street-metro-map/poster.png",
|
|
234
|
+
"view": {
|
|
235
|
+
"units": { "metre": 1, "unit": "m" },
|
|
236
|
+
"upAxis": "Y",
|
|
237
|
+
"front": "+Z",
|
|
238
|
+
"defaultView": "overview",
|
|
239
|
+
"views": [
|
|
240
|
+
{ "id": "overview", "label": "Overview", "group": "overview", "direction": "iso" },
|
|
241
|
+
{ "id": "plan", "label": "Plan", "group": "overview", "direction": "top" },
|
|
242
|
+
{ "id": "konbini", "label": "Konbini", "group": "spot", "area": "Shops", "direction": "front", "part": "konbini" },
|
|
243
|
+
{ "id": "north-platform", "label": "North platform", "group": "spot", "area": "Station",
|
|
244
|
+
"camera": { "position": [42, 6, -18], "target": [42, 1, -4] } }
|
|
245
|
+
],
|
|
246
|
+
"layers": [
|
|
247
|
+
{ "id": "konbini", "label": "Konbini", "part": "konbini" },
|
|
248
|
+
{ "id": "station-platforms", "label": "Station platforms", "part": "station-platforms" }
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
"parts": ["konbini", "station-platforms"]
|
|
252
|
+
}`;
|
|
253
|
+
|
|
162
254
|
export const SYSTEM_AGENT_MODEL = `You are the human's own Claude, working one 3D MODEL card in a git worktree of
|
|
163
255
|
their repository. Nobody is watching this run. You have the repo and the tools
|
|
164
256
|
TOOLS ON THIS CARD names below — no board, no chat.
|
|
@@ -177,8 +269,9 @@ WHAT TO DO:
|
|
|
177
269
|
|
|
178
270
|
2. Write ONE viewer, .flowviant/artifacts/<kebab-name>/index.html — a short
|
|
179
271
|
kebab-case name for what it is (for example
|
|
180
|
-
.flowviant/artifacts/reading-chair/index.html). Keep the viewer self-contained
|
|
181
|
-
|
|
272
|
+
.flowviant/artifacts/reading-chair/index.html). Keep the viewer self-contained:
|
|
273
|
+
beside it, its folder holds only the view.json below (and a poster.png, if
|
|
274
|
+
you already have one), never scripts or notes. Flowviant materializes index.glb
|
|
182
275
|
beside this exact page before completing the card, through the handler below.
|
|
183
276
|
Keep original generated texture PNGs elsewhere under .flowviant/artifacts/;
|
|
184
277
|
preserve their bytes and material mapping. The page BUILDS
|
|
@@ -186,6 +279,9 @@ WHAT TO DO:
|
|
|
186
279
|
- three.js as ES modules from cdn.jsdelivr.net/npm, through an import map
|
|
187
280
|
for "three" and "three/addons/"; orbit controls, lights and a neutral
|
|
188
281
|
ground. Nothing else loads from the network, and nothing can be sent.
|
|
282
|
+
- the page is the scene and nothing else: a full-bleed canvas, with no
|
|
283
|
+
title, panel, legend, button or text over the model. Flowviant draws
|
|
284
|
+
every control.
|
|
189
285
|
- the model is ONE THREE.Group named for what it is, built from three.js
|
|
190
286
|
geometry (lathe, extrude, tube, box and the rest, one mesh per part),
|
|
191
287
|
with MeshStandardMaterial only. Colour, roughness and metalness are
|
|
@@ -228,6 +324,40 @@ WHAT TO DO:
|
|
|
228
324
|
|
|
229
325
|
${LOOK_HEADLESS}
|
|
230
326
|
|
|
327
|
+
THE PAGE'S DEFAULT VIEW IS THE POSTER FLOWVIANT SHOWS. Flowviant renders
|
|
328
|
+
this page itself when it uploads it, and that still is the model's picture
|
|
329
|
+
wherever the model is listed. So the page opens on the default view below:
|
|
330
|
+
the whole model, fitted full-bleed to the frame, with nothing over it — no
|
|
331
|
+
loading text, label or overlay — standing within a few seconds of load.
|
|
332
|
+
Take no poster yourself; a poster.png you already have may sit in the
|
|
333
|
+
folder, named in "poster" below.
|
|
334
|
+
|
|
335
|
+
Then write <kebab-name>/view.json — how Flowviant's viewer frames the
|
|
336
|
+
model, exactly in this shape (a map's, with every field):
|
|
337
|
+
|
|
338
|
+
${MODEL_VIEW_MANIFEST.replace(/^/gm, ' ')}
|
|
339
|
+
|
|
340
|
+
"kind" is "model" for one object, "map" for a scene that places several
|
|
341
|
+
kits. "entry" is a path under .flowviant/artifacts/: this folder's
|
|
342
|
+
index.glb. "poster" is optional: name this folder's poster.png only when
|
|
343
|
+
the folder holds one; otherwise leave the field out. Declare "units"
|
|
344
|
+
once, as the size of the scene's unit in metres —
|
|
345
|
+
{ "stud": 0.28, "unit": "m" } for a scene built in studs — and never
|
|
346
|
+
write units or any other text into the scene; Flowviant draws the scale
|
|
347
|
+
bar. "upAxis" is "Y" (three.js is Y-up); "front" is the side the model
|
|
348
|
+
faces: "+X", "-X", "+Z" or "-Z". "views": up to three "overview" views,
|
|
349
|
+
"defaultView" naming one; a map adds named "spot" views, each grouped
|
|
350
|
+
under its "area". A view looks from a "direction" ("iso", "top",
|
|
351
|
+
"front", "side" or "back") fitted to the whole scene or to one "part", or
|
|
352
|
+
from a "camera" position and target in scene units. "layers", for a map:
|
|
353
|
+
one per kit, its "part" the kit's scene node name exactly. "parts": every
|
|
354
|
+
node you name in code (.name, which GLTFExporter keeps); each "part"
|
|
355
|
+
above is one of them, spelled the same. A single model has no spots and
|
|
356
|
+
no layers: Flowviant gives it Display and Turntable. A label or area is a
|
|
357
|
+
few words, 40 characters at most; at most 40 views, 60 layers and 200
|
|
358
|
+
parts — a manifest past any limit is not read. The page's opening view
|
|
359
|
+
is the one "defaultView" names, so the poster and the viewer agree.
|
|
360
|
+
|
|
231
361
|
${ARTIFACT_CONTRACT_TAIL({
|
|
232
362
|
ask: `If you cannot model it without a DECISION only a person can make — which of
|
|
233
363
|
two directions, what size or style, which asset it stands beside — STOP AND
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Explicit iteration relationships, plus measured local deletions. No model runs here. */
|
|
2
|
+
import { REFERENCE_CAPTURE_FILE } from './referenceCaptureProtocol.mjs';
|
|
2
3
|
import { constants, openSync, closeSync, fstatSync, readFileSync, lstatSync, unlinkSync, realpathSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
|
3
4
|
import { join, dirname, resolve, sep } from 'node:path';
|
|
4
5
|
import { createHash } from 'node:crypto';
|
|
@@ -117,7 +118,7 @@ export function seenArtifacts(record, stored, present, bornAt = null) {
|
|
|
117
118
|
let seen = record;
|
|
118
119
|
if (!seen) {
|
|
119
120
|
const since = Number.isFinite(bornAt)
|
|
120
|
-
? stored.filter((a) => storedAt(a) >= bornAt).map((a) => a.name) : [];
|
|
121
|
+
? stored.filter((a) => storedAt(a) >= bornAt && !REFERENCE_CAPTURE_FILE.test(a.name)).map((a) => a.name) : [];
|
|
121
122
|
seen = since.some((n) => present.has(n)) ? new Set(since) : new Set();
|
|
122
123
|
}
|
|
123
124
|
return new Set([...seen, ...present].filter((n) => names.has(n) || present.has(n)));
|
|
@@ -297,7 +297,7 @@ export function createArtifactReporter({
|
|
|
297
297
|
|
|
298
298
|
/** Upload what this turn changed in `placeDir`. Resolves to the number of
|
|
299
299
|
* bodies SENT this call (delivered or held), for the tests and the log. */
|
|
300
|
-
const report = async ({ placeDir, before, sessionId, agentId, turnId, idle = false, requireStored = false, includeNames = null, requiredOutputs = null }) => {
|
|
300
|
+
const report = async ({ placeDir, before, sessionId, agentId, turnId, idle = false, requireStored = false, includeNames = null, requiredOutputs = null, referenceCapture = false }) => {
|
|
301
301
|
if (!placeDir || (!sessionId && !agentId)) return 0;
|
|
302
302
|
const changes = iterationChanges(placeDir, before);
|
|
303
303
|
const protectedNames = imageRevisionProtection(before?.stored, before?.imageRevision);
|
|
@@ -347,7 +347,7 @@ export function createArtifactReporter({
|
|
|
347
347
|
failures.push(`Required intermediate output ${name} is missing, unsafe or excluded from reporting; its handoff is unfinished.`);
|
|
348
348
|
let n = 0;
|
|
349
349
|
for (const entry of changed) {
|
|
350
|
-
const body = buildUpload(entry, { sessionId, agentId, turnId }, scrub, secretIn);
|
|
350
|
+
const body = buildUpload(entry, { sessionId, agentId, turnId, ...(referenceCapture ? { referenceCapture: true } : {}) }, scrub, secretIn);
|
|
351
351
|
if (required.has(entry.name) && (!body?.bytes || body.withheld || body.fields.sha256 !== required.get(entry.name))) {
|
|
352
352
|
failures.push(`Required intermediate output ${entry.name} is unreadable, withheld or changed since preparation. Restore its exact producer bytes and retry.`);
|
|
353
353
|
continue;
|
package/bin/lib/artifacts.mjs
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
* that cannot show an artifact must not have the CLI told it will.
|
|
75
75
|
*/
|
|
76
76
|
|
|
77
|
+
import { REFERENCE_CAPTURE_MANIFEST, REFERENCE_CAPTURE_FILE } from './referenceCaptureProtocol.mjs';
|
|
77
78
|
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync } from 'node:fs';
|
|
78
79
|
import { createHash } from 'node:crypto';
|
|
79
80
|
import { join } from 'node:path';
|
|
@@ -305,7 +306,7 @@ export const artifactStoredName = (name) => name.split('/').map(safeArtifactName
|
|
|
305
306
|
* re-sent exactly as it was first built (scrubbed once, never re-read from a
|
|
306
307
|
* disk the agent may have changed since).
|
|
307
308
|
*/
|
|
308
|
-
export function buildArtifactUpload(entry, { sessionId, agentId, turnId }, scrub = (s) => s, secretIn = () => null) {
|
|
309
|
+
export function buildArtifactUpload(entry, { sessionId, agentId, turnId, referenceCapture = false }, scrub = (s) => s, secretIn = () => null) {
|
|
309
310
|
const type = artifactTypeFor(entry.name);
|
|
310
311
|
const fields = {
|
|
311
312
|
...(sessionId ? { sessionId } : { agentId }),
|
|
@@ -313,6 +314,9 @@ export function buildArtifactUpload(entry, { sessionId, agentId, turnId }, scrub
|
|
|
313
314
|
name: artifactStoredName(entry.name),
|
|
314
315
|
bytes: String(entry.size),
|
|
315
316
|
};
|
|
317
|
+
// Reserved evidence names are writable only by the measured browser
|
|
318
|
+
// consumer. Model-authored JSON cannot become a page-access measurement.
|
|
319
|
+
if (REFERENCE_CAPTURE_FILE.test(entry.name) && !referenceCapture) return { withheld: 'measured-reference-capture-only', fields };
|
|
316
320
|
if (!type) return { fields, bytes: null }; // reported by name only
|
|
317
321
|
const maxBytes = artifactMaxBytesFor(entry.name);
|
|
318
322
|
if (entry.size > maxBytes) return { fields: { ...fields, tooLarge: '1' }, bytes: null };
|
|
@@ -328,7 +332,10 @@ export function buildArtifactUpload(entry, { sessionId, agentId, turnId }, scrub
|
|
|
328
332
|
const secret = secretIn(bytes);
|
|
329
333
|
if (secret) return { withheld: secret, fields };
|
|
330
334
|
} else {
|
|
335
|
+
const original = bytes;
|
|
331
336
|
bytes = Buffer.from(String(scrub(bytes.toString('utf8'))), 'utf8');
|
|
337
|
+
if (REFERENCE_CAPTURE_MANIFEST.test(entry.name) && !bytes.equals(original))
|
|
338
|
+
return { withheld: 'reference-capture-redaction', fields };
|
|
332
339
|
// A redaction marker can be longer than the value it replaced.
|
|
333
340
|
if (bytes.byteLength > maxBytes) {
|
|
334
341
|
return { fields: { ...fields, bytes: String(bytes.byteLength), tooLarge: '1' }, bytes: null };
|
package/bin/lib/claudeStream.mjs
CHANGED
|
@@ -198,6 +198,7 @@ export function handleStreamLine(line, options) {
|
|
|
198
198
|
for (const b of ev.message.content) {
|
|
199
199
|
if (b.type === 'tool_result') onToolEvent?.(null, {}, {
|
|
200
200
|
id: b.tool_use_id, result: true, isError: b.is_error === true,
|
|
201
|
+
toolSuccess: b.is_error !== true,
|
|
201
202
|
...(agentId ? { agentId } : {}),
|
|
202
203
|
output: typeof b.content === 'string' ? b.content : Array.isArray(b.content) ? b.content.slice(0, 8).filter(x => x?.type === 'text' && typeof x.text === 'string').map(x => x.text).join('\n') : '',
|
|
203
204
|
detail: typeof b.content === 'string' ? b.content : Array.isArray(b.content) ? b.content.filter(x => x?.type === 'text' && typeof x.text === 'string').map(x => x.text).join('\n') : '',
|
|
@@ -232,6 +233,12 @@ export function handleStreamLine(line, options) {
|
|
|
232
233
|
// alias, and this is the CLI naming what it chose. See turnModel.mjs.
|
|
233
234
|
const resolved = turnModelOf(ev.model);
|
|
234
235
|
if (resolved) onModel?.(resolved);
|
|
236
|
+
if (Array.isArray(ev.tools)) for (const tool of ['WebSearch', 'WebFetch']) {
|
|
237
|
+
onToolEvent?.('WebsiteAccess', { method: 'capability', tool }, {
|
|
238
|
+
id: `website-capability:${tool}`, result: true,
|
|
239
|
+
website: { access: 'unknown', availability: ev.tools.includes(tool) ? 'advertised' : 'unknown' },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
235
242
|
if (Array.isArray(ev.skills) || typeof ev.session_id === 'string' || Array.isArray(ev.mcp_servers)) {
|
|
236
243
|
onInit?.({
|
|
237
244
|
skills: Array.isArray(ev.skills) ? ev.skills.map(String) : undefined,
|
|
@@ -109,7 +109,7 @@ const table = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
|
109
109
|
* config.toml could not be read), and
|
|
110
110
|
* `unmounted` `[{ name, reason, source: 'project' | 'machine', partial? }]`.
|
|
111
111
|
*/
|
|
112
|
-
export function codexFencedMount(usable, expand, machineEnv, turnServers = {}) {
|
|
112
|
+
export function codexFencedMount(usable, expand, machineEnv, turnServers = {}, { includeMachine = true } = {}) {
|
|
113
113
|
const args = [];
|
|
114
114
|
const childEnv = {};
|
|
115
115
|
const mounted = [];
|
|
@@ -192,7 +192,7 @@ export function codexFencedMount(usable, expand, machineEnv, turnServers = {}) {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
const projectNames = new Set([...usable.map((t) => t.name), ...ownNames]);
|
|
195
|
-
const personal = machineCodexServers(machineEnv);
|
|
195
|
+
const personal = includeMachine ? machineCodexServers(machineEnv) : [];
|
|
196
196
|
for (const [name, server] of personal ?? []) {
|
|
197
197
|
if (projectNames.has(name) || server.enabled === false) continue;
|
|
198
198
|
if (typeof server.command !== 'string' && typeof server.url !== 'string') continue;
|
|
@@ -11,6 +11,7 @@ import { CODEX_CHILD_THREAD_RE } from './conversationIds.mjs';
|
|
|
11
11
|
import { followJsonl } from './jsonlTail.mjs';
|
|
12
12
|
import { codexCollabUpdates, codexSpawnResult, statusOf } from './codexSubagentEvents.mjs';
|
|
13
13
|
import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
|
|
14
|
+
import { isSealedMessage } from './orchestratorNative.mjs';
|
|
14
15
|
import { SUBAGENT_CAP } from './subagentBounds.mjs';
|
|
15
16
|
|
|
16
17
|
const CODEX = NATIVE_AGENT_TOOLS.codex;
|
|
@@ -84,7 +85,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
84
85
|
if (!COLLAB_CALLS.includes(name)) return;
|
|
85
86
|
let args;
|
|
86
87
|
try { args = JSON.parse(p.arguments); } catch { return; }
|
|
87
|
-
calls.set(p.call_id, { name, args });
|
|
88
|
+
calls.set(p.call_id, { name, args, namespace: p.namespace });
|
|
88
89
|
if (calls.size > 128) calls.delete(calls.keys().next().value);
|
|
89
90
|
const id = targetId(args.target ?? args.id);
|
|
90
91
|
if (id && name !== 'spawn_agent') {
|
|
@@ -122,7 +123,10 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
122
123
|
const title = args.task_name ?? args.description ?? output.agent_nickname ?? '';
|
|
123
124
|
if (args.task_name) rememberTarget(args.task_name, id);
|
|
124
125
|
if (typeof path === 'string') rememberTarget(path, id);
|
|
125
|
-
|
|
126
|
+
// A V2 message is ciphertext (orchestratorNative.mjs `isSealedMessage`):
|
|
127
|
+
// its words are unknown, so the child carries no brief rather than that.
|
|
128
|
+
const sealed = isSealedMessage(call.namespace ? `${call.namespace}.${call.name}` : call.name, args.message);
|
|
129
|
+
report({ id, kind: args.agent_type ?? '', title, ...(sealed ? {} : { brief: args.message ?? '' }), status: 'running', now: '' });
|
|
126
130
|
return;
|
|
127
131
|
}
|
|
128
132
|
if (owner !== root && ev.type === 'response_item' && p.type === 'custom_tool_call') {
|
|
@@ -21,17 +21,22 @@ export function knowledgeFetcher({ fleetUrl, token, userAgent, maxBytes = KNOWLE
|
|
|
21
21
|
// A kept library item (0.97.0) is its sibling, `GET /fleet/library/:id` —
|
|
22
22
|
// the same credential, the same shape, the same caps.
|
|
23
23
|
const libraryBase = fleetEndpoint('library', fleetUrl);
|
|
24
|
-
|
|
24
|
+
// THE CAP IS THE FILE'S (2026-10-25): the sync holds a kept model under
|
|
25
|
+
// models/ or designs/ to 32 MiB (`knowledgeMaxFor`) and hands that cap here;
|
|
26
|
+
// this default alone turned every 20–32 MiB GLB into the over-cap sentinel,
|
|
27
|
+
// a failed hash on every poll, so a map the Library kept never reached an
|
|
28
|
+
// agent.
|
|
29
|
+
return async (id, { library = false, preview = false, fileIndex = null, maxBytes: cap = maxBytes } = {}) => {
|
|
25
30
|
const suffix = preview ? '/preview' : Number.isSafeInteger(fileIndex) && fileIndex >= 0 ? `/file/${fileIndex}` : '';
|
|
26
31
|
const res = await fetch(`${library ? libraryBase : base}/${encodeURIComponent(id)}${suffix}`, {
|
|
27
32
|
headers: { Authorization: `Bearer ${token}`, 'User-Agent': userAgent },
|
|
28
33
|
signal: AbortSignal.timeout(60_000),
|
|
29
34
|
});
|
|
30
35
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
31
|
-
if (Number(res.headers.get('content-length') ?? '0') >
|
|
36
|
+
if (Number(res.headers.get('content-length') ?? '0') > cap) {
|
|
32
37
|
// Returned rather than thrown: over the cap is a REFUSAL, permanent for
|
|
33
38
|
// this rev, not a failure the backoff should keep retrying.
|
|
34
|
-
return Buffer.alloc(
|
|
39
|
+
return Buffer.alloc(cap + 1);
|
|
35
40
|
}
|
|
36
41
|
return Buffer.from(await res.arrayBuffer());
|
|
37
42
|
};
|
|
@@ -217,7 +217,7 @@ export async function syncKnowledge({ checkoutDir, manifest, fetchFile, maxBytes
|
|
|
217
217
|
sha256: f.sha256,
|
|
218
218
|
bytes: f.bytes,
|
|
219
219
|
maxBytes: knowledgeMaxFor(f.name, maxBytes),
|
|
220
|
-
fetch: () => fetchFile(f.id),
|
|
220
|
+
fetch: () => fetchFile(f.id, { maxBytes: knowledgeMaxFor(f.name, maxBytes) }),
|
|
221
221
|
});
|
|
222
222
|
// Stale or failed clears `ok` and is retried (a stale file is better than
|
|
223
223
|
// a missing one until the retry lands); a refusal is listed as one.
|
|
@@ -299,7 +299,7 @@ async function syncLibrary({ dir, items, fetchFile, maxBytes, keep, result }) {
|
|
|
299
299
|
sha256: it.sha256,
|
|
300
300
|
bytes: it.bytes,
|
|
301
301
|
maxBytes: itemMax,
|
|
302
|
-
fetch: () => fetchFile(it.id, { library: true }),
|
|
302
|
+
fetch: () => fetchFile(it.id, { library: true, maxBytes: itemMax }),
|
|
303
303
|
beforeWrite: () => ensureLibraryParents(dir, it.path),
|
|
304
304
|
});
|
|
305
305
|
if (recordSyncState(result, entryState, it.path) === 'refused') {
|
|
@@ -317,7 +317,7 @@ async function syncLibrary({ dir, items, fetchFile, maxBytes, keep, result }) {
|
|
|
317
317
|
sha256: it.preview.sha256,
|
|
318
318
|
maxBytes: 2 * 1024 * 1024,
|
|
319
319
|
oversizeFails: true,
|
|
320
|
-
fetch: () => fetchFile(it.id, { library: true, preview: true }),
|
|
320
|
+
fetch: () => fetchFile(it.id, { library: true, preview: true, maxBytes: 2 * 1024 * 1024 }),
|
|
321
321
|
beforeWrite: () => ensureLibraryParents(dir, it.previewPath),
|
|
322
322
|
});
|
|
323
323
|
recordSyncState(result, previewState, it.previewPath);
|
|
@@ -335,7 +335,7 @@ async function syncLibrary({ dir, items, fetchFile, maxBytes, keep, result }) {
|
|
|
335
335
|
sha256: String(f.sha256),
|
|
336
336
|
bytes: f.bytes,
|
|
337
337
|
maxBytes: knowledgeMaxFor(f.path, maxBytes),
|
|
338
|
-
fetch: () => fetchFile(it.id, { library: true, fileIndex: f.index }),
|
|
338
|
+
fetch: () => fetchFile(it.id, { library: true, fileIndex: f.index, maxBytes: knowledgeMaxFor(f.path, maxBytes) }),
|
|
339
339
|
beforeWrite: () => ensureLibraryParents(dir, f.path),
|
|
340
340
|
});
|
|
341
341
|
if (recordSyncState(result, fileState, f.path) === 'refused') wanted[sub].delete(rel);
|