eleven / ocho Public

web: the apps directory holds apps, and nothing else (#1008)

Nine of the .js files under web/static/apps/ never called defineApp. They
were shared machinery that drifted in next to the apps that use it, which
made the directory a poor index of what an app actually IS — you couldn't
list it and see the apps.

They move to web/static/, alongside the other shared modules (crypto.js,
format.js, ui.js, lightbox.js, media.js), imported from an app as
../<name>.js:

  eleven.js       Eleven's scripted-conversation feel (setup, welcome)
  flow.js         the conversation-flow engine
  setup.flow.js   the setup conversation as data
  welcome.flow.js the welcome conversation as data
  quickform.js    the quick-form planner (shell.js + onboardingui.js use it)
  schema-engine.js  the app-schema interpreter
  schema-render.js  its DOM renderer
  tutorial.js     /tutorial — a floor command, not an app
  vaultcrypto.js  the Vault lock layer's crypto

Pure moves: no behaviour changes, only import paths. Apps are imported
explicitly (shell.js lists them; nothing globs the directory), so this is a
mechanical path rewrite — the existing tests import six of the nine and
would fail loudly on a stale path.

tutorial.js is the one judgement call: it registers a floor command rather
than defining an app, so by the rule it moves.

A new test (test/apps-dir.test.mjs) enforces the contract, matching how the
flow documents are kept JSON-pure. It matches a defineApp *call*, not a
mention — schema-render.js describes the framework in prose and would have
slipped past a substring check.
Co-authored-by: Claude <noreply@anthropic.com>
James Adam 704cac887bd5491e12db3c7d22cdee3d1b464876 parent a2dafa6
31 files changed, +76 −39
  • docs/app-schema.md +1 −1
  • docs/conversation-scripts.md +1 −1
  • docs/conversational-onboarding.md +1 −1
  • ios/LChatCore/Sources/LChatCore/FlowEngine.swift +2 −2
  • ios/LChatCore/Sources/LChatCore/SchemaEngine.swift +1 −1
  • package.json +1 −1
  • test/apps-dir.test.mjs +30 −0
  • test/flow.test.mjs +4 −4
  • test/quickform.test.mjs +3 −3
  • test/schema-engine.test.mjs +2 −2
  • test/tutorial.test.mjs +1 −1
  • test/vaultcrypto.test.mjs +2 −2
  • test/views.test.mjs +1 −1
  • web/static/apps/README.md +8 −1
  • web/static/apps/remind.js +1 −1
  • web/static/apps/setup.js +4 −4
  • web/static/apps/todo.js +1 −1
  • web/static/apps/vault.js +1 −1
  • web/static/apps/views.js +1 −1
  • web/static/apps/welcome.js +4 −4
  • web/static/{apps => }/eleven.js +1 −1
  • web/static/{apps => }/flow.js +0 −0
  • web/static/onboardingui.js +1 −1
  • web/static/{apps => }/quickform.js +0 −0
  • web/static/{apps => }/schema-engine.js +0 −0
  • web/static/{apps => }/schema-render.js +0 −0
  • web/static/{apps => }/setup.flow.js +0 −0
  • web/static/shell.js +2 −2
  • web/static/{apps => }/tutorial.js +1 −1
  • web/static/{apps => }/vaultcrypto.js +1 −1
  • web/static/{apps => }/welcome.flow.js +0 −0
diff --git a/docs/app-schema.md b/docs/app-schema.md
index fbd06b9..8293128 100644
--- a/docs/app-schema.md
+++ b/docs/app-schema.md
@@ -6,7 +6,7 @@ A mini-app's behaviour has always been specified twice: the JS app file
large class of apps: a single JSON file that *formally describes how the app
looks and behaves*, interpreted by both runtimes:
-- **JS engine + DOM renderer:** `web/static/apps/schema-engine.js` and
+- **JS engine + DOM renderer:** `web/static/schema-engine.js` and
`schema-render.js` (the app file wires `schemaEvents`/`schemaRender` into
defineApp and keeps only the glue a schema doesn't cover: commands, sidebar
preview/unread, chat wording, named parsers)
diff --git a/docs/conversation-scripts.md b/docs/conversation-scripts.md
index 9fd0887..709770b 100644
--- a/docs/conversation-scripts.md
+++ b/docs/conversation-scripts.md
@@ -2,7 +2,7 @@
Status: **phase 1 complete — schema v1 is frozen against reality.** Both
shipped conversations are pure data (`setup.flow.js`, `welcome.flow.js`)
-interpreted by the engine (`web/static/apps/flow.js`); `test/flow.test.mjs`
+interpreted by the engine (`web/static/flow.js`); `test/flow.test.mjs`
drives full transcripts of both in Node with a fake io and enforces JSON
purity, so the flow files are shippable fixtures for the Swift interpreter.
Next: phase 2, serving the scripts from the binary. Design captured
diff --git a/docs/conversational-onboarding.md b/docs/conversational-onboarding.md
index a6b4717..b17dd2f 100644
--- a/docs/conversational-onboarding.md
+++ b/docs/conversational-onboarding.md
@@ -5,7 +5,7 @@ app v1 — landed with this doc); later phases still proposed. Captured from
design discussion 2026-07-06.
Builds on `onboarding-and-home-server.md` (welcome page, home-server vault,
fragility banner — phases 1–3a shipped) and the `/tutorial` mini-app
-(`web/static/apps/tutorial.js`), whose client-driven, in-Eleven's-voice pattern
+(`web/static/tutorial.js`), whose client-driven, in-Eleven's-voice pattern
this design generalises.
> **Follow-on: `conversation-scripts.md`.** With iOS/Mac moving to fully
diff --git a/ios/LChatCore/Sources/LChatCore/FlowEngine.swift b/ios/LChatCore/Sources/LChatCore/FlowEngine.swift
index c1dde4d..abb0fc5 100644
--- a/ios/LChatCore/Sources/LChatCore/FlowEngine.swift
+++ b/ios/LChatCore/Sources/LChatCore/FlowEngine.swift
@@ -1,6 +1,6 @@
import Foundation
-// 🤖 The Swift twin of web/static/apps/flow.js: the interpreter for the
+// 🤖 The Swift twin of web/static/flow.js: the interpreter for the
// conversation scripts (docs/conversation-scripts.md) — the JSON documents
// that ARE Eleven's onboarding conversations (setup.flow.js, welcome.flow.js).
// The docs promised "the document the server will ship to native
@@ -145,7 +145,7 @@ public final class FlowEnv {
// original — never clean a name to nothing. Prefix lists are matched on a
// lowercased, straight-apostrophe copy (iOS keyboards type ’), longest
// preamble first so "i'm called" wins over "i'm".
-// Twin: cleanNameAnswer in web/static/apps/flow.js.
+// Twin: cleanNameAnswer in web/static/flow.js.
private let nameGreetings = ["hiya", "howdy", "hello", "hey", "hi"]
private let namePreambles = [
"you can call me", "everyone calls me", "people call me", "they call me",
diff --git a/ios/LChatCore/Sources/LChatCore/SchemaEngine.swift b/ios/LChatCore/Sources/LChatCore/SchemaEngine.swift
index ea9c23b..05d658d 100644
--- a/ios/LChatCore/Sources/LChatCore/SchemaEngine.swift
+++ b/ios/LChatCore/Sources/LChatCore/SchemaEngine.swift
@@ -1,6 +1,6 @@
import Foundation
-// 🤖 The Swift twin of web/static/apps/schema-engine.js: the interpreter for
+// 🤖 The Swift twin of web/static/schema-engine.js: the interpreter for
// the declarative app schema (docs/app-schema.md) that renders To-do and
// Reminders (and any schema-described app) with native components. The two
// engines MUST behave identically — they are drift-gated by the shared event
diff --git a/package.json b/package.json
index 4394afe..2f712c0 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,6 @@
"private": true,
"type": "module",
"scripts": {
- "test": "node --test test/module-refs.test.mjs && node test/crypto.test.mjs && node test/vaultcrypto.test.mjs && node test/integrity.test.mjs && node test/ui.test.mjs && node test/blurhash.test.mjs && node test/format.test.mjs && node test/markdown.test.mjs && node test/composer.test.mjs && node test/composer-vectors.test.mjs && node test/bootscene.test.mjs && node test/message.test.mjs && node test/threadkey.test.mjs && node test/nicknames.test.mjs && node test/messageblob.test.mjs && node test/blobbytes.test.mjs && node test/nativeblob.test.mjs && node test/apps.test.mjs && node test/cards.test.mjs && node test/event-vectors.test.mjs && node test/views.test.mjs && node test/schema-engine.test.mjs && node test/flow.test.mjs && node test/quickform.test.mjs && node test/tutorial.test.mjs && node test/actions.test.mjs && node test/commands.test.mjs && node test/context.test.mjs && node test/knowledge.test.mjs && node test/invite.test.mjs && node test/backup.test.mjs && node test/connect.test.mjs && node test/serviceop.test.mjs && node test/github.test.mjs && node test/github-render.test.mjs && node test/linear.test.mjs && node test/linear-render.test.mjs && node test/check-app.test.mjs && node test/sandbox.test.mjs && node test/sandbox-bridge.test.mjs && node test/status-contract.test.mjs && node test/store.test.mjs && node test/memory.test.mjs && node test/search.test.mjs && node home/test/crypto.test.mjs && node home/test/restore-payload.test.mjs && node home/test/serverlist.test.mjs && node home/test/ceremony-errors.test.mjs && node home/test/fader.test.mjs"
+ "test": "node --test test/module-refs.test.mjs && node test/apps-dir.test.mjs && node test/crypto.test.mjs && node test/vaultcrypto.test.mjs && node test/integrity.test.mjs && node test/ui.test.mjs && node test/blurhash.test.mjs && node test/format.test.mjs && node test/markdown.test.mjs && node test/composer.test.mjs && node test/composer-vectors.test.mjs && node test/bootscene.test.mjs && node test/message.test.mjs && node test/threadkey.test.mjs && node test/nicknames.test.mjs && node test/messageblob.test.mjs && node test/blobbytes.test.mjs && node test/nativeblob.test.mjs && node test/apps.test.mjs && node test/cards.test.mjs && node test/event-vectors.test.mjs && node test/views.test.mjs && node test/schema-engine.test.mjs && node test/flow.test.mjs && node test/quickform.test.mjs && node test/tutorial.test.mjs && node test/actions.test.mjs && node test/commands.test.mjs && node test/context.test.mjs && node test/knowledge.test.mjs && node test/invite.test.mjs && node test/backup.test.mjs && node test/connect.test.mjs && node test/serviceop.test.mjs && node test/github.test.mjs && node test/github-render.test.mjs && node test/linear.test.mjs && node test/linear-render.test.mjs && node test/check-app.test.mjs && node test/sandbox.test.mjs && node test/sandbox-bridge.test.mjs && node test/status-contract.test.mjs && node test/store.test.mjs && node test/memory.test.mjs && node test/search.test.mjs && node home/test/crypto.test.mjs && node home/test/restore-payload.test.mjs && node home/test/serverlist.test.mjs && node home/test/ceremony-errors.test.mjs && node home/test/fader.test.mjs"
}
}
diff --git a/test/apps-dir.test.mjs b/test/apps-dir.test.mjs
new file mode 100644
index 0000000..f708e6f
--- /dev/null
+++ b/test/apps-dir.test.mjs
@@ -0,0 +1,30 @@
+// The apps-directory contract: web/static/apps/ holds apps and nothing else.
+// Every .js file there calls defineApp — one app per file — so listing the
+// directory lists the apps. Shared machinery an app leans on (the schema
+// engine + renderer, the flow engine, Eleven's scripted-conversation feel, the
+// Vault's lock crypto) is a sibling module in web/static/, imported as
+// ../<name>.js. Helpers drifted in here once and made the directory a poor
+// index of what an app IS; this test keeps it honest.
+// node test/apps-dir.test.mjs
+import assert from "node:assert/strict";
+import { readFileSync, readdirSync } from "node:fs";
+
+const dir = new URL("../web/static/apps/", import.meta.url);
+const files = readdirSync(dir).filter((f) => f.endsWith(".js")).sort();
+
+// A defineApp CALL, not a mention: schema-render.js documented the framework in
+// prose and would have passed a bare substring match.
+const CALLS_DEFINE_APP = /\bdefineApp\s*\(/;
+
+const notApps = files.filter((f) => !CALLS_DEFINE_APP.test(readFileSync(new URL(f, dir), "utf8")));
+assert.deepStrictEqual(
+ notApps,
+ [],
+ `web/static/apps/ must contain only apps, but these call no defineApp: ${notApps.join(", ")}.\n` +
+ `Move shared machinery to web/static/ and import it as ../<name>.js.`,
+);
+
+// Guard the guard: a directory that somehow scanned empty would pass vacuously.
+assert.ok(files.length > 10, `expected the apps directory to hold apps, found ${files.length} .js files`);
+
+console.log(`apps-dir tests passed (${files.length} apps, all calling defineApp)`);
diff --git a/test/flow.test.mjs b/test/flow.test.mjs
index 326b6d9..14e3773 100644
--- a/test/flow.test.mjs
+++ b/test/flow.test.mjs
@@ -5,9 +5,9 @@
// fixture discipline the Swift interpreter will inherit.
import assert from "node:assert/strict";
import { test } from "node:test";
-import { runFlow, renderText, evalCond, cleanNameAnswer } from "../web/static/apps/flow.js";
-import SETUP_FLOW from "../web/static/apps/setup.flow.js";
-import WELCOME_FLOW from "../web/static/apps/welcome.flow.js";
+import { runFlow, renderText, evalCond, cleanNameAnswer } from "../web/static/flow.js";
+import SETUP_FLOW from "../web/static/setup.flow.js";
+import WELCOME_FLOW from "../web/static/welcome.flow.js";
const tick = () => new Promise((r) => setImmediate(r));
@@ -664,7 +664,7 @@ await test("setup: enrolment confirmed while on keys-waiting settles through", a
// ---- native bundle drift gate ------------------------------------------------
// The Swift interpreter (ios/.../FlowEngine.swift) runs byte-derived copies of
// these flow documents (Flows/*.json). Regenerate after editing a flow:
-// node -e 'import("./web/static/apps/setup.flow.js").then(async m => { (await import("fs")).writeFileSync("ios/LChatCore/Sources/LChatCore/Flows/setup.json", JSON.stringify(m.default, null, 2) + "\n"); })'
+// node -e 'import("./web/static/setup.flow.js").then(async m => { (await import("fs")).writeFileSync("ios/LChatCore/Sources/LChatCore/Flows/setup.json", JSON.stringify(m.default, null, 2) + "\n"); })'
// (and the welcome twin).
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
diff --git a/test/quickform.test.mjs b/test/quickform.test.mjs
index a0fdd49..e0eee0d 100644
--- a/test/quickform.test.mjs
+++ b/test/quickform.test.mjs
@@ -4,9 +4,9 @@
// the planner) must change with it, or the two paths' transcripts diverge.
import assert from "node:assert/strict";
import { test } from "node:test";
-import { planQuickForm } from "../web/static/apps/quickform.js";
-import SETUP_FLOW from "../web/static/apps/setup.flow.js";
-import WELCOME_FLOW from "../web/static/apps/welcome.flow.js";
+import { planQuickForm } from "../web/static/quickform.js";
+import SETUP_FLOW from "../web/static/setup.flow.js";
+import WELCOME_FLOW from "../web/static/welcome.flow.js";
await test("welcome plan: cleaned name + the photo skip record", () => {
// The same clean-before-cap rule as the typed answer ("I'm Dermot" → Dermot).
diff --git a/test/schema-engine.test.mjs b/test/schema-engine.test.mjs
index 360ff01..bcc7641 100644
--- a/test/schema-engine.test.mjs
+++ b/test/schema-engine.test.mjs
@@ -1,4 +1,4 @@
-// 🤖 Tests for the app-schema engine (web/static/apps/schema-engine.js) and the
+// 🤖 Tests for the app-schema engine (web/static/schema-engine.js) and the
// todo/remind schema files. Three layers of proof:
// 1. The schema fold reproduces the shared event vectors (the cross-platform
// spec native also tests against).
@@ -25,7 +25,7 @@ import {
tickerRules,
evalTemplate,
relTime,
-} from "../web/static/apps/schema-engine.js";
+} from "../web/static/schema-engine.js";
import { todoVectors, remindVectors } from "./event-vectors/index.js";
import { app as todoApp } from "../web/static/apps/todo.js";
import { app as remindApp, parseReminder } from "../web/static/apps/remind.js";
diff --git a/test/tutorial.test.mjs b/test/tutorial.test.mjs
index 77746fb..8fc3a95 100644
--- a/test/tutorial.test.mjs
+++ b/test/tutorial.test.mjs
@@ -1,6 +1,6 @@
// Tests for Eleven's guided tour (client-side flow). node test/tutorial.test.mjs
import assert from "node:assert/strict";
-import { startTour } from "../web/static/apps/tutorial.js";
+import { startTour } from "../web/static/tutorial.js";
import { runCommand, commandCatalog, setDefaultBot } from "../web/static/apps.js";
let failures = 0;
diff --git a/test/vaultcrypto.test.mjs b/test/vaultcrypto.test.mjs
index 326f346..910b91f 100644
--- a/test/vaultcrypto.test.mjs
+++ b/test/vaultcrypto.test.mjs
@@ -1,4 +1,4 @@
-// Tests for the vault lock layer's crypto (web/static/apps/vaultcrypto.js), run
+// Tests for the vault lock layer's crypto (web/static/vaultcrypto.js), run
// in Node (Node 22 has Web Crypto). node test/vaultcrypto.test.mjs
//
// The WebAuthn ceremonies need an authenticator, so — like home's crypto tests —
@@ -22,7 +22,7 @@ import {
acceptGrant,
wrapVaultPrivRecovery,
unwrapVaultPrivRecovery,
-} from "../web/static/apps/vaultcrypto.js";
+} from "../web/static/vaultcrypto.js";
let failures = 0;
async function test(name, fn) {
diff --git a/test/views.test.mjs b/test/views.test.mjs
index 1d80d57..d2d2901 100644
--- a/test/views.test.mjs
+++ b/test/views.test.mjs
@@ -4,7 +4,7 @@
// limits) and proves the expanded schema drives the shared engine.
import assert from "assert";
import { viewsApp, viewSchema } from "../web/static/apps/views.js";
-import { listModel, rowModel, initialState, foldSchemaEvent } from "../web/static/apps/schema-engine.js";
+import { listModel, rowModel, initialState, foldSchemaEvent } from "../web/static/schema-engine.js";
let passes = 0;
let fails = 0;
diff --git a/web/static/apps/README.md b/web/static/apps/README.md
index 8120120..ae08dd7 100644
--- a/web/static/apps/README.md
+++ b/web/static/apps/README.md
@@ -4,6 +4,13 @@ A **mini-app** is a small client-side program that lives alongside chats — a
to-do list, a poll, reminders. This guide is written for both humans and LLMs:
follow it top to bottom and you can add a new app in one self-contained file.
+> **This directory holds apps and nothing else.** Every `.js` file in
+> `web/static/apps/` calls `defineApp` — one app per file. Shared machinery an
+> app leans on (the schema engine + renderer, the conversation-flow engine,
+> Eleven's scripted-conversation feel, the Vault's lock crypto) is a sibling
+> module in `web/static/`, imported as `../<name>.js`. So listing this
+> directory lists the apps.
+
## The one idea
An app has no server. It trades **typed events** over the same end-to-end
@@ -18,7 +25,7 @@ or even recognise app logic — everything happens in the browser.
> **The app schema.** To-do and Reminders are SCHEMA-DESCRIBED: a declarative
> JSON schema (`schemas/<name>.json`, spec: `docs/app-schema.md`) declares
> their state, folds, actions, and views, and BOTH platforms interpret it —
-> the web via `schema-engine.js` + `schema-render.js` (their app files wire
+> the web via `../schema-engine.js` + `../schema-render.js` (their app files wire
> `schemaEvents`/`schemaRender` into defineApp and keep only the glue:
> commands, preview/unread, chat wording, named parsers), the native apps via
> the Swift twin engine with SwiftUI components. To change those apps' events,
diff --git a/web/static/apps/remind.js b/web/static/apps/remind.js
index f8ab5f0..55da526 100644
--- a/web/static/apps/remind.js
+++ b/web/static/apps/remind.js
@@ -29,7 +29,7 @@
// in Node tests while still resolving to /static/apps.js in the browser.
import { defineApp } from "../apps.js";
import { ICON_ALARM_CLOCK } from "../icons.js";
-import { schemaEvents, schemaInitialState, schemaRender, schemaStyles } from "./schema-render.js";
+import { schemaEvents, schemaInitialState, schemaRender, schemaStyles } from "../schema-render.js";
import schema from "./schemas/remind.json" with { type: "json" };
// ---- time parsing (pure, unit-tested) -----------------------------------
diff --git a/web/static/apps/setup.js b/web/static/apps/setup.js
index d80cc78..49a4394 100644
--- a/web/static/apps/setup.js
+++ b/web/static/apps/setup.js
@@ -11,10 +11,10 @@
// persistence) and shell.js's setup bridge (setSetupBridge). Like /tutorial it
// must work with no AI and no bot at all (a fresh instance has neither).
import { defineApp, setupBridge, awaitAnswer, cancelAnswer, appStateFor, emitAppEvent, spaceInfo, openAppThread } from "../apps.js";
-import { makeSay, makeLiveSay, sayEvents, sayBubble } from "./eleven.js";
-import { runFlow } from "./flow.js";
-import SETUP_FLOW from "./setup.flow.js";
-import { startTour } from "./tutorial.js";
+import { makeSay, makeLiveSay, sayEvents, sayBubble } from "../eleven.js";
+import { runFlow } from "../flow.js";
+import SETUP_FLOW from "../setup.flow.js";
+import { startTour } from "../tutorial.js";
export const app = defineApp({
name: "setup",
diff --git a/web/static/apps/todo.js b/web/static/apps/todo.js
index bab2d4c..7d98e61 100644
--- a/web/static/apps/todo.js
+++ b/web/static/apps/todo.js
@@ -14,7 +14,7 @@
// tests while still resolving to /static/apps.js in the browser.
import { defineApp } from "../apps.js";
import { ICON_SQUARE_CHECK } from "../icons.js";
-import { schemaEvents, schemaInitialState, schemaRender, schemaStyles } from "./schema-render.js";
+import { schemaEvents, schemaInitialState, schemaRender, schemaStyles } from "../schema-render.js";
import schema from "./schemas/todo.json" with { type: "json" };
// A to-do is { id, text, done, doneAt, by } — see schemas/todo.json for the
diff --git a/web/static/apps/vault.js b/web/static/apps/vault.js
index 08cb2d8..a3c043f 100644
--- a/web/static/apps/vault.js
+++ b/web/static/apps/vault.js
@@ -69,7 +69,7 @@ import {
unsealValue,
grantVaultKey,
acceptGrant,
-} from "./vaultcrypto.js";
+} from "../vaultcrypto.js";
// How long a revealed value stays visible before it re-locks itself — short, so a
// vault left open on screen doesn't sit unmasked. Clicking it again re-arms it.
diff --git a/web/static/apps/views.js b/web/static/apps/views.js
index 28878c1..21cb19c 100644
--- a/web/static/apps/views.js
+++ b/web/static/apps/views.js
@@ -10,7 +10,7 @@
// bounded, non-https URLs dropped, item and byte caps enforced, oldest views
// evicted past the per-thread cap.
import { defineApp, appStateFor, openCommandPanel } from "../apps.js";
-import { schemaRender, schemaStyles } from "./schema-render.js";
+import { schemaRender, schemaStyles } from "../schema-render.js";
// Caps — the fold's half of the two-sided bound (the sidecar's validator in
// internal/bot/view.go is the other; keep them in step).
diff --git a/web/static/apps/welcome.js b/web/static/apps/welcome.js
index 8345a2b..d3c5421 100644
--- a/web/static/apps/welcome.js
+++ b/web/static/apps/welcome.js
@@ -11,10 +11,10 @@
// eleven.js's feel and shell.js's setup bridge. Like setup it must work with
// no AI and no sidecar (the sidecar's welcome DM stood down in its favour).
import { defineApp, setupBridge, awaitAnswer, cancelAnswer, appStateFor, emitAppEvent, spaceInfo } from "../apps.js";
-import { makeSay, makeLiveSay, sayEvents, sayBubble } from "./eleven.js";
-import { runFlow } from "./flow.js";
-import WELCOME_FLOW from "./welcome.flow.js";
-import { startTour } from "./tutorial.js";
+import { makeSay, makeLiveSay, sayEvents, sayBubble } from "../eleven.js";
+import { runFlow } from "../flow.js";
+import WELCOME_FLOW from "../welcome.flow.js";
+import { startTour } from "../tutorial.js";
export const app = defineApp({
name: "welcome",
diff --git a/web/static/apps/eleven.js b/web/static/eleven.js
similarity index 99%
rename from web/static/apps/eleven.js
rename to web/static/eleven.js
index 2a2cb18..96c23f1 100644
--- a/web/static/apps/eleven.js
+++ b/web/static/eleven.js
@@ -6,7 +6,7 @@
// action chips and composer hints ride a client-only notice (they need the
// running script's closures); on a resumed run a line already in history
// re-arms its chips instantly, with no typing beat and no duplicate bubble.
-import { emitAppEvent, appStateFor, awaitAnswer } from "../apps.js";
+import { emitAppEvent, appStateFor, awaitAnswer } from "./apps.js";
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
diff --git a/web/static/apps/flow.js b/web/static/flow.js
similarity index 100%
rename from web/static/apps/flow.js
rename to web/static/flow.js
diff --git a/web/static/onboardingui.js b/web/static/onboardingui.js
index 5d13214..65de222 100644
--- a/web/static/onboardingui.js
+++ b/web/static/onboardingui.js
@@ -9,7 +9,7 @@ import { session, roster, myUserId, threads, threadKeys, activeId, threadName, e
import { encrypt } from "/static/crypto.js";
import { appStateFor, setupBridge, spaceInfo, emitAppEvent } from "/static/apps.js";
import { appendNotice, setNoticeTyping, feedGlideToBottom } from "/static/apps/chat.js";
-import { planQuickForm } from "/static/apps/quickform.js";
+import { planQuickForm } from "/static/quickform.js";
import { startWelcome } from "/static/apps/welcome.js";
import { showDiscover, createThread } from "/static/newchat.js";
diff --git a/web/static/apps/quickform.js b/web/static/quickform.js
similarity index 100%
rename from web/static/apps/quickform.js
rename to web/static/quickform.js
diff --git a/web/static/apps/schema-engine.js b/web/static/schema-engine.js
similarity index 100%
rename from web/static/apps/schema-engine.js
rename to web/static/schema-engine.js
diff --git a/web/static/apps/schema-render.js b/web/static/schema-render.js
similarity index 100%
rename from web/static/apps/schema-render.js
rename to web/static/schema-render.js
diff --git a/web/static/apps/setup.flow.js b/web/static/setup.flow.js
similarity index 100%
rename from web/static/apps/setup.flow.js
rename to web/static/setup.flow.js
diff --git a/web/static/shell.js b/web/static/shell.js
index 4878901..cf5df70 100644
--- a/web/static/shell.js
+++ b/web/static/shell.js
@@ -125,10 +125,10 @@ import "/static/apps/linear.js";
import { openThreadView } from "/static/apps/views.js"; // views: bot-created data surfaces (docs/views.md); also registers the app
import "/static/apps/emoji.js"; // /emoji — the command-panel reference app
import "/static/apps/gif.js"; // /gif — Giphy search in a command panel (bring-your-own key; posts via ctx.host.gif)
-import { startTour } from "/static/apps/tutorial.js"; // also registers the /tutorial floor command
+import { startTour } from "/static/tutorial.js"; // also registers the /tutorial floor command
import { startSetup } from "/static/apps/setup.js"; // the first-run setup conversation (also registers its app)
import { startWelcome } from "/static/apps/welcome.js"; // the joiner welcome conversation (also registers its app)
-import { planQuickForm } from "/static/apps/quickform.js"; // "Fill in a form instead" — values → verbs+records
+import { planQuickForm } from "/static/quickform.js"; // "Fill in a form instead" — values → verbs+records
import "/static/apps/admin.js"; // the admin panel (admin-only; opened via /admin or setup's chip)
import "/static/apps/appstudio.js"; // make + manage your own member apps (docs/member-apps.md)
import { openMemoryPanel } from "/static/apps/memory.js"; // /memory — the AI-memory scratchpad (docs/eleven-memory.md)
diff --git a/web/static/apps/tutorial.js b/web/static/tutorial.js
similarity index 99%
rename from web/static/apps/tutorial.js
rename to web/static/tutorial.js
index 68faec6..309e2cb 100644
--- a/web/static/apps/tutorial.js
+++ b/web/static/tutorial.js
@@ -13,7 +13,7 @@
// ephemeral (see buildLocalNotice). The tour reads the group name + optional
// administrator identity via spaceInfo(), and shows the AI section only when a
// conversational helper is present.
-import { registerFloorCommand, defaultBot, openAppThread, spaceInfo, elevenTour } from "../apps.js";
+import { registerFloorCommand, defaultBot, openAppThread, spaceInfo, elevenTour } from "./apps.js";
// The tour is entered two ways — typing /tutorial, and the welcome DM's "Take the
// tour" chip — so registration lives here and startTour is exported for the chip.
diff --git a/web/static/apps/vaultcrypto.js b/web/static/vaultcrypto.js
similarity index 99%
rename from web/static/apps/vaultcrypto.js
rename to web/static/vaultcrypto.js
index 1c1bbba..acfeaef 100644
--- a/web/static/apps/vaultcrypto.js
+++ b/web/static/vaultcrypto.js
@@ -24,7 +24,7 @@ import {
openSealed,
wrapThreadKey,
unwrapThreadKey,
-} from "../crypto.js";
+} from "./crypto.js";
// ---- PRF -> KEK (the browser end) ------------------------------------------
diff --git a/web/static/apps/welcome.flow.js b/web/static/welcome.flow.js
similarity index 100%
rename from web/static/apps/welcome.flow.js
rename to web/static/welcome.flow.js