@floless/app 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16329,7 +16329,7 @@ var require_core = __commonJS({
16329
16329
  };
16330
16330
  var MAX_EXPRESSION = 200;
16331
16331
  function requiredOptions(o) {
16332
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
16332
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v2, _w, _x, _y, _z, _0;
16333
16333
  const s = o.strict;
16334
16334
  const _optz = (_a = o.code) === null || _a === void 0 ? void 0 : _a.optimize;
16335
16335
  const optimize = _optz === true || _optz === void 0 ? 1 : _optz || 0;
@@ -16347,7 +16347,7 @@ var require_core = __commonJS({
16347
16347
  meta: (_s = o.meta) !== null && _s !== void 0 ? _s : true,
16348
16348
  messages: (_t = o.messages) !== null && _t !== void 0 ? _t : true,
16349
16349
  inlineRefs: (_u = o.inlineRefs) !== null && _u !== void 0 ? _u : true,
16350
- schemaId: (_v = o.schemaId) !== null && _v !== void 0 ? _v : "$id",
16350
+ schemaId: (_v2 = o.schemaId) !== null && _v2 !== void 0 ? _v2 : "$id",
16351
16351
  addUsedSchema: (_w = o.addUsedSchema) !== null && _w !== void 0 ? _w : true,
16352
16352
  validateSchema: (_x = o.validateSchema) !== null && _x !== void 0 ? _x : true,
16353
16353
  validateFormats: (_y = o.validateFormats) !== null && _y !== void 0 ? _y : true,
@@ -51362,6 +51362,9 @@ function cancelActiveRun() {
51362
51362
  killTree(run.child);
51363
51363
  return true;
51364
51364
  }
51365
+ function isRunActive() {
51366
+ return activeRun !== null;
51367
+ }
51365
51368
  var LOGS_DIR = process.env.AWARE_HOME ? (0, import_node_path3.join)(process.env.AWARE_HOME, "logs") : (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".aware", "logs");
51366
51369
  var TRIGGER_POLL_MS = 250;
51367
51370
  function parseRunHandle(stdout) {
@@ -51797,12 +51800,12 @@ function cmpVersions(a, b) {
51797
51800
  return 0;
51798
51801
  }
51799
51802
  function decideBootstrap(input) {
51800
- const { toolchain, aware: aware2, pin, skip } = input;
51803
+ const { toolchain, aware: aware2, floor, skip } = input;
51801
51804
  if (!toolchain.hasNode) return { kind: "fail", reason: "no-node" };
51802
51805
  if (skip) return aware2.installed ? { kind: "ready" } : { kind: "fail", reason: "absent-and-skipped" };
51803
- if (aware2.installed && isValidVersion(aware2.version) && cmpVersions(aware2.version, pin) >= 0) return { kind: "ready" };
51806
+ if (aware2.installed && isValidVersion(aware2.version) && cmpVersions(aware2.version, floor) >= 0) return { kind: "ready" };
51804
51807
  if (!toolchain.hasNpm) return aware2.installed ? { kind: "ready" } : { kind: "fail", reason: "no-npm" };
51805
- return { kind: "install", version: pin };
51808
+ return { kind: "install", version: "latest" };
51806
51809
  }
51807
51810
  var REMEDIATION = {
51808
51811
  "no-node": "AWARE needs Node.js 20+. Install it from https://nodejs.org and relaunch floless.app.",
@@ -51840,7 +51843,7 @@ function runBootstrap(deps) {
51840
51843
  set(deps, { status: "probing", awareReady: false, awareVersion: null, reason: null, remediation: null });
51841
51844
  const aware0 = deps.getAware();
51842
51845
  version = aware0.version;
51843
- const decision = decideBootstrap({ toolchain: deps.probeToolchain(), aware: aware0, pin: deps.pin, skip: deps.skip });
51846
+ const decision = decideBootstrap({ toolchain: deps.probeToolchain(), aware: aware0, floor: deps.floor, skip: deps.skip });
51844
51847
  if (decision.kind === "fail") return fail(deps, decision.reason, version);
51845
51848
  if (decision.kind === "install") {
51846
51849
  set(deps, { status: "installing", awareReady: false, awareVersion: version, reason: null, remediation: null });
@@ -52251,7 +52254,7 @@ function appVersion() {
52251
52254
  return resolveVersion({
52252
52255
  isSea: isSea2(),
52253
52256
  sqVersionXml: readSqVersionXml(),
52254
- define: true ? "0.5.2" : void 0,
52257
+ define: true ? "0.6.0" : void 0,
52255
52258
  pkgVersion: readPkgVersion()
52256
52259
  });
52257
52260
  }
@@ -52261,7 +52264,7 @@ function resolveChannel(s) {
52261
52264
  return "dev";
52262
52265
  }
52263
52266
  function appChannel() {
52264
- return resolveChannel({ isSea: isSea2(), define: true ? "0.5.2" : void 0 });
52267
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.6.0" : void 0 });
52265
52268
  }
52266
52269
 
52267
52270
  // bake.ts
@@ -53730,12 +53733,95 @@ async function applyUpdate(check, opts) {
53730
53733
  return { applied: true, message: `updating to v${check.targetVersion}\u2026 the app will relaunch` };
53731
53734
  }
53732
53735
 
53736
+ // aware-update.ts
53737
+ var REGISTRY_LATEST = "https://registry.npmjs.org/@aware-aeco/cli/latest";
53738
+ var FEED_TIMEOUT_MS2 = 15e3;
53739
+ var VERSION_TTL_MS = 5e3;
53740
+ var _v = null;
53741
+ function awareInstalledVersion(probe = aware.npmVersion) {
53742
+ const now = Date.now();
53743
+ if (_v && now - _v.at < VERSION_TTL_MS) return _v.val;
53744
+ _v = { at: now, val: probe() };
53745
+ return _v.val;
53746
+ }
53747
+ function invalidateAwareVersion() {
53748
+ _v = null;
53749
+ }
53750
+ function versionGt2(a, b) {
53751
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
53752
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
53753
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
53754
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
53755
+ if (d !== 0) return d > 0;
53756
+ }
53757
+ return false;
53758
+ }
53759
+ function decideAwareUpdate(cur, latest) {
53760
+ const base = { supported: true, currentVersion: cur };
53761
+ if (!latest) return { ...base, updateAvailable: false, reason: "could not read the npm registry latest" };
53762
+ if (!cur) return { ...base, updateAvailable: false, targetVersion: latest, reason: "AWARE not installed yet" };
53763
+ if (versionGt2(latest, cur)) return { ...base, updateAvailable: true, targetVersion: latest };
53764
+ return { ...base, updateAvailable: false, targetVersion: latest, reason: `already up to date (v${cur})` };
53765
+ }
53766
+ async function fetchAwareLatest() {
53767
+ try {
53768
+ const res = await fetch(REGISTRY_LATEST, { redirect: "follow", signal: AbortSignal.timeout(FEED_TIMEOUT_MS2) });
53769
+ if (!res.ok) return null;
53770
+ const body = await res.json();
53771
+ return typeof body.version === "string" ? body.version : null;
53772
+ } catch {
53773
+ return null;
53774
+ }
53775
+ }
53776
+ async function checkAwareUpdate() {
53777
+ const cur = awareInstalledVersion();
53778
+ const latest = await fetchAwareLatest();
53779
+ return decideAwareUpdate(cur, latest);
53780
+ }
53781
+ async function applyAwareUpdate(deps) {
53782
+ try {
53783
+ await deps.install("latest");
53784
+ deps.refresh();
53785
+ invalidateAwareVersion();
53786
+ return { ok: true, version: deps.probe() };
53787
+ } catch (err) {
53788
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
53789
+ }
53790
+ }
53791
+ function awareApplyDeps(install) {
53792
+ return { install, refresh: refreshInvoker, probe: aware.npmVersion };
53793
+ }
53794
+ function awareUpgradeBlockReason(s) {
53795
+ if (s.bootstrapInstalling || s.installInFlight) return "an AWARE install is already in progress \u2014 try again in a moment";
53796
+ if (s.runActive) return "a workflow run is in progress \u2014 wait for it to finish before upgrading AWARE";
53797
+ if (s.hostWatcherActive) return "a live trigger/watch session is using the host \u2014 stop it before upgrading AWARE";
53798
+ return null;
53799
+ }
53800
+
53733
53801
  // skill-sync.ts
53734
53802
  var import_node_fs15 = require("node:fs");
53735
53803
  var import_node_os9 = require("node:os");
53736
53804
  var import_node_path13 = require("node:path");
53737
53805
  var import_node_url = require("node:url");
53738
53806
  var import_yaml5 = __toESM(require_dist6(), 1);
53807
+
53808
+ // build/ship-skills.mjs
53809
+ var PRODUCT_SKILLS = [
53810
+ "floless-app-bridge",
53811
+ // drive the floless.app CLI / desktop bridge from the user's AI
53812
+ "floless-app-onboarding",
53813
+ // guided, re-runnable tour of AWARE + floless.app for new users
53814
+ "floless-app-routines",
53815
+ // author event-driven ("on trigger") routines
53816
+ "floless-app-workflows"
53817
+ // author/run .flo workflows
53818
+ ];
53819
+ function selectShippedSkillNames(names) {
53820
+ const present = new Set(names);
53821
+ return PRODUCT_SKILLS.filter((name) => present.has(name));
53822
+ }
53823
+
53824
+ // skill-sync.ts
53739
53825
  var __dirname2 = (0, import_node_path13.dirname)((0, import_node_url.fileURLToPath)(__import_meta_url));
53740
53826
  function bundledSkillsRoot() {
53741
53827
  const candidates = [
@@ -53799,7 +53885,7 @@ function decideAction(installed, bundled) {
53799
53885
  function bundledSkills(root) {
53800
53886
  let entries = [];
53801
53887
  try {
53802
- entries = (0, import_node_fs15.readdirSync)(root).filter((n) => n.startsWith("floless-app-"));
53888
+ entries = selectShippedSkillNames((0, import_node_fs15.readdirSync)(root));
53803
53889
  } catch {
53804
53890
  return [];
53805
53891
  }
@@ -55645,7 +55731,7 @@ async function startServer() {
55645
55731
  }
55646
55732
  });
55647
55733
  await app.register(import_static.default, { root: WEB_ROOT, prefix: "/" });
55648
- const KNOWN_GOOD_AWARE = "0.51.0";
55734
+ const MIN_AWARE = "0.51.0";
55649
55735
  const isWin3 = process.platform === "win32";
55650
55736
  const has = (cmd) => {
55651
55737
  try {
@@ -55655,14 +55741,10 @@ async function startServer() {
55655
55741
  return false;
55656
55742
  }
55657
55743
  };
55658
- const bootstrapDeps = {
55659
- probeToolchain: () => ({ hasNode: has("node"), hasNpm: has("npm") }),
55660
- getAware: () => {
55661
- const v = aware.npmVersion();
55662
- return { installed: v !== null, version: v };
55663
- },
55664
- installAware: (version, onLine) => new Promise((resolve4, reject) => {
55665
- const child = (0, import_node_child_process5.spawn)("npm", ["i", "-g", `@aware-aeco/cli@${version}`], { shell: isWin3, windowsHide: true });
55744
+ function installAwareGlobal(spec, onLine = () => {
55745
+ }) {
55746
+ return new Promise((resolve4, reject) => {
55747
+ const child = (0, import_node_child_process5.spawn)("npm", ["i", "-g", `@aware-aeco/cli@${spec}`], { shell: isWin3, windowsHide: true });
55666
55748
  let stderrTail = "";
55667
55749
  let combinedTail = "";
55668
55750
  const tail = (buf, s) => (buf + s).slice(-4e3);
@@ -55679,11 +55761,19 @@ async function startServer() {
55679
55761
  });
55680
55762
  child.on("error", (e) => reject(e));
55681
55763
  child.on("close", (code) => code === 0 ? resolve4() : reject(new Error(`npm exited ${code}: ${stderrTail || combinedTail}`)));
55682
- }),
55764
+ });
55765
+ }
55766
+ const bootstrapDeps = {
55767
+ probeToolchain: () => ({ hasNode: has("node"), hasNpm: has("npm") }),
55768
+ getAware: () => {
55769
+ const v = aware.npmVersion();
55770
+ return { installed: v !== null, version: v };
55771
+ },
55772
+ installAware: installAwareGlobal,
55683
55773
  smoke: () => aware.smoke(),
55684
55774
  refreshInvoker,
55685
55775
  emit: broadcast,
55686
- pin: KNOWN_GOOD_AWARE,
55776
+ floor: MIN_AWARE,
55687
55777
  skip: process.env.AWARE_SKIP_BOOTSTRAP === "1"
55688
55778
  };
55689
55779
  app.setErrorHandler((err, _req, reply) => {
@@ -55715,7 +55805,7 @@ async function startServer() {
55715
55805
  });
55716
55806
  app.addHook("onRequest", async (req, reply) => {
55717
55807
  if (!req.url.startsWith("/api/")) return;
55718
- if (req.url.startsWith("/api/health") || req.url.startsWith("/api/license/") || req.url.startsWith("/api/bootstrap/") || req.url.startsWith("/api/autostart") || req.url.startsWith("/api/update")) return;
55808
+ if (req.url.startsWith("/api/health") || req.url.startsWith("/api/license/") || req.url.startsWith("/api/bootstrap/") || req.url.startsWith("/api/autostart") || req.url.startsWith("/api/update") || req.url.startsWith("/api/aware/update")) return;
55719
55809
  const { state: state2, signInUrl: signInUrl2 } = await getLicenseStatus();
55720
55810
  if (state2 !== "valid" && state2 !== "offline-grace") {
55721
55811
  return reply.status(402).send({ ok: false, error: "subscription required", state: state2, signInUrl: signInUrl2 });
@@ -55734,7 +55824,8 @@ async function startServer() {
55734
55824
  ok: true,
55735
55825
  appVersion: appVersion(),
55736
55826
  // the installed build (sq.version), so it's scriptable
55737
- awareVersion: bs.awareVersion,
55827
+ awareVersion: awareInstalledVersion() ?? bs.awareVersion,
55828
+ // fresh (TTL-cached) install version; the boot snapshot is only a fallback
55738
55829
  awareReady: bs.awareReady,
55739
55830
  bootstrap: bs.status,
55740
55831
  bootstrapReason: bs.reason,
@@ -55781,6 +55872,27 @@ async function startServer() {
55781
55872
  reply.send({ ok: true, ...result });
55782
55873
  setTimeout(() => process.exit(0), 250);
55783
55874
  });
55875
+ app.get("/api/aware/update", async () => {
55876
+ return { ok: true, ...await checkAwareUpdate() };
55877
+ });
55878
+ let awareInstallInFlight = false;
55879
+ app.post("/api/aware/update/apply", async (_req, reply) => {
55880
+ const block = awareUpgradeBlockReason({
55881
+ bootstrapInstalling: getBootstrapState().status === "installing",
55882
+ installInFlight: awareInstallInFlight,
55883
+ runActive: isRunActive(),
55884
+ hostWatcherActive: hostWatcherActive()
55885
+ });
55886
+ if (block) return reply.status(409).send({ ok: false, error: block });
55887
+ awareInstallInFlight = true;
55888
+ try {
55889
+ const res = await applyAwareUpdate(awareApplyDeps(installAwareGlobal));
55890
+ if (!res.ok) return reply.status(409).send({ ok: false, error: res.error ?? "aware upgrade failed" });
55891
+ return { ok: true, version: res.version };
55892
+ } finally {
55893
+ awareInstallInFlight = false;
55894
+ }
55895
+ });
55784
55896
  app.post("/api/bootstrap/retry", async () => {
55785
55897
  const st = getBootstrapState().status;
55786
55898
  if (st === "failed" || st === "idle") {
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: floless-app-onboarding
3
+ description: This skill should be used to onboard a user into floless.app and its AWARE runtime as a guided, re-runnable walkthrough. It first explains what AWARE is (the runtime), checks the installed AWARE version and offers an update, maps the command surface, and shows the AVAILABLE agents via the `aware agent catalog` command (installing one on request); then it tours floless.app (the 3-column chat/canvas/inspect UI, the Compile-Approve-Run gate, Simulate vs real Run, Templates, Routines, the Agents library, Bake and Graft, the Inspect tabs, the version footer) and the three floless skills (bridge, routines, workflows). Use it when the user is new or wants a refresher, on triggers like "onboard me", "get me started", "I'm new to floless.app or AWARE", "give me a tour", "how do I use this", "walk me through floless.app", "what can AWARE do", "show me the agents", or "/floless-app-onboarding". It can be re-invoked anytime for the whole tour or a single section.
4
+ metadata:
5
+ version: 0.1.0
6
+ ---
7
+
8
+ # Onboarding a user into floless.app + AWARE
9
+
10
+ Guide the user through the whole program end-to-end, as the **host terminal AI** they are talking
11
+ to. floless.app is a **thin web UI**; the terminal AI (you) is the brain and AWARE is the runtime.
12
+ This skill makes you the tour guide: explain AWARE first (the runtime everything sits on), then
13
+ floless.app (the window onto it), driving the user's **real** environment as you go.
14
+
15
+ Two arcs, with detail pushed to references (read each one when entering its arc):
16
+
17
+ - **Arc A — AWARE, the runtime** → `references/aware-runtime.md`
18
+ - **Arc B — floless.app, the UI** → `references/floless-app-tour.md`
19
+
20
+ ## Operating principles (apply throughout)
21
+
22
+ - **Drive live, don't just lecture.** Run the read-only probes as they come up (`/api/health`, the
23
+ AWARE version, `aware agent catalog`) and narrate the user's **actual** state — their version,
24
+ their agents, what's on their canvas. A tour of the real thing beats prose.
25
+ - **Mutations need consent.** Anything that changes the machine — updating AWARE, installing an
26
+ agent — is **offered, then run only on an explicit yes**. Read-only probes run freely.
27
+ - **Degrade gracefully, never block.** Features land at different times. If a command or endpoint is
28
+ missing (e.g. an older AWARE without `aware agent catalog`, or the in-app AWARE-upgrade pill not
29
+ yet present), say so plainly, fall back to what exists, and keep going. Each arc's reference spells
30
+ out its fallback.
31
+ - **Stay faithful to the architecture.** The UI never becomes the brain; determinism is AWARE's (the
32
+ `.lock` is the approved contract, Run is gated on source-hash freshness). Never teach anything that
33
+ contradicts this.
34
+ - **One concept at a time; check in often.** This is a walkthrough, not a wall of text. After each
35
+ section, pause — "make sense? want to go deeper, or move on?" Let the user steer the pace.
36
+
37
+ ## Step 0 — Orient and route
38
+
39
+ 1. **Confirm the ground.** Probe the floless.app server and the installed AWARE version (read-only):
40
+ ```bash
41
+ curl -s http://127.0.0.1:4317/api/health
42
+ # → {"ok":true,"license":"valid","awareVersion":"<installed>", ...}
43
+ ```
44
+ - No response → the floless.app desktop app isn't running. Ask the user to start it (so the tour
45
+ can drive its UI). The AWARE arc still works without it.
46
+ - `awareVersion` is the AWARE version the app reports. On current builds it's freshly re-probed
47
+ and trustworthy; an **older app build can show a stale snapshot**, so when the exact version
48
+ matters (e.g. deciding whether to update), confirm it against npm in A2 — `npm ls -g
49
+ @aware-aeco/cli` is the source of truth. Note the version for Arc A.
50
+ - `license` not `valid`/`offline-grace` → some UI actions sit behind a sign-in gate; mention it
51
+ when the UI arc reaches a gated action, don't block the tour.
52
+
53
+ 2. **Pick the path.** Ask the user once:
54
+ > "Want the **full guided tour** (AWARE → floless.app), or **jump to one section**?"
55
+ - **New user / unsure → full tour:** run Arc A then Arc B, in order, top to bottom.
56
+ - **Refresher → menu:** show the section map below and run only the chosen section(s).
57
+ - The user can always say "skip ahead", "go deeper on X", or "stop" — honor it.
58
+
59
+ ## The section map (also the re-run menu)
60
+
61
+ Offer this list when the user wants to jump. Each item maps to a section in a reference file.
62
+
63
+ **Arc A — AWARE (the runtime)** — `references/aware-runtime.md`
64
+ - **A1 · What AWARE is** — the substrate; where it lives (`~/.aware`); build-once / run-forever.
65
+ - **A2 · Version & update** — show the installed version, check for newer, offer the update (consent).
66
+ - **A3 · The command surface** — the `aware` verbs worth knowing (apps, agents, sidecars, logs).
67
+ - **A4 · Available agents** — browse `aware agent catalog`, search, capability-check, install one (consent).
68
+
69
+ **Arc B — floless.app (the UI)** — `references/floless-app-tour.md`
70
+ - **B1 · The 3-column window** — chat│canvas│inspect; the app is the prompt, the browser is the window.
71
+ - **B2 · The gate** — Compile → Approve → Run; the source-hash Run gate; Simulate vs real Run.
72
+ - **B3 · Templates, Routines, Agents, Inspect** — the panels and tabs the user will actually click.
73
+ - **B4 · The footer** — installed AWARE version + app version; updating AWARE / the app from here.
74
+ - **B5 · Bake & Graft** — export a workflow as an agent (Bake); import your own tool as an agent (Graft).
75
+ - **B6 · The floless skills** — bridge / routines / workflows, and how to drive them from the terminal.
76
+ - **B7 · How to run + best practices** — the everyday loop and the habits that keep it deterministic.
77
+
78
+ ## Running an arc
79
+
80
+ When entering **Arc A**, read `references/aware-runtime.md` and walk A1→A4 (or the chosen item).
81
+ When entering **Arc B**, read `references/floless-app-tour.md` and walk B1→B7 (or the chosen item).
82
+ Each reference carries the exact commands, the talking points, the consent prompts, and the
83
+ fallbacks. Narrate the user's real output; do not paste a reference verbatim.
84
+
85
+ ## Close
86
+
87
+ When the tour (or chosen section) is done, recap in two or three lines what the user now knows, and
88
+ tell them how to come back:
89
+
90
+ > "That's the tour. Re-run **/floless-app-onboarding** anytime — for the whole thing, or just name a
91
+ > section ('the agents part', 'Bake & Graft', 'routines') and I'll jump straight there."
92
+
93
+ Then offer a concrete next step grounded in what they have — e.g. "want to build your first
94
+ workflow?" (hand off to **floless-app-workflows**) or "install one of those agents?" (A4).
95
+
96
+ ## Guardrails
97
+
98
+ - **Read-only by default; mutations on consent.** Never update AWARE or install an agent without an
99
+ explicit yes.
100
+ - **Never invent.** Don't name agents, commands, or UI elements that aren't really there — probe and
101
+ report what exists. If `aware agent catalog` is unavailable, say so and use `aware agent list`.
102
+ - **Don't contradict the architecture.** The UI renders AWARE state and triggers `aware` verbs; it
103
+ never composes workflows or calls an LLM. Determinism is AWARE's `.lock`.
104
+ - **Hand off for depth.** Onboarding orients; it does not replace the doing-skills. Point to
105
+ **floless-app-workflows** (build/run `.flo` apps), **floless-app-routines** (automate them), and
106
+ **floless-app-bridge** (apply UI tweaks/templates) when the user wants to actually do the thing.
@@ -0,0 +1,159 @@
1
+ # Arc A — AWARE, the runtime
2
+
3
+ The detail behind Arc A. Walk A1→A4 (or the one section the user picked). Run the read-only probes
4
+ live and narrate the user's real output; offer mutations (update, install) and run them only on a
5
+ yes. Keep each section short and check in before moving on.
6
+
7
+ ---
8
+
9
+ ## A1 · What AWARE is
10
+
11
+ **The pitch (say it in plain terms):** AWARE is the open-source **agentic substrate for AECO**
12
+ ([github.com/aware-aeco/aware](https://github.com/aware-aeco/aware)) — the runtime everything in
13
+ floless.app sits on. You describe what you want; the terminal AI composes a plain-text **`.flo`**
14
+ workflow; AWARE **compiles** it into a frozen, version-pinned **`.lock`** (the approved contract) and
15
+ **runs** it. floless.app is just the window onto this — it holds no engine and no LLM of its own.
16
+
17
+ **Build once, run forever** (the core idea — make sure it lands):
18
+ - `aware app compile` freezes a workflow's source into `<app>.lock` and pins the agent versions.
19
+ - `aware app run` **refuses to run** if the source changed since that compile (a "source-hash drift").
20
+ So an approved app stays exactly as approved until you deliberately recompile. That gate is what
21
+ makes runs reproducible — and it's the same gate the floless.app **Run** button enforces (B2).
22
+ - Determinism is layered: the `.lock` at the **app** level, and compiled code (an `exec` node's C#,
23
+ a Smart Node) at the **node** level.
24
+
25
+ **Where it lives** (show them — read-only):
26
+ ```bash
27
+ ls ~/.aware
28
+ # apps/ installed workflows (<id>/<app>.flo + <app>.lock)
29
+ # agents/ installed capabilities apps can call
30
+ # bridges/ host connectors (e.g. aware-tekla.exe)
31
+ # logs/ per-run JSONL execution traces
32
+ # credentials/ connection secrets (opaque; never through the model)
33
+ ```
34
+ AWARE itself is installed as the npm global **`@aware-aeco/cli`** (not bundled into floless.app —
35
+ floless.app installs/repairs it on first start). That detail matters for A2.
36
+
37
+ ---
38
+
39
+ ## A2 · Version & update
40
+
41
+ **Show the installed version.** The **authoritative source is the installed npm package** (a file
42
+ read of what's actually on disk, not a guess):
43
+ ```bash
44
+ npm ls -g @aware-aeco/cli # the installed global — the source of truth
45
+ ```
46
+ The floless.app footer and `/api/health` (`awareVersion`, captured in Step 0) also surface it —
47
+ freshly re-probed on current builds, but an **older app build can show a stale boot snapshot**. If
48
+ the app's number and npm disagree, trust npm. State the version back to the user.
49
+
50
+ **Check for a newer release** (read-only):
51
+ ```bash
52
+ npm view @aware-aeco/cli version # the latest published on npm
53
+ ```
54
+ Compare to the installed version. If they match, say "you're on the latest" and move on.
55
+
56
+ **If newer exists, offer the update (consent — don't run it unprompted):**
57
+ > "AWARE v<new> is out (you're on v<cur>). Update now?"
58
+
59
+ The reliable path on **every** build is the npm global — lead with it:
60
+ ```bash
61
+ npm i -g @aware-aeco/cli@latest # always works; no running app required
62
+ ```
63
+ **Newer floless.app builds also add an in-app shortcut:** a footer **`↑ Upgrade AWARE to v<target>`**
64
+ pill next to the version (clicking it reinstalls in place, no relaunch) — programmatically
65
+ `GET /api/aware/update` then `POST /api/aware/update/apply`. If that pill isn't there, the app isn't
66
+ running, or `GET /api/aware/update` 404s, the build predates the shortcut — just use the npm command
67
+ above.
68
+
69
+ After applying, **re-probe** (`npm ls -g @aware-aeco/cli`) and confirm the new version to the user.
70
+
71
+ **Why there's a button at all:** floless.app installs `@aware-aeco/cli@latest` above a minimum floor
72
+ on first run, but it deliberately does **not** force-upgrade an existing install on every boot (no
73
+ surprise upgrades, no slow starts). Moving an up-to-date-enough install to the newest release is
74
+ exactly what this on-demand update is for.
75
+
76
+ ---
77
+
78
+ ## A3 · The command surface
79
+
80
+ Give a **map**, not an exhaustive dump. Most of these the user will drive through floless.app's
81
+ buttons (B2) — the CLI is the same machinery underneath. Group it like this, then suggest
82
+ `aware --help` (and `aware app --help` / `aware agent --help`) to see the live surface for *their*
83
+ version rather than enumerating flags they may not have.
84
+
85
+ **Apps — the workflows (`aware app …`):**
86
+ - `list` — installed apps. `show <id>` — an app's manifest. `inspect <dir>` — the "Glass Box" plan
87
+ (takes the app **path**, like `validate`/`compile`).
88
+ - `install <dir>` — install from an app **directory**. `uninstall <id>`.
89
+ - `validate <dir>` / `compile <dir>` — check / freeze to `<app>.lock` (compile is "Approve").
90
+ - `run <id> [--input k=v] [--json]` — execute the approved app (run takes the **id**).
91
+ - `logs` — past runs (traces also land at `~/.aware/logs/<id>/…`).
92
+
93
+ **Agents — the capabilities apps call (`aware agent …`):**
94
+ - `list` — **installed** agents. `describe <id>` — an installed agent's commands + skills.
95
+ - `install <id>` / `uninstall <id>` — add/remove a capability.
96
+ - …plus the **catalog** commands for discovering agents you *don't* have yet — that's A4.
97
+
98
+ **Sidecars / bridges — host connectors (`aware sidecar …`):**
99
+ - `install <host>` — fetch a host bridge (e.g. `tekla`, `rhino`, `sketchup`, `revit`) so workflows
100
+ can drive that application. Required before a real host run against, say, a live Tekla model.
101
+
102
+ **Build — graft a tool into an agent (`aware build agent --from-…`):**
103
+ - Turns a foreign tool you own (a DLL, an OpenAPI spec, a NuGet/npm package, a COM ProgID, a CLI…)
104
+ into a first-class AWARE agent. This is the **Graft** capability — covered in B5.
105
+
106
+ **Connections / credentials (`aware connect …`):** wire up external services; secrets live as opaque
107
+ handles under `~/.aware/credentials/`, never passed through the model.
108
+
109
+ ---
110
+
111
+ ## A4 · Available agents (browse before you install)
112
+
113
+ **The gap to explain:** `aware agent list` and `aware agent describe` only see agents you've
114
+ **already installed**. To discover *what agents exist* before installing — and what each one does —
115
+ use the **catalog** — a searchable index of available agents across architecture, construction,
116
+ engineering, and visualization (the live count is whatever `aware agent catalog` prints for the
117
+ user; don't quote a fixed number).
118
+
119
+ **Browse — run it live and narrate the result:**
120
+ ```bash
121
+ aware agent catalog
122
+ # ID · DISPLAY-NAME · VERSION · STATUS · COMMANDS · SKILLS · DESCRIPTION (one line)
123
+ ```
124
+ Point out a few agents relevant to the user's domain (for an AEC user: `tekla`, `rhino`, `revit`,
125
+ etc.). Add `--json` for machine-readable output.
126
+
127
+ **Search by functionality:**
128
+ ```bash
129
+ aware agent search "steel connection"
130
+ aware agent search "macro" --capability # --capability biases to command names/methods
131
+ ```
132
+
133
+ **Describe one you haven't installed:**
134
+ ```bash
135
+ aware agent describe <id> --available # full description + commands + skills for a NOT-installed agent
136
+ ```
137
+ (Plain `aware agent describe <id>` stays installed-only; `--available` reaches into the catalog.)
138
+
139
+ **Capability check (scriptable, exits 0/1):**
140
+ ```bash
141
+ aware agent has <agent> <capability> # exit 0 if it exposes that capability, nonzero if not
142
+ ```
143
+
144
+ **Install on request (consent — this changes the machine):**
145
+ > "Want me to install `<id>`?"
146
+ ```bash
147
+ aware agent install <id>
148
+ ```
149
+ It lands under `~/.aware/agents/<id>/`, then appears in `aware agent list` and is usable as a
150
+ workflow node (`agent: <id>`). It also shows up in floless.app's **`⊞ Agents`** library (B3).
151
+
152
+ **Degradation — older AWARE has no catalog (handle this gracefully):**
153
+ The catalog commands are newer than some installs. If `aware agent catalog` errors, is an unknown
154
+ subcommand, or prints something like *"this AWARE's registry has no catalog yet — update AWARE"*,
155
+ then the user's AWARE predates the feature. Don't push through it:
156
+ 1. Say so plainly: "Your AWARE is a bit older and doesn't have the agent catalog yet."
157
+ 2. Point back to **A2** and offer the update (`npm i -g @aware-aeco/cli@latest`) — on consent.
158
+ 3. Until they update, fall back to `aware agent list` (installed-only) so the section still delivers
159
+ *something* — show what they already have, and note the catalog unlocks discovery once updated.
@@ -0,0 +1,149 @@
1
+ # Arc B — floless.app, the UI
2
+
3
+ The detail behind Arc B. Walk B1→B7 (or the one section the user picked). Point the user at the
4
+ real elements in their browser (floless.app at `http://127.0.0.1:4317`) and, where useful, show the
5
+ live state. Keep each section short; check in before moving on. If the app isn't running, ask the
6
+ user to start it first.
7
+
8
+ Throughout: floless.app is a **thin UI** — it renders AWARE state and triggers `aware` verbs. It
9
+ never composes workflows or calls an LLM. The terminal AI (you) is the brain.
10
+
11
+ ---
12
+
13
+ ## B1 · The 3-column window
14
+
15
+ The layout is **header / `chat │ canvas │ inspect` / footer**:
16
+ - **Left — chat (~340px):** "the app is the prompt; it lives in your AI terminal; the browser is the
17
+ window." This panel is a relay/echo of the terminal session, **not a second chatbot** — the real
18
+ conversation is where you're typing now, in the terminal.
19
+ - **Center — canvas:** the open workflow's **topology** — nodes and the edges between them, rendered
20
+ from the `.flo`. Zoom / fit controls; click a node to inspect it; **star ★** a node to save it as a
21
+ reusable Template (B3).
22
+ - **Right — inspect (~420px):** details of the selected node (B3).
23
+ - **Top-left — the workflow switcher:** pick which installed app is on the canvas. A small dot marks
24
+ unsaved edits.
25
+
26
+ Have the user pick a workflow from the switcher so there's something real on the canvas to tour.
27
+
28
+ ---
29
+
30
+ ## B2 · The gate (Compile → Approve → Run)
31
+
32
+ The header carries the run controls and a **state** chip. This is "build once, run forever" (A1)
33
+ made visible:
34
+
35
+ - **`⎙ Compile`** — freezes the current `.flo` into its **`.lock`**. This *is* "Approve": the lock is
36
+ the contract that will run. After any edit, the Run gate **locks** until you Compile again (the
37
+ source-hash changed — a "drift").
38
+ - **`▶ Run workflow`** — the **real** run: executes the approved app against your live host. Each node
39
+ paints progress on the canvas (running → ✓ done / ✗ failed); if the app produces a report, the HTML
40
+ Viewer opens with the result. Run always executes the single open workflow against your one live
41
+ host — there's no parallel or multi-target run.
42
+ - **`Simulate`** — a **composition check**: every node is stubbed from its schema, no host touched.
43
+ Great for sanity-checking a workflow's *shape* fast. Caveat to mention: Simulate can't truly run
44
+ data-carrying `exec` apps (stubs carry no real values) — for those, use the real Run.
45
+ - **state chip** — shows where the gate is (needs compile / ready / running).
46
+
47
+ The everyday rhythm: **edit → Compile/Approve → Run**. Run stays disabled on drift until you
48
+ re-Compile. That gate is what keeps runs reproducible.
49
+
50
+ ---
51
+
52
+ ## B3 · Templates, Routines, Agents, Inspect
53
+
54
+ The panels and buttons the user will actually click:
55
+
56
+ - **Templates (★):** star any node on the canvas — or, inside the Agents library, any command — to
57
+ save it as a reusable **Template**. It appears in the template bar and is reusable in **any**
58
+ workflow, and persists across projects. The terminal AI is *template-aware*: when a plain-English
59
+ request matches a saved Template, it reuses that saved logic instead of rebuilding it.
60
+ - **Routines (`⏱ Routines`):** run a workflow **automatically** — on a **schedule** (e.g. weekdays at
61
+ 07:00) or on a **trigger** (a live event, e.g. *whenever your Tekla model changes*). Runs happen on
62
+ this machine through your live host, exactly like clicking ▶ Run. Authoring path: the
63
+ **floless-app-routines** skill (B6).
64
+ - **Agents (`⊞ Agents`):** the **Agents library** — every installed agent, each command starrable as a
65
+ Template. Agents you install (A4) show up here. It's also the entry point to **Graft** (B5).
66
+ - **Inspect (right panel):** click any node → its details. The tabs surface the node's
67
+ **Description / Skill**, its **Code** (the real source — an `exec` node's C#, syntax-highlighted),
68
+ and the live **Execution** trace while a run is in flight. A report/viewer node carries a
69
+ **"View report ▸"** button that opens the in-app **HTML Viewer** (a sandboxed iframe showing the
70
+ last result — load is split from run, so it shows the last report instantly without recomputing).
71
+ - **Tweak (✎):** select a node and queue a plain-English change ("also group by assembly"). The
72
+ terminal AI picks it up (via **floless-app-bridge**, B6), edits just that one node, and recompiles —
73
+ the UI itself never edits the `.flo`.
74
+
75
+ ---
76
+
77
+ ## B4 · The footer
78
+
79
+ The bottom strip shows the **installed AWARE version** and the **floless.app version**.
80
+ - On current builds the AWARE version is **live** — it re-stamps if AWARE changes out of band (e.g.
81
+ you updated it from the terminal). On older builds it can be a boot-time snapshot, so if it ever
82
+ looks off, npm is the source of truth (A2).
83
+ - Update affordances live here too: an **app self-update** pill (`↑ Update to v…`) when a newer
84
+ floless.app is published, and — on builds that include it — an **`↑ Upgrade AWARE to v…`** pill (the
85
+ in-app AWARE update from A2). If you don't see the AWARE pill, your build predates it; the reliable
86
+ path is always the CLI (`npm i -g @aware-aeco/cli@latest`).
87
+
88
+ ---
89
+
90
+ ## B5 · Bake & Graft (the two killer moves)
91
+
92
+ A matched pair — the input and output sides of "turn things into agents":
93
+
94
+ - **Bake — export a workflow as an agent.** (Reachable from the workflow menu as **"Bake into
95
+ agent"**; the confirm action carries the **⊙ Bake** glyph.) Takes a working multi-node `.flo` and
96
+ packages it as a single **callable agent** whose inputs become the agent's parameters. AWARE
97
+ reinstalls it with that exposed interface; it then appears in the **Agents library** as one callable
98
+ node that other workflows can drop in. (Mechanism: `exposes-as-agent` + `exposed-commands` in the
99
+ `.flo`.) Use it to turn a proven workflow into a reusable building block.
100
+ - **Graft — import your own tool as an agent.** (Reachable from the **`⊞ Agents`** library —
101
+ **"Graft new agent"** — or the workflow menu.) The input-side mirror of Bake. Turn a foreign tool
102
+ you already own — a .NET DLL (or a whole folder of them), an OpenAPI spec, a NuGet/npm package, a
103
+ COM ProgID, a CLI — into a first-class AWARE agent you can compose. The hero case: an in-house Tekla
104
+ plugin becomes a headless command you can run across an entire model at 2am. (Mechanism: AWARE's
105
+ `aware build agent --from-…`, A3; floless stages a preview of the discovered commands before you
106
+ commit.)
107
+
108
+ The full arc: **Graft your tool → compose it into a workflow → Bake the workflow → distribute.** That
109
+ is how floless.app turns "use the agents we ship" into "any tool you own becomes composable AI
110
+ infrastructure." Keep it concept-level here; the deep authoring lives in **floless-app-workflows**.
111
+
112
+ ---
113
+
114
+ ## B6 · The floless skills (how to drive it from the terminal)
115
+
116
+ floless.app ships three skills into the AI terminal — they are how the terminal AI acts on the
117
+ user's behalf. When the user wants to *do* something, invoke the matching one:
118
+
119
+ - **floless-app-workflows** — build / modify / run `.flo` workflows: the install→validate→compile→run
120
+ loop, `exec` nodes (Roslyn C# against a live host), app inputs, the HTML Viewer. The doing-skill for
121
+ authoring.
122
+ - **floless-app-routines** — set up automatic runs (schedule or trigger) through the Routines API.
123
+ - **floless-app-bridge** — pick up what the user did in the UI (a queued **Tweak**, a "use this
124
+ Template" request), apply the change to the `.flo`, recompile, and clear the request.
125
+
126
+ All three talk to the same local server (port 4317) and preserve the contract: the UI queues intent;
127
+ the terminal AI does the real authoring. The user drives by **asking you in the terminal**, then
128
+ watching and approving in the browser.
129
+
130
+ ---
131
+
132
+ ## B7 · How to run + best practices
133
+
134
+ **The everyday loop:** pick a workflow (switcher) → set its inputs (on the input node) →
135
+ **`⎙ Compile`/Approve** → **`▶ Run workflow`** → read the result (HTML Viewer, or the Execution tab).
136
+ Edit → recompile → run.
137
+
138
+ **Habits that keep it good:**
139
+ - **Compile after every edit.** Run is gated on a fresh lock — determinism is AWARE's, not the UI's.
140
+ - **Trust the real Run, not a clean compile.** `Simulate` checks shape; only a real `▶ Run` proves a
141
+ workflow works against the host.
142
+ - **Tweak, don't rebuild.** To change one node, select it and **✎ Tweak** — the AI edits just that
143
+ node and recompiles.
144
+ - **Automate the repeat.** Once a workflow is solid, make it a **Routine** (schedule or trigger).
145
+ - **Reuse aggressively.** ★ star nodes/commands as **Templates**; **Bake** proven workflows into
146
+ agents; **Graft** your own tools in.
147
+ - **Keep AWARE current (A2).** New releases bring new agents and features.
148
+ - **Remember who's the brain.** Ask the terminal AI to build and change things; watch, approve, and
149
+ run in the browser. The UI is the window, not the engine.
package/dist/web/app.css CHANGED
@@ -158,9 +158,12 @@
158
158
  quietest tier. Label kept — an unlabeled icon would be an unclear affordance. */
159
159
  #browse-btn { background: transparent; border-color: transparent; color: var(--text-dim); }
160
160
  #browse-btn:hover { background: var(--surface-2); border-color: var(--border-strong); color: var(--text); }
161
- #sim-btn { background: var(--surface-2); color: var(--text-muted); }
162
- #sim-btn:hover { color: var(--text); }
163
- #sim-btn:disabled { opacity: 0.5; cursor: not-allowed; }
161
+ /* Simulate mirrors Compile (outline-accent tier) a consistent secondary
162
+ sibling; Run stays the only filled (primary) button, so the two outline
163
+ buttons reading alike is the correct hierarchy, not a lost signal. */
164
+ #sim-btn { color: var(--text); border-color: var(--accent-dim); }
165
+ #sim-btn:hover { color: var(--text); border-color: var(--accent); }
166
+ #sim-btn:disabled { color: var(--text-dim); border-color: var(--border-strong); cursor: default; opacity: 0.6; }
164
167
  #run-btn {
165
168
  background: var(--accent);
166
169
  color: #ffffff;
@@ -396,18 +399,27 @@
396
399
  .onb-num { font-family: var(--mono); font-size: 11px; font-weight: 600; color: var(--accent); min-width: 14px; }
397
400
  .onb-step-label { font-size: 12px; color: var(--text); }
398
401
  .onb-step-note { font-size: 11px; color: var(--text-dim); font-style: italic; }
399
- .onb-cmd-label { font-size: 11px; color: var(--text-dim); margin: 18px 0 6px; }
400
- .onb-cmd {
401
- display: flex; justify-content: space-between; align-items: center; gap: 10px;
402
- background: var(--bg); border: 1px solid var(--border-strong); border-radius: 4px;
403
- padding: 9px 12px;
404
- }
405
- .onb-cmd code { font-family: var(--mono); font-size: 12px; color: var(--text-muted); }
406
- .onb-copy {
407
- background: transparent; border: none; color: var(--text-dim);
408
- font-size: 14px; line-height: 1; cursor: pointer; padding: 2px 4px; border-radius: 4px;
409
- }
410
- .onb-copy:hover { color: var(--text); }
402
+ /* Secondary AI-native pointer (replaces the old raw `aware app install` chip — confusing for a
403
+ newcomer and off-message against "your terminal AI builds the workflows"). */
404
+ .onb-foot { font-size: 12px; color: var(--text-muted); margin-top: 16px; line-height: 1.5; }
405
+ /* "Start the guided tour" the primary first-run action (copies the onboarding launch prompt).
406
+ Accent-soft region + filled-accent button mirror the .agent-card.selected / #run-btn primary
407
+ tier; all tokens existing (locked baseline — no new fonts/colors). */
408
+ .onb-tour {
409
+ display: flex; align-items: center; justify-content: space-between; gap: 12px;
410
+ margin-top: 14px; padding: 11px 14px;
411
+ background: var(--accent-soft); border: 1px solid var(--accent-dim); border-radius: 6px;
412
+ }
413
+ .onb-tour-hint { flex: 1; font-size: 11px; color: var(--text-dim); font-style: italic; line-height: 1.45; }
414
+ .onb-tour-btn {
415
+ flex: none; background: var(--accent); color: #ffffff; border: 1px solid var(--accent);
416
+ border-radius: 4px; font-family: var(--ui); font-size: 12px; font-weight: 600;
417
+ padding: 7px 14px; letter-spacing: 0.02em; cursor: pointer; white-space: nowrap;
418
+ transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
419
+ }
420
+ .onb-tour-btn:hover { background: var(--accent-bright); border-color: var(--accent-bright); box-shadow: 0 0 14px var(--accent-glow); }
421
+ .onb-tour-btn:disabled { cursor: default; opacity: 0.85; box-shadow: none; }
422
+ .onb-tour-ico { font-size: 11px; opacity: 0.7; margin-left: 6px; }
411
423
 
412
424
  .agent-card {
413
425
  background: var(--surface);
@@ -1198,7 +1210,11 @@
1198
1210
  .sep { color: var(--text-dim); opacity: 0.4; }
1199
1211
  .version-info { display: inline-flex; align-items: center; gap: 8px; }
1200
1212
  .aware-version, .app-version { font-family: var(--mono); font-size: 11px; color: var(--text-muted); letter-spacing: 0.02em; }
1201
- .aware-version:not(:empty) + .app-version::before { content: '·'; margin-right: 8px; opacity: 0.4; }
1213
+ /* Separator between the AWARE runtime version and the app build version. An explicit
1214
+ .ver-sep span (revealed once versions stamp) — robust to the upgrade pill sitting
1215
+ between the two versions, which a `+`-adjacency ::before could not span. Flex `gap`
1216
+ on .version-info spaces it on both sides. */
1217
+ .ver-sep { font-size: 11px; }
1202
1218
 
1203
1219
  /* ========== TOOLTIPS (styled, app-themed; replaces native title="") ========== */
1204
1220
  #tooltip {
@@ -1589,15 +1605,27 @@
1589
1605
  header dirty-dot so the menu reinforces it without a new component. */
1590
1606
  .menu-item-dirty .menu-icon { color: var(--warn); }
1591
1607
  .menu-item-dirty .menu-label::after { content: " ●"; color: var(--warn); font-size: 9px; vertical-align: 1px; }
1608
+ /* Icon column. Glyphs are inline Lucide stroke SVGs (shadcn's icon family),
1609
+ not Unicode characters — ad-hoc symbols rendered at wildly different ink
1610
+ sizes (7–19px tall at the same font-size); a uniform 15px SVG box fixes it.
1611
+ stroke:currentColor keeps the dirty-state recolor (.menu-item-dirty) working. */
1592
1612
  .menu-icon {
1593
1613
  width: 18px;
1594
- font-size: 13px;
1595
1614
  color: var(--text-muted);
1596
1615
  display: inline-flex;
1597
1616
  align-items: center;
1598
1617
  justify-content: center;
1599
1618
  flex-shrink: 0;
1600
1619
  }
1620
+ .menu-icon svg {
1621
+ width: 15px;
1622
+ height: 15px;
1623
+ fill: none;
1624
+ stroke: currentColor;
1625
+ stroke-width: 1.5;
1626
+ stroke-linecap: round;
1627
+ stroke-linejoin: round;
1628
+ }
1601
1629
  .menu-label { flex: 1; }
1602
1630
  .menu-kbd {
1603
1631
  font-family: var(--mono);
@@ -1871,7 +1899,7 @@ body {
1871
1899
 
1872
1900
  /* Compile / approve — the gated step before Run. Outline-accent tier: an
1873
1901
  accent-dim border pairs it visually with the filled Run button, so the eye
1874
- reads "Compile → Run" as the spine, above the muted Simulate. */
1902
+ reads "Compile → Run" as the spine; Simulate is a matching outline sibling. */
1875
1903
  #compile-btn { color: var(--text); border-color: var(--accent-dim); }
1876
1904
  #compile-btn:hover { color: var(--text); border-color: var(--accent); }
1877
1905
  #compile-btn:disabled { color: var(--text-dim); border-color: var(--border-strong); cursor: default; opacity: 0.6; }
@@ -2066,12 +2094,45 @@ body {
2066
2094
  .agent-card .node-inputs .ni-val { color: var(--accent); font-weight: 600; }
2067
2095
 
2068
2096
  /* Per-node run status, painted from the live trace (running → done / error).
2069
- Uses a box-shadow ring (composes with the left mode-stripe ::after) plus a
2070
- text pip colour is never the only signal. */
2071
- .agent-card.node-running { box-shadow: 0 0 0 1px var(--accent), 0 0 12px var(--accent-glow); animation: nodePulse 1.3s ease-in-out infinite; }
2072
- @keyframes nodePulse {
2073
- 0%, 100% { box-shadow: 0 0 0 1px var(--accent), 0 0 5px var(--accent-glow); }
2074
- 50% { box-shadow: 0 0 0 1px var(--accent), 0 0 16px var(--accent-glow); }
2097
+ Running = a STATIC accent ring + glow (set once, never animated so the
2098
+ compositor isn't recalculating box-shadow blur on every node every frame)
2099
+ plus a bright accent band that travels left→right across the card face. Colour
2100
+ is never the only signal — the spinner + "running" text pip and the filled
2101
+ stat bar are static cues alongside the sweep. */
2102
+ .agent-card.node-running { box-shadow: 0 0 0 1px var(--accent), 0 0 14px var(--accent-glow); }
2103
+ /* Repurpose ::before (the hover/selected corner-wash) ONLY while running: it
2104
+ becomes a traveling light band. The card is overflow:hidden + radius:8px, so
2105
+ the band clips to the rounded rect for free. position/inset/pointer-events
2106
+ come from the base ::before rule; we override background + animation and kill
2107
+ the inherited opacity transition so the band shows instantly (no 0.25s fade). */
2108
+ .agent-card.node-running::before {
2109
+ opacity: 1;
2110
+ /* Wide soft band with a bright (but glow-cored, not hard-line) centre, so the
2111
+ sweep reads clearly without washing out the text it passes over. */
2112
+ background: linear-gradient(100deg,
2113
+ transparent 0%, transparent 28%,
2114
+ var(--accent-soft) 38%,
2115
+ color-mix(in srgb, var(--accent) 78%, transparent) 47%,
2116
+ color-mix(in srgb, var(--accent-bright) 85%, transparent) 50%,
2117
+ color-mix(in srgb, var(--accent) 78%, transparent) 53%,
2118
+ var(--accent-soft) 62%,
2119
+ transparent 72%, transparent 100%);
2120
+ background-size: 235% 100%;
2121
+ background-repeat: no-repeat;
2122
+ background-position: -30% 0;
2123
+ animation: nodeSweep 1.7s linear infinite;
2124
+ transition: none;
2125
+ }
2126
+ /* Off the left edge → off the right edge, no idle gap, constant speed. */
2127
+ @keyframes nodeSweep {
2128
+ 0% { background-position: -30% 0; }
2129
+ 100% { background-position: 130% 0; }
2130
+ }
2131
+ /* Selected + running: keep BOTH rings (.node-running would otherwise clobber the
2132
+ .selected ring). The sweep ::before overrides the static .selected::before wash. */
2133
+ .agent-card.selected.node-running {
2134
+ border-color: var(--accent);
2135
+ box-shadow: 0 0 0 1px var(--accent), 0 0 14px var(--accent-glow), 0 8px 32px -8px var(--accent-glow);
2075
2136
  }
2076
2137
  .agent-card.node-done { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ok) 50%, transparent); }
2077
2138
  .agent-card.node-error { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--err) 60%, transparent); }
@@ -2082,8 +2143,46 @@ body {
2082
2143
  border-top: 1px solid var(--border);
2083
2144
  }
2084
2145
  .agent-card.node-running .node-stat { color: var(--accent); background: var(--accent-soft); }
2146
+ /* "running" gets a real working indicator — a small ring spinner before the
2147
+ label, reusing the global `spin` keyframe + the .rtn-spinner recipe (so it
2148
+ speaks the same "in-flight" language as the routines panel). No arbitrary
2149
+ glyph. done/error keep their meaningful ✓/✗ text marks. */
2150
+ .agent-card.node-running .node-stat::before {
2151
+ content: "";
2152
+ display: inline-block;
2153
+ width: 10px; height: 10px;
2154
+ border: 1.5px solid var(--border-strong);
2155
+ border-top-color: var(--accent);
2156
+ border-radius: 50%;
2157
+ animation: spin 0.8s linear infinite;
2158
+ vertical-align: middle;
2159
+ margin-right: 5px;
2160
+ position: relative; top: -1px; /* nudge the ring onto the 10px text midline */
2161
+ }
2085
2162
  .agent-card.node-done .node-stat { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, transparent); }
2086
2163
  .agent-card.node-error .node-stat { color: var(--err); background: color-mix(in srgb, var(--err) 14%, transparent); }
2164
+ /* Reduced motion — no traveling band, no spin. The node still reads as running
2165
+ via the static ring + glow, the filled accent-soft stat bar, and the "running"
2166
+ label; the card ::before falls back to the native 135deg wash, and the stat
2167
+ spinner becomes a static accent dot (active via colour, no implied motion).
2168
+ Mirrors the .rtn-fired-flash / .so-spinner reduced-motion precedents. */
2169
+ @media (prefers-reduced-motion: reduce) {
2170
+ .agent-card.node-running::before {
2171
+ animation: none;
2172
+ background: linear-gradient(135deg, var(--accent-soft), transparent 60%);
2173
+ background-size: auto;
2174
+ background-position: 0 0;
2175
+ opacity: 1;
2176
+ transition: none;
2177
+ }
2178
+ .agent-card.node-running .node-stat::before {
2179
+ animation: none;
2180
+ width: 5px; height: 5px;
2181
+ border: none;
2182
+ background: var(--accent);
2183
+ top: 0;
2184
+ }
2185
+ }
2087
2186
  /* Description-tab pointer to the Code tab (not the code itself). */
2088
2187
  .dim-note { color: var(--text-dim); font-size: 12px; margin-top: 8px; }
2089
2188
 
package/dist/web/app.js CHANGED
@@ -97,10 +97,14 @@ function renderCanvasPlaceholder(kind) {
97
97
  `</div>`;
98
98
  }
99
99
 
100
- // First-run onboarding card (the 'empty' placeholder). Purely informational
101
- // the only action is copying the install command (the steps' real controls live
102
- // in the header and are already disabled). No repeated brand mark (not a splash).
103
- const ONB_INSTALL_CMD = 'aware app install <folder>';
100
+ // First-run onboarding card (the 'empty' placeholder). The primary action is "Start the guided tour"
101
+ // it copies a prompt the user pastes into their terminal AI to launch onboarding. No raw CLI command
102
+ // is shown: the AI builds the workflows (per the headline), so the card stays AI-native, not "go run
103
+ // this command yourself". No repeated brand mark (not a splash).
104
+ // The natural-language launch prompt the "Start the guided tour" button copies. Phrased to match the
105
+ // floless-app-onboarding skill's auto-trigger ("onboard me", "guided tour") so pasting it into Claude
106
+ // Code / Codex starts the tour. Plain language (not a slash command) = portable across terminal hosts.
107
+ const ONB_TOUR_PROMPT = 'Onboard me into floless.app and AWARE — give me the guided tour.';
104
108
  function onboardingHtml() {
105
109
  const step = (n, label, note) =>
106
110
  `<li><span class="onb-num">${n}</span><span class="onb-step"><span class="onb-step-label">${label}</span> <span class="onb-step-note">${note}</span></span></li>`;
@@ -108,6 +112,11 @@ function onboardingHtml() {
108
112
  `<div class="canvas-empty onboarding" data-kind="empty">` +
109
113
  `<div class="onb-headline">Your terminal AI builds the workflows.<br>You run and inspect them here.</div>` +
110
114
  `<div class="onb-sub">floless.app shows the canvas, triggers runs, and streams results. Compose in Claude Code or Codex; the browser is the window.</div>` +
115
+ `<div class="onb-tour">` +
116
+ `<span class="onb-tour-hint">New here? Copies a prompt — paste it into Claude Code or Codex.</span>` +
117
+ `<button class="onb-tour-btn" type="button" aria-label="Start the guided tour — copies the onboarding prompt to your clipboard"><span class="onb-tour-label">Start the guided tour</span><span class="onb-tour-ico" aria-hidden="true">⎘</span></button>` +
118
+ `<span class="sr-only onb-tour-live" aria-live="polite"></span>` +
119
+ `</div>` +
111
120
  `<div class="onb-divider"></div>` +
112
121
  `<ol class="onb-steps">` +
113
122
  step(1, 'Pick a workflow', '— the picker, top right') +
@@ -115,26 +124,37 @@ function onboardingHtml() {
115
124
  step(3, 'Compile', '— freeze the approved lock') +
116
125
  step(4, 'Run', '— against the live host; results render here') +
117
126
  `</ol>` +
118
- `<div class="onb-cmd-label">Add a workflow from your terminal:</div>` +
119
- `<div class="onb-cmd"><code>${ONB_INSTALL_CMD.replace('<', '&lt;').replace('>', '&gt;')}</code>` +
120
- `<button class="onb-copy" type="button" data-tip="Copy command" aria-label="Copy command">⎘</button></div>` +
127
+ `<div class="onb-foot">No workflows yet? Just ask your terminal AI to build one.</div>` +
121
128
  `</div>`
122
129
  );
123
130
  }
124
131
  function wireOnboardingCopy() {
125
- const btn = $topology.querySelector('.onb-copy');
126
- if (!btn) return;
127
- btn.onclick = async () => {
128
- try {
129
- await navigator.clipboard.writeText(ONB_INSTALL_CMD);
130
- const prev = btn.textContent;
131
- btn.textContent = '';
132
- showToast('Copied: ' + ONB_INSTALL_CMD, 'ok');
133
- setTimeout(() => { btn.textContent = prev; }, 1200);
134
- } catch {
135
- showToast('Could not copy — select the command and copy manually.', 'warn');
136
- }
137
- };
132
+ // The primary first-run action: copy the natural-language onboarding prompt so the user can paste it
133
+ // into their terminal AI to launch the guided tour (the floless-app-onboarding skill).
134
+ const tour = $topology.querySelector('.onb-tour-btn');
135
+ if (tour) {
136
+ const live = $topology.querySelector('.onb-tour-live');
137
+ const label = tour.querySelector('.onb-tour-label');
138
+ const ico = tour.querySelector('.onb-tour-ico');
139
+ tour.onclick = async () => {
140
+ try {
141
+ await navigator.clipboard.writeText(ONB_TOUR_PROMPT);
142
+ tour.disabled = true;
143
+ if (label) label.textContent = 'Copied';
144
+ if (ico) ico.textContent = '✓';
145
+ if (live) live.textContent = 'Copied to clipboard';
146
+ showToast('Copied — paste it into your terminal AI', 'ok');
147
+ setTimeout(() => {
148
+ if (label) label.textContent = 'Start the guided tour';
149
+ if (ico) ico.textContent = '⎘';
150
+ tour.disabled = false;
151
+ if (live) live.textContent = '';
152
+ }, 1600);
153
+ } catch {
154
+ showToast('Could not copy — select and paste the prompt manually.', 'warn');
155
+ }
156
+ };
157
+ }
138
158
  }
139
159
 
140
160
  function nodeIds(p) {
package/dist/web/aware.js CHANGED
@@ -1521,6 +1521,61 @@
1521
1521
  setInterval(refreshUpdate, 6 * 60 * 60 * 1000); // re-check every 6h so a long-running window notices
1522
1522
  }
1523
1523
 
1524
+ // ── AWARE runtime upgrade tag ─────────────────────────────────────────────────
1525
+ // Surface aware-update.ts: poll GET /api/aware/update (on load + every 6h); when a
1526
+ // newer @aware-aeco/cli exists, show a footer pill → confirm → POST apply, which
1527
+ // reinstalls AWARE in place (no relaunch — the app stays open) and re-stamps the
1528
+ // version live. Mirrors the app self-update pill; differs in that success is silent
1529
+ // (the live version re-stamp IS the confirmation) and there is no relaunch.
1530
+ const $awareUpdate = document.getElementById('aware-update');
1531
+ // After a successful upgrade, briefly trust the upgraded version: a /api/health request that
1532
+ // was in flight before the reinstall finished could resolve with the OLD version and momentarily
1533
+ // revert the footer. The live re-stamp (health poll) honors this short settle window.
1534
+ let awareUpgradeFloor = null, awareUpgradeFloorUntil = 0;
1535
+ async function refreshAwareUpdate() {
1536
+ if (!$awareUpdate) return;
1537
+ try {
1538
+ const r = await fetch('/api/aware/update');
1539
+ const d = await r.json();
1540
+ if (r.ok && d.updateAvailable && d.targetVersion) {
1541
+ $awareUpdate.textContent = '↑ Upgrade AWARE to v' + d.targetVersion;
1542
+ $awareUpdate.dataset.target = d.targetVersion;
1543
+ $awareUpdate.dataset.tip = 'A newer AWARE runtime (v' + d.targetVersion + ') is available — click to install it in place (no relaunch)';
1544
+ $awareUpdate.hidden = false;
1545
+ } else {
1546
+ $awareUpdate.hidden = true; // up-to-date / absent / registry error → no pill
1547
+ }
1548
+ } catch { $awareUpdate.hidden = true; }
1549
+ }
1550
+ if ($awareUpdate) {
1551
+ $awareUpdate.onclick = async () => {
1552
+ const v = $awareUpdate.dataset.target || '';
1553
+ if (!window.confirm('Upgrade AWARE runtime to v' + v + '? This reinstalls the npm package in place — the app stays open and the version restamps automatically.')) return;
1554
+ $awareUpdate.disabled = true;
1555
+ $awareUpdate.textContent = '↑ Upgrading…';
1556
+ try {
1557
+ // Cap the wait so the pill never sticks disabled if the global npm install wedges.
1558
+ const r = await fetch('/api/aware/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' }, signal: AbortSignal.timeout(120000) });
1559
+ const d = await r.json().catch(() => ({}));
1560
+ if (!r.ok || !d.ok) throw new Error(d.error || 'aware upgrade failed');
1561
+ // Success is silent: the pill disappears and the AWARE version re-stamps live.
1562
+ $awareUpdate.hidden = true;
1563
+ $awareUpdate.disabled = false;
1564
+ const wv = document.getElementById('aware-version');
1565
+ if (wv && d.version) { wv.textContent = 'AWARE ' + d.version; awareUpgradeFloor = d.version; awareUpgradeFloorUntil = Date.now() + 10000; }
1566
+ } catch (e) {
1567
+ $awareUpdate.disabled = false;
1568
+ const msg = (e && e.name === 'TimeoutError')
1569
+ ? 'AWARE upgrade is taking a while — it may still be installing; the version updates when it finishes'
1570
+ : 'AWARE upgrade failed — ' + String((e && e.message) || e).slice(0, 80);
1571
+ showToast(msg, 'warn');
1572
+ refreshAwareUpdate(); // restore the pill text for retry
1573
+ }
1574
+ };
1575
+ refreshAwareUpdate();
1576
+ setInterval(refreshAwareUpdate, 6 * 60 * 60 * 1000); // re-check every 6h
1577
+ }
1578
+
1524
1579
  // ONE Run (the approved single-Run model). "▶ Run workflow" does a REAL run against
1525
1580
  // the live host, using the app inputs. If the app has a report node, it drives
1526
1581
  // the in-app HTML Viewer (renders + caches the returned HTML); otherwise it
@@ -1603,7 +1658,7 @@
1603
1658
  // — that's the during-run feedback; pushTrace then refines each node to
1604
1659
  // done/error as the (batched) trace arrives. Covers UI runs, report runs (the
1605
1660
  // canvas paints behind the modal), and terminal-driven runs (trace-file).
1606
- const NODE_STAT_LABEL = { running: 'running', done: '✓ done', error: '✗ failed' };
1661
+ const NODE_STAT_LABEL = { running: 'running', done: '✓ done', error: '✗ failed' };
1607
1662
  function setNodeStatus(nodeId, status) {
1608
1663
  if (!nodeId) return;
1609
1664
  const card = document.querySelector(`.agent-card[data-agent-id="${(window.CSS && CSS.escape) ? CSS.escape(nodeId) : nodeId}"]`);
@@ -2059,13 +2114,22 @@
2059
2114
  // stale install or AWARE mismatch is self-diagnosable). appVersion is
2060
2115
  // the real sq.version on an install (package.json only from source);
2061
2116
  // awareVersion is the aware npm package this app drives.
2062
- if (!shownVersion) {
2063
- const av = document.getElementById('app-version');
2064
- if (av && h && h.appVersion) av.textContent = 'v' + h.appVersion;
2065
- const wv = document.getElementById('aware-version');
2066
- if (wv && h && h.awareVersion) wv.textContent = 'AWARE ' + h.awareVersion;
2067
- if (h && (h.appVersion || h.awareVersion)) shownVersion = true;
2117
+ // The app BUILD version is immutable at runtime → stamp once. The AWARE RUNTIME
2118
+ // version CAN change (an out-of-band `npm i -g`, or the in-app upgrade), so
2119
+ // re-stamp it whenever /api/health reports a different value never show a
2120
+ // stale runtime version in the footer.
2121
+ const av = document.getElementById('app-version');
2122
+ if (av && h && h.appVersion && !shownVersion) { av.textContent = 'v' + h.appVersion; shownVersion = true; }
2123
+ const wv = document.getElementById('aware-version');
2124
+ if (wv && h && h.awareVersion) {
2125
+ const next = 'AWARE ' + h.awareVersion;
2126
+ // During the post-upgrade settle window, ignore a stale health value that disagrees
2127
+ // with the version we just upgraded to (a poll in flight during the reinstall).
2128
+ const settling = awareUpgradeFloor && Date.now() < awareUpgradeFloorUntil && h.awareVersion !== awareUpgradeFloor;
2129
+ if (!settling && wv.textContent !== next) wv.textContent = next;
2068
2130
  }
2131
+ const sep = document.getElementById('ver-sep');
2132
+ if (sep && h && (h.appVersion || h.awareVersion)) sep.hidden = false;
2069
2133
  // Bootstrap BACKSTOP: reconcile the setup overlay from cached health
2070
2134
  // state in case an SSE event was missed. ready → finish the beat &
2071
2135
  // dismiss; failed/installing/probing → (re)render that state.
@@ -147,6 +147,8 @@
147
147
  </div>
148
148
  <span class="version-info">
149
149
  <span class="aware-version" id="aware-version" data-tip="AWARE runtime version — the aware CLI/npm package this app drives"></span>
150
+ <button id="aware-update" class="app-update" hidden data-tip="An AWARE runtime upgrade is available — reinstalls in place, no relaunch"></button>
151
+ <span class="sep ver-sep" id="ver-sep" hidden>·</span>
150
152
  <span class="app-version" id="app-version" data-tip="floless.app build serving this window (installed build, or the dev build when run from source)"></span>
151
153
  <button id="app-update" class="app-update" hidden></button>
152
154
  </span>
@@ -155,38 +157,38 @@
155
157
 
156
158
  <div class="menu" id="menu" role="menu">
157
159
  <button class="menu-item" data-action="open" role="menuitem">
158
- <span class="menu-icon">⌸</span>
160
+ <span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg></span>
159
161
  <span class="menu-label">Open</span>
160
162
  <span class="menu-kbd">Ctrl+O</span>
161
163
  </button>
162
164
  <button class="menu-item" data-action="save" role="menuitem">
163
- <span class="menu-icon">⤓</span>
165
+ <span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg></span>
164
166
  <span class="menu-label">Save inputs</span>
165
167
  <span class="menu-kbd">Ctrl+S</span>
166
168
  </button>
167
169
  <div class="menu-divider"></div>
168
170
  <button class="menu-item" data-action="find" role="menuitem">
169
- <span class="menu-icon">⌕</span>
171
+ <span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg></span>
170
172
  <span class="menu-label">Find on canvas</span>
171
173
  <span class="menu-kbd">Ctrl+F</span>
172
174
  </button>
173
175
  <div class="menu-divider"></div>
174
176
  <button class="menu-item" data-action="graft" role="menuitem">
175
- <span class="menu-icon">⊕</span>
177
+ <span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/></svg></span>
176
178
  <span class="menu-label">Graft into agent</span>
177
179
  </button>
178
180
  <button class="menu-item" data-action="bake" id="menu-bake-item" role="menuitem">
179
- <span class="menu-icon">⊙</span>
181
+ <span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"/><path d="M12 22V12"/><polyline points="3.29 7 12 12 20.71 7"/><path d="m7.5 4.27 9 5.15"/></svg></span>
180
182
  <span class="menu-label" id="menu-bake-label">Bake into agent</span>
181
183
  </button>
182
184
  <div class="menu-divider"></div>
183
185
  <button class="menu-item" data-action="integrations" role="menuitem">
184
- <span class="menu-icon">⊟</span>
186
+ <span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span>
185
187
  <span class="menu-label">Integrations</span>
186
188
  <span class="menu-kbd">Ctrl+I</span>
187
189
  </button>
188
190
  <button class="menu-item" data-action="routines" role="menuitem">
189
- <span class="menu-icon">⏱</span>
191
+ <span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><line x1="10" x2="14" y1="2" y2="2"/><line x1="12" x2="15" y1="14" y2="11"/><circle cx="12" cy="14" r="8"/></svg></span>
190
192
  <span class="menu-label">Routines</span>
191
193
  </button>
192
194
  <!-- Start-on-login + Theme are sibling machine preferences (no divider between
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {
@@ -24,6 +24,7 @@
24
24
  "app:restart": "node launch.mjs restart",
25
25
  "build": "node build/bundle.mjs",
26
26
  "build:exe": "node build/bundle.mjs && node build/make-sea.mjs",
27
+ "verify:skills": "node build/verify-shipped-skills.mjs",
27
28
  "typecheck": "tsc --noEmit",
28
29
  "prepack": "npm run build"
29
30
  },