Build a Polydance experiment
For an agent working in a project's own repository, building an experiment that Polydance puts in front of invited testers. This page is the whole loop: build it, hand it over, read what testers said, and publish the next revision. Each part says what exists today and what does not, so build against today, and neither wait for the rest nor pretend it is there.
It is published signed-out at polydance.net/docs, and the same text is
served as plain markdown at polydance.net/docs.md. In the Polydance
repository it is docs/BUILD-AN-EXPERIMENT.md. Roadmap is why the loop looks like
this, and Agent submission is the older conversion path, closed to new work.
An experiment answers one question about an interaction. It is judged by someone trying it for a couple of minutes and saying what they noticed. Polydance records what they say, attaches it to the exact revision they tried, and gives it back to you so the next revision can be better. Everything below follows from that loop.
Where things stand, 2026-09-16. Ball Drums, the first experiment authored to this page in another repository, is running in the pilot at its second revision. Handover is a message to an owner, who runs one copy script against your commit, and feedback reaches you through that owner. The packaging, submission API, project tokens and read API that the roadmap describes are not built; What is not built yet lists them so that nothing here is mistaken for an API.
Before you start#
A module, not a page. Polydance runs many experiments in one continuous session, so the host already owns the renderer, the camera rig, the XR session, the audio context, input routing, the microphone, the transcript and the navigation. Your module mounts into a scene group it is given and returns a frame callback. A self-contained HTML page with its own renderer cannot be used.
Never create a second renderer, XR session, animation loop, microphone, or an in-world panel that only repeats what the shell already shows. Never take over Next, Finish, Upvote, Bug found or the mic: a tester must always be able to leave. Your module is trusted code in the same page as the microphone stream, not a sandbox: a synchronous infinite loop or a GPU failure takes the host down with it.
Read what input you will actually get before designing the interaction. The table under What input gives you is the complete list, and it decides what is buildable. The first experiment built to this page designed a hover-and-drag interaction, found that the contract delivered a tap and nothing else, and shipped two taps instead. If the idea needs something the table does not offer, stop and say so in your write-up rather than reaching around the host. That is a gap for the SDK to fill, and saying so has so far got a gap closed within a day.
1. Build it#
Where the files go#
One directory per experiment in your own repository. Museverse uses experiments/<slug>/; the
layout is what matters, not the parent.
| File | What it is |
|---|---|
index.ts |
The module. Exports one factory of type ExperimentFactory. |
polydance.json |
The manifest: the object Polydance's catalogue holds, field for field. source.commit stays empty here, because a file cannot contain the hash of the commit that contains it; the handover fills it in. |
assets/ |
Every file the module loads, each declared in the manifest. |
README.md |
The one question the experiment tests, a revision table, where the assets came from, the gaps you hit, what is deliberately not built and what is unverified. A reviewer and the next agent read this first. |
Write the imports for where the file lands in Polydance, which today is
src/experiments/<slug>.ts:
import * as THREE from 'three';
import type { ExperimentFactory } from '../sdk';
import { label, sampler, spatial } from './kit';
Your own repository cannot compile this file, because ../sdk does not exist there. Keep the
directory outside your build, typecheck and lint globs; the file is compiled and checked inside a
Polydance checkout, as described under Check it. An @polydance/sdk alias that would
let one path form work in both places is a known gap, not something to write against yet.
The contract#
export const myExperiment: ExperimentFactory = async ctx => {
// ctx.root THREE.Group — add everything to this, and nothing outside it
// ctx.mode 'browser' | 'immersive-vr' | 'immersive-ar'
// ctx.audio AudioContext, already running
// ctx.signal AbortSignal — abandon async work when it fires
// ctx.interact(object, hit => {}) a tap: one callback when the object is selected
// ctx.hover(object, entered => {}) enter and leave, one object at a time, the nearest hit
// ctx.drag(object, {start, move, end}) a held press; move runs with the live ray
// ctx.status(text) one line in the shell, in every mode
// ctx.frame(point, distance?) ask the browser view to look somewhere; ignored in a session
// ctx.cleanup(fn) run fn on unmount
return {
update(time, delta) { /* called every frame */ },
dispose() { /* optional; the host disposes geometry, materials and textures for you */ },
};
};
Drive timing from update(time, delta), never setTimeout. A state machine on the host clock cannot
go stale and stops when the host stops. Allocate per-effect objects once and reuse them: modules are
mounted and unmounted repeatedly, and a leak fails the suite. An InstancedMesh owns its instance
buffers, which the host's disposal does not free, so call its dispose() from your own. Make every
interact target solid: a ray passes straight through the hole in a torus.
A complete module, small enough to read whole. Three spheres that brighten under a pointer and play
a note when tapped, using the SDK's built-in tone voice so it needs no asset:
import * as THREE from 'three';
import { tone, type ExperimentFactory } from '../sdk';
import { label } from './kit';
export const threeNotes: ExperimentFactory = ctx => {
const one = new THREE.Vector3(1, 1, 1); // allocated once, reused every frame
const spheres = [60, 64, 67].map((midi, i) => {
const material = new THREE.MeshStandardMaterial({ color: '#c5b9ec', emissive: '#c5b9ec', emissiveIntensity: .1 });
const mesh = new THREE.Mesh(new THREE.SphereGeometry(.12, 32, 16), material);
mesh.position.set((i - 1) * .5, 1.4, -1.6); ctx.root.add(mesh);
ctx.hover(mesh, entered => { material.emissiveIntensity = entered ? .5 : .1; });
ctx.interact(mesh, () => { tone(ctx, midi, (i - 1) * .6); mesh.scale.setScalar(1.3); ctx.status(`Note ${i + 1} of 3.`); });
return mesh;
});
const caption = label('TAP A SPHERE', .8, .08, '#a9b29d');
caption.position.set(0, 1.05, -1.6); ctx.root.add(caption);
ctx.frame(new THREE.Vector3(0, 1.3, -1.6), 2.2);
ctx.status('Tap a sphere to hear it.');
return { update(time, delta) { for (const s of spheres) s.scale.lerp(one, Math.min(1, delta * 6)); } };
};
What input gives you#
Read this before designing the interaction, because it decides what is buildable.
| Gesture | What arrives | Where it works |
|---|---|---|
| Tap | interact(object, hit => …), once per selection, with the intersection |
Everywhere |
| Hover | hover(object, entered => …), one object at a time |
Mouse, and any headset with a pointing ray. Never on a bare pinch with no ray, so nothing essential may hide behind it |
| Press and drag | drag(object, { start, move, end }), with the live ray |
Mouse, controllers, tracked hands |
A press becomes a drag only once it has moved, so the same object can carry both a tap and a drag
without the drag swallowing the tap. move is not called at a steady rate. In a session it runs
every frame the press is held; on a desktop it runs only when the pointer actually moves, so it stops
ticking while a held pointer is still. Anything that must keep running belongs in update, and a
watchdog counting missed calls is safe in a session and wrong in a browser, so gate one on
ctx.mode. move hands you the ray rather than a point, and you project it onto whatever surface
the interaction lives on; the host will not guess a depth for you. The ray is the host's own and is
valid only inside the call.
The shell refuses a press before the experiment sees it, so a press that lands on the panel or the voice orb never arrives. There is no pinch strength, no two-handed gesture and no squeeze: squeeze belongs to the voice orb. Ask for what you need rather than working around its absence.
The kit#
Optional helpers, already solved by earlier experiments. Nothing here is privileged and you may ignore all of it.
| Helper | What it gives you |
|---|---|
label(text, w, h, colour) |
A caption that repaints in place with .set(text). Never rebuild a mesh to change text. |
sampler(ctx, 'shared/marimba.opus') |
A decoded sample played pitched, every voice stopped on abort. |
spatial(ctx, x, y, z) |
An HRTF-panned point in the room, disconnected on abort. |
midiNotes(bytes) |
Note-ons in beats, first note at beat 0, plus until() for looping. |
discZone(point, centre, normal, r, inner) |
Which concentric zone of a facing surface a strike hit. |
There is no physics and no model loader in the kit. The first experiment carried its own sphere against box solver, which was fine; the next idea that wants a mesh collider or a loader should say so rather than build one, since that is where the kit grows.
Project-specific language, such as Museverse's hit windows and star thresholds, belongs in your own repository beside the experiment, not in this kit.
The manifest#
One entry describing the experiment for testers and for the owner's dashboard.
| Field | Meaning |
|---|---|
id |
Stable slug, lowercase and hyphens. It identifies the experiment across every revision. |
revision |
An ordinal as a string, bumped when behaviour, assets or questions change. Never edited in place to mean something else. |
priorRevisions |
Every revision this experiment has already published. Keep the old value here when you bump, so a tester part-way through the previous version can still submit what they said about it. |
revisions |
Every pass over this experiment, newest first with the published one at the head: the revision, the date at as YYYY-MM-DD, and a summary of what that pass changed. This is what tells a creator an agent has been back and what it did — their dashboard shows it beside the feedback that pass received. It has to describe the same revisions the save gate accepts: the head is revision, and the rest are exactly priorRevisions. |
title, summary, description |
Shown before and during. Neutral: say what it is, not how good it is. |
owner, category, duration, space |
The project's owner key, where it sits, how long, what room it needs. |
modes |
browser, immersive-vr, immersive-ar. AR must render without an opaque background. |
required, optional |
select, hands, controllers, gaze, transient-pointer, midi, touchpad. These say what a tester's setup must offer, not what the SDK delivers: the input table above is that list. |
questions |
One to three, or an empty list for the standard prompts. See below. |
assets |
Every file the module loads, as <slug>/<file>, relative to the experiment asset root. |
source |
Where the idea came from: url, the full 40-character commit, and notes on what is and is not done. |
accent |
One colour, used on the card. |
Abbreviated from Ball Drums, the shape to copy:
{
"id": "ball-drums",
"revision": "2",
"priorRevisions": ["1"],
"revisions": [
{ "revision": "2", "at": "2026-09-16", "summary": "Placement became a drag: a drum is dragged into the falling stream and locked where it is let go…" },
{ "revision": "1", "at": "2026-09-15", "summary": "First build: the spawner, a 24-ball pool, fixed-step physics and seven drum voices, placed by tapping…" }
],
"title": "Ball Drums",
"summary": "Drop a stream of balls through drums you place. Hear what falls out.",
"description": "A spawner above and in front of you drops golf-ball-sized balls at a steady rate. … Can a repeating beat be built by placing drums in a falling stream?",
"owner": "Museverse",
"category": "Rhythm & play",
"duration": "3–5 min",
"modes": ["browser", "immersive-vr", "immersive-ar"],
"required": [],
"optional": ["hands", "controllers"],
"questions": [
"Place a few drums in the stream. What do you pay attention to?",
"How does dragging a drum into place feel?",
"What did you end up making, and what would you change?"
],
"assets": ["ball-drums/kick.opus", "ball-drums/snare.opus", "ball-drums/HHclosed.opus"],
"source": {
"url": "https://github.com/Museverse-Labs/museverse-web/pull/74",
"commit": "",
"notes": "Authored to the Polydance contract in museverse-web experiments/ball-drums. Revision 2 changes the gesture and nothing else: … Not built: ball-to-ball collisions, drum rotation, any scoring. Unverified on any headset."
},
"space": "Seated or standing · the drum table is to your right · headphones recommended",
"accent": "#d7b7ff"
}
The checker refuses an id with anything outside lowercase letters, digits and hyphens; more than
three questions, or one longer than 250 characters; a priorRevisions list that repeats a value or
contains the current revision; a revisions list that is empty, is not headed by the current
revision, disagrees with priorRevisions, or has an entry missing its summary or its YYYY-MM-DD
date; an owner with no registered project; a source.commit that is not
40 hex characters; and an asset path that starts with /, contains .., or is not on disk.
Questions are asked of a tester while they play, so keep them neutral. "Play along, and tell us what you pay attention to" invites an observation. "How much more engaging was the spatial band?" presumes the answer and poisons the result. Your hypothesis belongs in the brief, not in front of the tester. One to three questions, short.
Assets#
Declare everything the module loads. Assets are fetched through the kit by name, never by a path into
another application's folders, and never from a mutable preview URL: an experiment has to keep working
long after the branch it was built on has gone. Pass ctx.signal to every fetch. Record the original
path, licence and commit for anything vendored from elsewhere, and if the licence is unknown, say so
in your README rather than leave the row blank.
Your assets/ directory lands at public/experiments/<slug>/ in Polydance, so a file declared as
ball-drums/kick.opus is loaded with sampler(ctx, 'ball-drums/kick.opus'). A shared pack of
instrument voices already exists under shared/; reuse a shipped sample rather than synthesise a
voice that exists as one.
Capabilities are declarations, not guesses#
Declare what an interaction needs and what it can fall back to. Polydance keeps an experiment visible on a device that cannot run it, with a plain reason, rather than hiding it. "Not detected", "not granted" and "not yet tested" are different states and an unobserved capability is not an unsupported device. If your idea needs something the contract does not expose, such as hand joints or a connected instrument, stop and say so rather than reaching around the host. That is a gap to fill in the SDK, and saying so is more useful than a workaround.
Check it#
The checks run against a Polydance checkout with the module in place, because your repository has no host to mount it in. With access to the repository, which is private, the recipe the first experiment used:
git clone https://github.com/robin-blocks/polydance2 && cd polydance2 && npm install
npm run import:experiment -- <your-repo> <commit> experiments/<slug>
npm run check:experiments # manifest valid, assets present, provenance recorded
npm run build # tsc, then Vite
npm test
The import is the same step the handover runs, described under Submit it: it copies
the module and the assets out of that commit and registers them. It reads a commit rather than your
working tree, so commit first. While you are still iterating, copying index.ts to
src/experiments/<slug>.ts and assets/ to public/experiments/<slug>/ by hand is the same thing;
the script exists to make the handover exact, not to slow down a loop.
To see it, npm run dev:desktop serves plain HTTP at http://localhost:5174. It needs a .env.local
with LOCAL_DEV_AUTH=1 and an AUTH_SECRET of at least 32 characters; with no cloud keys in that
file the sign-in screen offers Open local preview and every save stays in a gitignored local file.
The browser suite, npm run test:browser, mounts every experiment fifty times and fails on a GPU
resource leak; it needs the HTTPS server from npm run dev on port 5173, or POLYDANCE_TEST_URL
pointed at another. Working beside somebody else's checkout, use a scratch git worktree and a port
nobody else is on.
Without access, hand the experiment over as described next: Polydance runs the same checks before it publishes and reports back. Either way, none of this is headset evidence. Desktop checks prove a module mounts, sounds and disposes. Quest 3 and visionOS Safari remain separate, dated, manual results, and "unverified on device" is the useful sentence.
2. Submit it#
Today, by message; the copy itself is a script. The module and its manifest are copied into Polydance verbatim. Copying bytes is fine; rewriting is the thing that ruins the feedback, because a tester's bug then describes the rewrite rather than the idea. Polydance takes your files exactly as committed, so the revision testers try is the one your commit names.
Commit it in your repository, under your repository's own rules for an experiment. Museverse opens one pull request per experiment, on its own branch. The commit has to stay fetchable: Polydance records it, and the next revision starts from it.
Hand it over. Polydance needs three things: the repository, the full 40-character commit, and the directory. Today that is a message to the Polydance owner, or a pull request against the Polydance repository if you have access. There is no endpoint to call.
What Polydance does with it, so you can predict it and check it. One script does the copy and the registration, from the three things you handed over and nothing else:
npm run import:experiment -- <repo-path> <commit> experiments/<slug>It copies
index.tstosrc/experiments/<slug>.tsand everything underassets/topublic/experiments/<slug>/byte for byte; inserts or replaces thepolydance.jsonobject insrc/catalogue.tswithsource.commitfilled from your commit, the only field that changes in transit; and adds the loader line tosrc/experiments/index.ts, naming the one exportedExperimentFactoryit finds in your module. It refuses rather than guesses: a manifest carrying a field the catalogue has no place for or arevisionsentry it could not hold, a module exporting two factories or none, a declared asset that is not in the commit, a branch or tag in place of a commit, and the overwrite rule under Publish an update. The entry is the one thing that cannot cross as bytes, because your manifest is JSON and the catalogue is TypeScript, so it is written out by a small formatter in the catalogue's own style, field for field and in its field order. One field can come from somewhere other than your manifest: a manifest written beforerevisionsexisted keeps the list the catalogue already holds, and a new experiment without one is refused.An owner still does the rest by hand: record the source in
docs/SOURCE-PROVENANCE.md, run the checks, and push tomain, which deploys production. The Polydance commit that carried it is the one to cite in your README.Where it appears. At the project's own site,
https://<project>.polydance.net, and in the multi-project view atpolydance.net. Invited testers reach it through Next, which offers each tester the experiments they have not yet left feedback on before the ones they have. There is no per-revision URL yet, so the demo link is the project site.A new project is a conversation first. Before its first submission a project is registered in Polydance with an owner key, a display name, a logo and a subdomain. Museverse is registered.
Nothing comes back automatically. Ask for the Polydance commit and the date it deployed, and put them in your revision table.
Later, by submission. Your repository's CI packs the module with its declared assets and submits it with a project token, Polydance validates and publishes it, and a per-revision URL comes back as the demo link. That is milestone M4 in the roadmap. Do not script against it: neither the CLI nor the endpoint exists.
3. Read the feedback#
What Polydance records#
One exposure is one tester, one experiment, one revision, one mode, from the moment the experiment opened. It becomes a record the first time the tester leaves something in it, a spoken line, an upvote or a bug flag, and never otherwise: an experiment that was opened and left behind leaves nothing. Next or Finish submits it to the owner.
| Field | What it holds |
|---|---|
experimentId, revision |
Your slug and the revision the tester started on. A tester who opened revision 1 stays on revision 1 for that exposure, even if revision 2 publishes meanwhile. |
segments |
The transcript: each line the tester said, as text with a timestamp, from live speech-to-text. The raw wording is kept and never summarised. No audio is stored anywhere. |
upvote |
One toggle per exposure: the tester wants to see more of this. |
bugFound |
One boolean per exposure. There is no bug note; if the tester said what happened, it is in the transcript. |
question |
The questions on screen at the time, so an answer can be read against what was asked. |
mode |
browser, immersive-vr or immersive-ar. |
device |
The browser's user-agent string. |
hostBuild |
The Polydance commit the shell was running. A bug may be the host's rather than yours; this is how the two are told apart. |
origin |
participant or automation, decided by the server from the request. Automated runs are shown but never counted. |
startedAt |
When the exposure opened. |
What is never recorded: microphone audio; head or hand pose; what the tester looked at or selected; anything from onboarding, which is practice. Testers can delete any line at any time, including after sharing, and a deletion propagates. Submitted transcripts are kept for 90 days, after which the text is cleared and the flags and counts remain; a draft that was never submitted is cleared after 24 hours.
Where it is#
The creator dashboard, a tab on the project's site for signed-in owners. It shows shared sessions, upvotes and bug flags, counting participants only, with automated runs listed underneath and labelled. Each session is one card: experiment and revision, tester, mode, shell build, the flags, the questions shown, and the transcript, or "No spoken feedback shared" when the tester explored muted. Drafts and practice never appear.
Signing in needs an invited owner's email and a code sent to it, so an agent has no way in today, and there is no export. Feedback reaches you through the project's owner, who reads the dashboard and relays it. Ask for observations, not conclusions: the second revision of the first experiment was built from one relayed sentence, and its author said so. A read API for the project's own feedback is roadmap milestone M4 and is not built.
How to read it#
- Read against the revision. Every card names the revision it describes. Feedback on revision 1 says nothing about a change made in revision 2.
- Read against the question shown. An answer is to the prompt that was on screen. If a question changed between revisions, the answers to it are not comparable, and only the unchanged ones are.
- A bug flag is a flag, not a report. Read the transcript for what happened, and compare
hostBuildbefore assuming it is yours: the shell and the module share one page. - Counts need denominators. Three upvotes from three testers and three from thirty are different facts. With a handful of testers, a rate proves nothing; a repeated observation does.
- Muted is normal. Voice is optional and needs a microphone grant. A card with flags and no transcript is a tester who explored muted, not a broken session.
- Mode and device matter. An interaction that read badly on a mouse may read well on a controller. "Unverified on device" in your own README is the honest status until someone has tried it there.
What you may do with it#
Raw feedback stays in Polydance. Whatever you are given, never quote a tester into a commit message, a pull request, a README or a retro. Paraphrase the observation, count it, and refer to the Polydance session. A tester can delete what they said, and a deletion cannot reach a commit. Decisions, whether to pursue, iterate or park, are the owner's; write down what was observed and what you changed because of it.
4. Publish an update#
A revision is immutable. Anything that changes what a tester experiences, behaviour, assets or questions, is a new revision of the same experiment, never an edit of the current one.
Change the manifest#
| Field | What to do |
|---|---|
id |
Never changes. It is how feedback on every revision stays attached to one experiment. |
revision |
The next ordinal, as a string: "1" becomes "2". |
priorRevisions |
Append the revision you are replacing and keep every earlier one: ["1"], then ["1", "2"]. Polydance keeps accepting feedback for each of them, so a tester part-way through the old revision when the new one deploys can still submit what they said about it. |
revisions |
Add this pass at the head and leave every earlier entry as it stands: the new ordinal, the date you hand it over, and a sentence or two on what changed. The creator reads this next to the feedback the pass receives, so it is where they find out what you did and whether anyone has tried it since. |
source.notes |
Rewrite for this revision: what changed, what is deliberately unchanged, what is still not built, and what is unverified. Someone reading a disappointing session needs to know whether the idea or the change is being judged. |
source.commit |
Stays empty; the handover fills it with the new commit. source.url stays the same pull request if the work continued there. |
questions |
Keep the wording unless the interaction it asks about has changed. When you do change one, say so in source.notes and in your README: nothing records it for you yet, and a comparison across revisions would otherwise mix answers about two different things. |
assets |
Add and remove as the module does; the handover copies the whole directory over and names anything left behind by the revision before. |
required, optional |
Update if the interaction now uses a different input. |
Then update your README's revision table, and hand the commit over exactly as for a first submission: repository, commit, directory. Polydance replaces the module and the assets, updates the catalogue entry, records the new commit, runs the checks, and deploys.
The priorRevisions rule is enforced rather than remembered. The import refuses to overwrite a
revision the catalogue already holds with a different commit unless your manifest lists that
revision in priorRevisions, because that is the difference between publishing an update and
editing a revision testers have already been given.
What testers see#
- The new revision counts as unseen. Next offers it ahead of everything a tester has already left feedback on, so everyone who tried the previous revision is offered this one.
- Code is not pinned per revision yet. Once the new revision deploys, every fresh page load runs it, including for a tester who has not finished the old one. Their open exposure stays recorded against the revision it started on, and its saves are still accepted.
- There is no "please retest" flag, no per-revision link and no comparison view. Compare by hand from the dashboard cards, which name the revision on each.
One change per revision#
Ball Drums' second revision changed its gesture from two taps to a drag, and nothing else: not the placement frame it had measured as too narrow, because changing both would have made the two revisions incomparable. Question 2, which asked about the two taps, was reworded and recorded as reworded, so the comparison rests on questions 1 and 3. That is the shape to copy.
What is not built yet#
None of these exist. Design for today, and mention nothing below as if it worked.
- Packaging, artifact digests and a
polydanceCLI withvalidate,packandsubmit. - A submission endpoint and project tokens; a read API for your project's feedback.
- Per-revision URLs, revision-pinned code, a "retest" flag, and a comparison view across revisions.
- Decisions and observations stored in Polydance against a revision.
- An
@polydance/sdkpackage or import alias; SDK versioning. - Anything automatic about the handover itself. The copy step is a script an owner runs by hand against a checkout of your repository: it is not an endpoint, it does not fetch, and nothing starts it but a person.
Gaps reported so far#
Report gaps as a numbered, dated list in your README, with "closed since the last revision" and "found while building this one" as separate sections, so a closure can be tied to the report. What has been reported against this page:
| # | Reported | Gap | Status |
|---|---|---|---|
| 1 | 2026-09-15 | No hover, no press-and-drag, no ray between events: interact was a tap and nothing else. |
Closed 2026-09-16: ctx.hover and ctx.drag, above. |
| 2 | 2026-09-16 | On a desktop, dragging an object also orbited the camera. | Closed 2026-09-16 in the host. |
| 3 | 2026-09-16 | A press was ended only by an explicit release; a lost input source or a release off the canvas stranded it. | Closed 2026-09-16 in the host. |
| 4 | 2026-09-16 | move runs every frame in a session but only on pointer movement in a browser. |
Documented under input, above. By design; gate any timer built on move on ctx.mode. |
| 5 | 2026-09-15 | Which import path a verbatim copy should use. | Documented above. An alias is not built. |
| 6 | 2026-09-15 | source.commit cannot be known by the module the commit contains. |
Closed 2026-09-16: leave it empty. npm run import:experiment fills it from the commit handed over, and refuses a manifest that names a different one. |
| 7 | 2026-09-15 | No physics and no model loader in the kit. | Open. Added when an experiment needs one. |
| 8 | 2026-09-15 | Assets have to be vendored per experiment. | By design. Packaging will formalise it. |
| 9 | 2026-09-16 | Tester feedback is not readable from the project's own repository. | Open. Relayed by the owner until the read API. |
| 10 | 2026-09-16 | A changed question is not recorded in a machine-readable way. | Open. Say it in source.notes. |
polydance