@floless/app 0.5.1

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.
@@ -0,0 +1,352 @@
1
+ ---
2
+ name: floless-app-workflows
3
+ description: This skill should be used when building, modifying, or reasoning about AWARE .flo workflows for floless.app — especially reusable apps that take inputs (e.g. a phase number) and run across different Tekla models, exec nodes that run Roslyn C# against the live model, the in-app HTML Viewer, the Code tab, or "Debug in VS". Covers the exec contract, the install→validate→compile→run loop, app inputs templating, and the server seams (aware-adapter, app-reader, index).
4
+ metadata:
5
+ version: 0.1.0
6
+ ---
7
+
8
+ # Building reusable .flo workflows in floless.app
9
+
10
+ ## What floless.app is (and is not)
11
+
12
+ floless.app is a **thin web UI over the `aware` CLI**. It owns **no engine and no LLM**.
13
+ Every capability is either `aware <verb>` (shelled through one adapter) or reading AWARE
14
+ state off disk. The browser is just the window. Never make the UI compose workflows or call
15
+ an LLM — it renders AWARE state, triggers `aware` verbs, and relays user intent.
16
+
17
+ The deliverable floless.app produces is a **reusable AWARE `.flo` app**: one that declares
18
+ `inputs:` with defaults and runs unchanged across different models. The canonical example
19
+ lives at `demos/tekla-bom-by-phase/` — study it before building a new one.
20
+
21
+ ## The reusable-app shape
22
+
23
+ An app becomes reusable by declaring a top-level `inputs:` block and templating those inputs
24
+ into node config with `{{ inputs.<name> }}`. The canonical `tekla-bom-by-phase.flo` models the
25
+ **desktop-floless 3-node shape** — a Phase **input** node → the Tekla **core logic** node → an
26
+ HTML Report **viewer** node — as **three real `tekla/exec` nodes whose data flows along the
27
+ edges** (the canvas story and the data flow are the same thing):
28
+
29
+ ```yaml
30
+ app: tekla-bom-by-phase
31
+ version: 0.3.0
32
+ display-name: Tekla BOM by Phase
33
+ description: |
34
+ <one paragraph — required>
35
+ exposes-as-agent: false
36
+ inputs:
37
+ phase:
38
+ type: integer
39
+ default: 1 # keeps the app runnable with no user action
40
+ description: Construction phase number to report on (Tekla PHASE) — editable per run.
41
+ requires:
42
+ - tekla@0.1.x
43
+ layout: linear
44
+ nodes:
45
+ - id: phase-input # NODE 1 — Phase input: scope the run
46
+ agent: tekla
47
+ command: exec
48
+ mode: read # author intent (see exec-mode note below)
49
+ config:
50
+ version: "2025.0"
51
+ args: { phase: "{{ inputs.phase }}" } # from the app-input knob
52
+ code: |
53
+ <Roslyn C# — reads args["phase"], returns new { ok, phase, partsInPhase, modelName }>
54
+ - id: bom # NODE 2 — the Tekla core logic
55
+ agent: tekla
56
+ command: exec
57
+ mode: read
58
+ config:
59
+ version: "2025.0"
60
+ args: { phase: "{{ phase-input.result.phase }}" } # CARRIES node 1's output along the edge
61
+ code: |
62
+ <Roslyn C# — reads args["phase"], returns new { ok, phase, html, ... }>
63
+ - id: viewer # NODE 3 — HTML Report Viewer terminal
64
+ agent: tekla
65
+ command: exec
66
+ mode: read
67
+ config:
68
+ version: "2025.0"
69
+ args:
70
+ html: "{{ bom.result.html }}" # CARRIES the report along the edge
71
+ phase: "{{ bom.result.phase }}"
72
+ code: |
73
+ <Roslyn C# — returns new { ok, phase, html = args["html"] } (the terminal)>
74
+ connections:
75
+ - { from: phase-input, to: bom }
76
+ - { from: bom, to: viewer }
77
+ ```
78
+
79
+ At run time the user (or the UI) supplies `--input phase=2`; the runtime substitutes it into
80
+ node 1's `config.args.phase`; the bridge hands it to the C# as the `args` global. Node 1 emits
81
+ the phase, node 2 consumes it and emits the report HTML, node 3 carries that HTML as the
82
+ terminal — the edges carry real data.
83
+
84
+ ### `layout:` must match the topology (`linear` vs `dag`) — VERIFIED
85
+
86
+ The top-level `layout:` field is **load-bearing for the UI**, not a cosmetic hint. The canvas
87
+ (`renderTopology` in `web/aware.js`) only honors the `connections:` block when `layout: dag`;
88
+ under `layout: linear` it **ignores `connections` and draws a node-order chain** (each node → the
89
+ next, in file order), pulling only edge *labels* positionally. So: a straight chain → keep
90
+ `layout: linear`; **any branch — fan-out, fan-in, a node feeding two — → use `layout: dag`**, or
91
+ the real edges won't render (a fan-out `message-input → A` + `message-input → B` shows as the
92
+ chain `message-input → A → B`). `dag` triggers `layoutDag` (longest-path layering over the real
93
+ `connections`). Verified on aware 0.51.0 against the live UI; `college-phase-exporter` ships `dag`.
94
+
95
+ ### Cross-node templating (how data flows along an edge) — VERIFIED
96
+
97
+ A downstream node references an upstream node's returned object as **`{{ <node-id>.result.<field> }}`**
98
+ (empirically verified on aware 0.46.0). The `.result.` segment is required: the host bridge
99
+ wraps an exec script's `return` value under `result` (the node output is
100
+ `{ ok, result: {...}, host, host_version, ... }`), so `{{ bom.html }}` resolves to **empty** but
101
+ `{{ bom.result.html }}` resolves to the value. Large strings carry intact (an ~12 KB BOM HTML
102
+ travels node 2 → node 3 unchanged). exec output is dynamic, so no `output-schema` is needed for
103
+ the reference to resolve. (The `app-spec.md` example's `{{ runtime: <node>.<field> }}` prefix
104
+ form is stale — the no-prefix `{{ <node>.result.<field> }}` is what runs.)
105
+
106
+ **FOOTGUN — `{{ }}` is rendered across the WHOLE node config, including the exec `code`
107
+ (comments too) — VERIFIED on 0.51.0.** The template engine doesn't skip the `code` block, so any
108
+ `{{ … }}` in C# (even inside a `//` comment) is parsed/rendered at run start and aborts the run:
109
+ - a `{{ }}` literal (empty) → `template parse: syntax error: unexpected end of variable block`;
110
+ - a self-reference like `{{ <thisNode>.result.x }}` in the node's own comment → `template render:
111
+ undefined value` (a node's own `result` doesn't exist during its own execution).
112
+ The failure surfaces as `aware app run … failed (exit 3)` on the FIRST node, which reads like a
113
+ host-attach error but isn't. **Rule: never write `{{` or `}}` anywhere inside `code`** — describe
114
+ templates in prose without braces (e.g. "reads `node.result.x`"). The only legitimate `{{ }}` live
115
+ in `config.args` values. (`tekla-bom`'s comments dodge this only because they reference an
116
+ *upstream* node that has already run — fragile; prefer no braces in code at all.)
117
+
118
+ ### Why all three nodes are `tekla/exec` (hard constraints — predicates can't carry data)
119
+
120
+ - **Inline nodes can't carry data.** Only `inline.kind: predicate` runs, and predicates return a
121
+ **boolean** — they cannot emit a value to a downstream node. `kind: shape`/`map` (which could
122
+ reshape/pass data) are **rejected at validate + compile** on aware 0.46 (`E_APP_INLINE_KIND`,
123
+ closed #160) and marked reserved/not-yet-runtime-supported. So a genuine input→logic→viewer
124
+ **data flow** can only be built from real host (`exec`) nodes.
125
+ - **The `html-report` agent ships no binary** and on aware 0.46 is marked `status: planned` and
126
+ **rejected** at validate/install/compile/run (`E_APP_AGENT_UNAVAILABLE`, closed #161) — never
127
+ use it as the viewer. The viewer is a `tekla/exec` node that **forwards `{{ bom.result.html }}`**
128
+ and returns it, so the report HTML is the terminal node's own output. `extractReportHtml`
129
+ scans the trace and takes the **last** node carrying `data.result.html`; floless's HTML Viewer
130
+ binds to the chain **terminal** (`reportNodeId()` = last node when any node has exec code), so
131
+ the forwarded HTML is what renders.
132
+ - **Cost of the 3-exec shape:** 3 bridge round-trips per run (~10–15 s) and the report HTML
133
+ crosses the bridge twice (built in node 2, echoed by node 3). Acceptable for the
134
+ data-carrying canvas; the alternative (a single exec + UI-only "input"/"viewer" chrome)
135
+ doesn't honor "3 visible nodes".
136
+
137
+ ### exec mode label: declare `mode: read`, but AWARE overrides it (issue #165)
138
+
139
+ Declare `mode: read` on every read-only exec node (author intent). **AWARE ignores it for
140
+ `exec`:** the compiler can't resolve an exec command's mode, so the lock stamps `mode: write`
141
+ with the note *"…command exec not found; defaulting to write-mode for safety"* (filed as
142
+ aware-aeco/aware#165 — silent override of a documented field). It's cosmetic — read-only exec
143
+ runs fine with no `safety:` block. floless reconciles this in `app-reader.ts`: when the source
144
+ declares `mode: read` and the lock defaulted an exec node to write, the UI shows **read** and
145
+ rewrites the cryptic note into a plain-English one — surfacing AWARE's default, not hiding it.
146
+
147
+ ## The exec contract (tekla `agent: tekla, command: exec`)
148
+
149
+ The exec node runs Roslyn C# in the host bridge (`aware-tekla.exe`) against the **live model**.
150
+ Globals injected: `dynamic model` (a `Tekla.Structures.Model.Model`) and
151
+ `IDictionary<string, object> args` (the `config.args` block, JSON-typed). The script ends with
152
+ `return <object>;`. Read `references/exec-contract.md` for the full contract, the available/
153
+ unavailable references (no `System.Net`, no `System.Diagnostics.Process`; manual HTML-escape),
154
+ the part-NAME categorization, and phase filtering.
155
+
156
+ Two rules that bite immediately:
157
+ - **Return the report HTML inline** as `return new { ok = true, html = sb.ToString(), ... };`.
158
+ Do **not** write a file. AWARE ships no html-report render binary, and inline HTML keeps the
159
+ app model-agnostic (no filesystem permission, no hardcoded path). The HTML Viewer renders
160
+ whatever the node returns.
161
+ - **No `System.Net`** (so no `WebUtility.HtmlEncode`) — escape with a manual lambda.
162
+
163
+ ## The build loop (install → validate → compile → run)
164
+
165
+ Resolve the `aware` CLI the same way the adapter does: prefer the npm global
166
+ `@aware-aeco/cli/scripts/bin/aware.js` run via `node`. Then:
167
+
168
+ 1. **Install from the app's *directory*** (not the `.flo` file — registry-hosted/path-to-file is
169
+ rejected): `aware app install ./demos/tekla-bom-by-phase`. **Reinstalling an updated app first
170
+ needs `aware app uninstall <id>`** — a same-id `app install` errors `conflict: app <id> already
171
+ installed` and silently leaves the OLD source in place (a stale compile/run will mislead you).
172
+ 2. **Validate** — `aware app validate <PATH>` takes the app's **directory** (NOT an app id, and
173
+ NOT the `.flo` file): `aware app validate ./demos/tekla-bom-by-phase`. Verified on 0.51.0:
174
+ passing an id errors `io: cannot find the path` (os error 3); passing the `.flo` file errors
175
+ `directory name is invalid` (os error 267).
176
+ 3. **Compile** — `aware app compile <PATH>` also takes the app **directory** and writes
177
+ `<app>.lock` **into that same directory**. To refresh what the UI reads, compile the installed
178
+ copy (`aware app compile ~/.aware/apps/<id>`); or compile the editable source
179
+ (`aware app compile ./demos/<id>`) and copy the `.lock` across. If the `.lock` is missing/stale
180
+ in `~/.aware/apps/<id>/`, app-reader reports `uncompiled` and the UI Run gate stays disabled.
181
+ (Older docs said `compile <id>` resolved relative to CWD — stale; it's a `<PATH>` arg now.)
182
+ 4. **Run for real — MANDATORY before calling ANY `.flo` change done.** `aware app run <id> --input
183
+ k=v --json` (`run` takes the app **id**, unlike `validate`/`compile` which take a path). The
184
+ trace lands at `~/.aware/logs/<id>/<instance>/<run>.jsonl`; the exec result is at `data.result`
185
+ (the bridge wraps the script's return under `result`). **Read the trace** and confirm every node
186
+ emitted `node-output` with `ok:true` and the run ends `run-end · status:ok`. A clean compile and
187
+ a correctly-rendered canvas do **NOT** prove the `.flo` runs — a bad cross-node template or a
188
+ stray `{{ }}` in `code` only surfaces at run (it aborts the FIRST node with a misleading exit-3
189
+ "host" error). Then drive the same **▶ Run workflow** in the UI per the project's real-E2E rule.
190
+
191
+ **`--simulate` is NOT a substitute for exec apps.** Verified on 0.51.0: `--simulate` aborts at
192
+ the first template (`template render: undefined value (in t:1)`) for BOTH `hello-world` AND
193
+ `tekla-bom-by-phase` (the latter runs fine for real) — stubs carry no `result.<field>` and even
194
+ `{{ inputs.* }}` isn't seeded, so the first template renders undefined regardless of correctness.
195
+ A simulate "failure" on an exec-data-carrying app therefore proves nothing. Simulate only helps
196
+ for schema-backed (non-exec) compositions and for catching template *parse* errors; the real
197
+ gate is a live `aware app run` + trace check (above).
198
+
199
+ Keep both copies in sync: the editable source under `demos/<id>/` (committed) and the installed
200
+ copy under `~/.aware/apps/<id>/`. Copy the freshly compiled `.lock` back into `demos/<id>/`.
201
+
202
+ ## Bake a workflow into a reusable agent (`exposes-as-agent`) — VERIFIED, AWARE 0.52.0+
203
+
204
+ A multi-node `.flo` can be **baked into a single callable agent** so another app drops it in as one
205
+ node and still sets its inputs. Declarative (no `aware bake` verb): add two manifest fields to the
206
+ `.flo`. Wired end-to-end by aware-aeco/aware#178 (shipped 0.52.0); inert/dead-code on ≤0.51 — gate on
207
+ the CLI version. Verified on 0.52.0 (see `docs/plans/2026-05-29-bake-workflow-into-agent-design.md`).
208
+
209
+ ```yaml
210
+ exposes-as-agent: true
211
+ exposed-commands:
212
+ run: # the command other apps call: agent: <app>, command: run
213
+ lifecycle: single # single = one-shot, returns the terminal node's output;
214
+ # start = long-running, streams terminal outputs (event source)
215
+ inputs:
216
+ phase: { type: integer } # ← becomes the agent's parameter; caller sets it, type-validated
217
+ outputs: { type: single, schema: { ok: bool, phase: integer } }
218
+ nodes: [ ... the normal chain ... ]
219
+ ```
220
+
221
+ Mechanics (all confirmed against the substrate + a real run):
222
+ - **`aware app install`** of an exposes-as-agent app **synthesizes a callable agent** under
223
+ `~/.aware/agents/<app>/manifest.yaml` — it then shows in `aware agent list` and resolves as
224
+ `agent: <app>`. `aware app uninstall` removes it. (No new install flag; it's automatic.)
225
+ - **A consumer node** `{ agent: <app>, command: run, config: { phase: 7 } }` dispatches into the
226
+ nested chain; the caller's `config` inputs **route into the baked app's `{{ inputs.<name> }}`** and
227
+ are **type-validated** (wrong type → `validation failed: exposed command 'run' input 'phase':
228
+ expected integer, got string`). Observable proof: the nested run-start trace carries
229
+ `config:{phase:7}` (logs under `~/.aware/logs/<app>/nested/…jsonl`).
230
+ - **Guards:** an exposed app **cannot** compose another exposed app (`E_APP_EXPOSED_COMPOSES_EXPOSED`)
231
+ nor reference its own id; `exposes-as-agent: true` with no `exposed-commands` →
232
+ `E_APP_EXPOSES_NO_COMMANDS`. The caller inherits the backing app's `requires`/permissions union.
233
+ - **Test-without-a-host:** make the inner chain a single `inline.kind: predicate` node (runs with no
234
+ bridge) and assert the nested run-start `config`, or assert the type-rejection — both prove routing
235
+ without a live Tekla. (An `exec` inner node needs the real host.) Note: an inline predicate sees the
236
+ upstream *event* as `e.<field>`, NOT `inputs.<field>` — don't assert routing via the predicate body.
237
+
238
+ floless.app play (designed, now unblocked): a "Bake into agent" affordance that rewrites a working
239
+ `.flo` to add `exposes-as-agent` + `exposed-commands` mirroring the app's `inputs:`. Thin-UI: a
240
+ manifest edit + recompile, no engine. See the design doc for the Stage-1 plan.
241
+
242
+ ## How the UI surfaces map to AWARE state
243
+
244
+ - **Code tab** shows the node's real source: `node.config.code` (the exec C#), not lockfile
245
+ YAML. Pure-agent / inline nodes (no inline code) fall back to the resolved-lock YAML. The C#
246
+ is syntax-highlighted by a small dependency-free scanner in `web/aware.js`
247
+ (`highlightCSharp`) that reuses the demo's token classes (`kw`/`ty`/`st`/`cm` + `nu` for
248
+ numbers). No highlighting library — extend the scanner, don't add a dependency.
249
+ - **Inputs live on the input node** (the old above-canvas strip was removed). The input node
250
+ (`inputNodeId()` — first node with `{{ inputs.x }}` in `config.args`) carries a **"Set inputs ▸"**
251
+ button (styled dialog, not `window.prompt`) and an at-a-glance **value chip** ("phase 5",
252
+ refreshed via `markSpecialNodes`). The report node carries a **"View report ▸"** button. Both
253
+ are first-class buttons injected by `markSpecialNodes` in `web/aware.js`; double-click the node
254
+ still works as a shortcut.
255
+ - **One Run, single model** (the approved baseline has ONE `▶ Run workflow`; never add a second
256
+ run control). The header **`▶ Run workflow`** does the **REAL** run: `POST /api/run
257
+ {simulate:false, inputs}`. If the app has a report node (`reportNodeId()` — the exec terminal),
258
+ it renders the returned `html` in the HTML Viewer and caches it; otherwise it fills the Execution
259
+ trace. The secondary **`Simulate`** header button is `simulate:true` (every node stubbed from its
260
+ output-schema, no host) — a composition check.
261
+ - **Per-node run status on the canvas + a Stop button.** During a run each node card paints
262
+ running → ✓ done / ✗ failed from the trace (`pushTrace`), and the report-run overlay shows a
263
+ **Stop** button. See `references/dev-server-and-run-trace.md` for the trace event kinds, the live
264
+ fs-watcher streaming, and the cancel mechanics.
265
+ - **HTML Viewer** (in-app modal): **load vs run is split** (mirrors the desktop floless HTML
266
+ Viewer — the node displays the last result; it does not recompute).
267
+ - **Double-click the viewer node (or "View report ▸") → LOADS the last report** from an in-memory
268
+ per-app cache (`lastReportByApp`), instantly, **no run**. If nothing has run yet, it shows a
269
+ "click ▶ Run workflow" prompt (never a spinner). Do NOT make this trigger a run — a live run is
270
+ ~15s and showing a spinner over stale content reads as "stuck".
271
+ - Rendered in a **sandboxed iframe `srcdoc`** (`sandbox="allow-same-origin"`, scripts
272
+ disabled). The UI never builds the HTML; it relays exactly what the exec returned.
273
+ - **`Simulate` can't run an exec-data-carrying app**: stubs have no `result.<field>`, so the
274
+ cross-node templates (`{{ bom.result.html }}`) render undefined and the run aborts
275
+ (`template render: undefined value`, exit 3). `/api/run` returns that **in-band** (HTTP 200
276
+ `{ok:false}`) so the console stays clean and the UI explains it. Use `▶ Run app` for exec apps.
277
+ - **Debug in VS** (HTML Viewer header) reproduces the desktop floless action. There is no
278
+ `aware exec` verb for ad-hoc code, so `POST /api/debug-node` injects
279
+ `System.Diagnostics.Debugger.Launch(); Debugger.Break();` (after the last `using` directive)
280
+ and sends the code **straight to the bridge** (`~/.aware/bridges/aware-tekla.exe`). The .NET
281
+ JIT picker pops; the user attaches Visual Studio to `aware-tekla.exe`. It debugs the
282
+ **decompiled** Roslyn submission, not source.
283
+
284
+ ## Server seams (where to make changes)
285
+
286
+ - `server/aware-adapter.ts` — the **only** place that shells `aware` *and* the host bridge.
287
+ `run(id, {dryRun, simulate, inputs})` passes `--input k=v`; `execTekla(code, {version, args,
288
+ debug})` talks to the bridge directly (resolves `~/.aware/bridges`, injects the debugger when
289
+ `debug`). Nothing else touches raw `aware`/bridge argv. **`run()` serializes through a single
290
+ promise-chain run lock** — manual (UI) runs and scheduled (routine) runs share it, since the host
291
+ bridge does one exec at a time and `cancelActiveRun()` assumes a single active run.
292
+ - `server/routines.ts` + `/api/routines` (in `index.ts`) — `.flo` runs on a trigger. Two `kind`s
293
+ share the store (`~/.floless/routines.json`) and the **⏱ Routines** panel:
294
+ - **`schedule`** — a time-trigger; the in-server scheduler fires due routines through
295
+ `aware.run()` (the shared run lock above). Carries a `schedule` + `nextFireAt`.
296
+ - **`trigger`** — an event-trigger of a streaming `lifecycle:start` source (e.g. `tekla.watch`).
297
+ A long-lived `aware app run` lives in `server/trigger-sessions.ts` **outside** the run lock; the
298
+ row renders a live `session` snapshot `{state:'listening'|'blocked'|'error'|'stopped',
299
+ firedCount, lastEvent, error}` pushed over the `trigger-session-changed` SSE event. The enabled
300
+ toggle = start/stop the session (not "arm a timer"); run-now is refused (the UI hides ▶). The
301
+ create body is `{kind:'trigger', name, workflow, inputs, enabled}` (no `schedule`) — eligibility
302
+ is `app.triggerSource != null` (computed by `app-reader.detectTriggerSource`); `createRoutine`
303
+ records the binding (`trigger:{nodeId,agent,command}`) from `app.triggerSource` so the row's
304
+ "On trigger · agent/command" descriptor survives a reload. v1 sets **no** source params (the
305
+ watch's `filter` etc. live in node config — see `references/dev-server-and-run-trace.md`).
306
+ Authoring path for the terminal AI = the `floless-app-routines` skill. Both kinds are still
307
+ thin-UI: an event-/time-trigger of `aware app run`, never composing a workflow or calling an LLM.
308
+ - `server/app-reader.ts` — reads `~/.aware/apps/<id>/` off disk (the CLI's `app show` is
309
+ text-only). Parses the top-level `inputs:` block into `app.inputs`, exposes `node.config`
310
+ (incl. `code`), and computes the Run gate from the `.lock` source-hash. **Reconciles the exec
311
+ write-mode default** (issue #165): when the source declares `mode: read` and the lock defaulted
312
+ an `exec` node to `write`, it reports `read` and rewrites the cryptic compile note — so the
313
+ canvas isn't littered with false write-mode badges.
314
+ - `server/index.ts` — routes. `/api/run` accepts `inputs` and returns `report` via
315
+ `extractReportHtml(events)` (scans the trace for `data.result.html`); it surfaces a user Stop
316
+ in-band as `{ cancelled:true }`. `/api/run/stop` → `cancelActiveRun()` (kills the in-flight
317
+ run's process tree). `/api/debug-node` resolves `{{ inputs.x }}` in `config.args` and dispatches
318
+ via `execTekla(..., {debug:true})`.
319
+
320
+ ## Keeping this skill current (self-learning)
321
+
322
+ This skill is **self-learning**: it must improve every time a floless.app session surfaces a
323
+ new, non-obvious fact. Treat the skill as the living memory of how floless.app works.
324
+
325
+ At the end of any session that touched floless.app, before reporting done, ask: *did I learn
326
+ something a future session would waste time rediscovering?* If yes, fold it in **now**:
327
+
328
+ - A new AWARE constraint or footgun (e.g. "only inline predicate runs", "html-report has no
329
+ binary", a CLI arg quirk) → add it to the relevant section here or to `references/`.
330
+ - A new exec-contract detail / Tekla API gotcha → `references/exec-contract.md`.
331
+ - A new UI seam or pattern (a route, a render path, a token class) → the UI / server-seams
332
+ sections.
333
+ - A corrected assumption → fix the stale text so the wrong version never resurfaces.
334
+
335
+ How to update (do not hand-edit around the process):
336
+ 1. Route the change through the `skill-creator` skill (the project's standing rule —
337
+ `feedback_skill_creator_required.md`).
338
+ 2. Keep SKILL.md lean; push long detail into `references/`. Avoid duplicating a fact in both.
339
+ 3. Re-validate: `python <skill-creator>/scripts/quick_validate.py .claude/skills/floless-app-workflows`.
340
+ 4. Commit with the code it documents, or as a `docs(skill):` commit.
341
+
342
+ Keep entries terse and concrete (the constraint + why + where), not prose. If a section grows
343
+ past a few hard facts, split it into a `references/` file and link it.
344
+
345
+ ## Guardrails (do not drift)
346
+
347
+ - Determinism is AWARE's: `<app>.lock` is the approved artifact; Run is gated on a fresh
348
+ source-hash. Never let the UI run on drift.
349
+ - Any change under `web/` requires a Playwright pass (visual + interaction), not just a check of
350
+ the request body — see the project's standing rule.
351
+ - When a real `aware`/bridge bug surfaces, file it on `aware-aeco/aware` (don't file your own
352
+ misuse).
@@ -0,0 +1,119 @@
1
+ # Dev server + run trace (verifying floless.app changes)
2
+
3
+ Concrete, easily-rediscovered facts for verifying UI/server changes against a live host.
4
+
5
+ ## Running the dev server for verification
6
+
7
+ The repo's TypeScript host serves `web/` and shells `aware`. To verify changes you run it
8
+ on **port 4317** and drive it with Playwright (the standing real-E2E rule).
9
+
10
+ - **Command:** `tsx main.ts --serve` from `server/` (this is the `start` script). The `dev`
11
+ script is `tsx watch …` (auto-reload) but the server is usually launched with **`start`
12
+ (NO watch)** — so **server TS edits (`index.ts`, `aware-adapter.ts`, `app-reader.ts`) need a
13
+ manual restart**; a 404 on a route you just added means the old process is still running.
14
+ - **License env (the shared-dir trick) — REQUIRED:** the dev server's license dir
15
+ (`licensing.ts storeDir()`) defaults to `~/.aware`, which has **no token**, so without an
16
+ override the dev server is signed-out and the UI is gated. Start it with
17
+ `FLOLESS_LICENSE_DIR=%LOCALAPPDATA%\FlolessApp-data` (git-bash: `"$LOCALAPPDATA/FlolessApp-data"`)
18
+ so it shares the **installed SEA app's** token + `floless-install-id` (so no seat takeover —
19
+ single-session/newest-login-wins). Token lives at
20
+ `%LOCALAPPDATA%\FlolessApp-data\floless-license.json`.
21
+ - **`web/` is served fresh from disk per request** — edits to `web/*.js|css|html` need only a
22
+ browser **reload**, no server restart. Only server TS needs a restart.
23
+ - **Restart recipe (Windows):** find the listener (`netstat -ano | grep :4317`), get its tree
24
+ (`Get-CimInstance Win32_Process`), `taskkill //PID <tsx-pid> //T //F`, then relaunch
25
+ `FLOLESS_LICENSE_DIR="$LOCALAPPDATA/FlolessApp-data" npx tsx main.ts --serve` in the background.
26
+
27
+ ## The run trace (what `aware app run` emits)
28
+
29
+ Verified event kinds in the JSONL trace (`~/.aware/logs/<id>/<instance>/<run>.jsonl`), in order:
30
+ `run-start` → per node `node-start` then **`node-output`** → `run-end` (`status:"ok"|…`).
31
+
32
+ - The completion kind is **`node-output`**, NOT `output`/`write` (those legacy kinds don't appear
33
+ from current aware — a `switch` that only cased `output`/`write` silently misclassifies them).
34
+ - Each per-node event carries **`ev.node`** (the node id) + `ev.agent`/`ev.command`. Keep the raw
35
+ `node` on trace rows if anything downstream (e.g. the Execution-tab node filter) needs it.
36
+
37
+ ### The trace streams LIVE for report runs (non-obvious)
38
+
39
+ floless watches `~/.aware/logs` and broadcasts a `trace-file` SSE event as the runtime writes the
40
+ JSONL — replaying the file cumulatively. That branch is gated `!state.running`; a **report run
41
+ sets `reportRunning` (NOT `state.running`)**, so the branch fires and per-node canvas status
42
+ (running → done) lights up **in real time** behind the modal. Non-report/`simulate` runs set
43
+ `state.running`, so they paint from the **batched** trace that `/api/run` broadcasts after the CLI
44
+ returns. Either way, painting per-node status in `pushTrace` (in `web/aware.js`) covers both,
45
+ plus terminal-driven runs.
46
+
47
+ ## Verification gotchas (driving the localhost API + Playwright on Windows)
48
+
49
+ Two traps that waste time when verifying server changes against `http://127.0.0.1:4317`:
50
+
51
+ - **Rapid-fire `curl` to the localhost API returns `000` (connection failed) for all but the last
52
+ call in a tight loop.** It's Windows ephemeral-port / TIME_WAIT pressure from many short-lived
53
+ connections, NOT a server bug (the route works — a single call returns 200). To drive the API in
54
+ bulk (seeding/cleanup), use ONE `python - <<'PY' … urllib.request …` process (keeps the work in
55
+ one process, no connection storm) or space `curl`s out. The browser's SSE/keepalive connection is
56
+ unaffected, so real UI verification is fine.
57
+ - **Playwright MCP "Browser is already in use for …mcp-chrome…".** After a crash/disconnect the
58
+ MCP can't relaunch because an orphan Chrome still holds the profile. Recover: find the orphan
59
+ chrome (`Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | ? CommandLine -like
60
+ '*mcp-chrome*'`), `taskkill /PID <pid> /T /F` it (its parent is the active `@playwright/mcp`
61
+ node process), optionally delete a stale `%LOCALAPPDATA%\ms-playwright\<profile>\lockfile`, then
62
+ `browser_navigate` again — the MCP relaunches cleanly. The MCP browser can drop mid-session here;
63
+ re-attach with the same recipe and re-assert via a DOM probe (`browser_evaluate`).
64
+
65
+ ## Verifying "On trigger" (event-driven) routines without live Tekla
66
+
67
+ The trigger-routine UI (the **⏱ Routines** panel's `trigger` kind) is real-E2E-verifiable on any
68
+ machine via a `self_test:true` watch fixture — no Tekla, no `--simulate`:
69
+
70
+ - **Fixture:** `demos/tekla-watch-smoke/` — one `tekla.watch` node with `config.self_test:true`
71
+ (+ `filter:welded` etc.) → the bridge emits one `listening` then **5 synthetic `fired` events**
72
+ through the real watch code path. Install/compile like any app (`aware app install ./demos/…`;
73
+ reinstall needs `aware app uninstall` first; compile from `~/.aware/apps`). The watch's `filter`
74
+ lives in **node config**, not top-level `inputs:`, so v1 trigger routines take no source params
75
+ (an eligible app with no `inputs:` renders zero input fields — fine).
76
+ - **Eligibility:** `GET /api/app/:id` → `app.triggerSource` is non-null when the source node runs a
77
+ `lifecycle:start` + `stream` command. The Add/Edit form keys the schedule-vs-trigger chooser off
78
+ this; `tekla-watch-smoke` is eligible.
79
+ - **Live row state to assert:** create `{kind:'trigger', workflow, inputs:{}, enabled:true}` → the
80
+ row's `.rtn-next` goes `listening` → (events) → ends `stopped` (self_test exits after 5; a real
81
+ never-ending watch stays `listening · N×`). `GET /api/routines` annotates each trigger routine
82
+ with `session:{state,firedCount,lastEvent,error}` and the persisted `trigger:{nodeId,agent,command}`
83
+ — assert `firedCount>=5` there (the label may have already settled to `stopped` by the time you
84
+ poll, so gate the pass on the **API** firedCount, not only the transient label).
85
+
86
+ ### Driving the licensed dev server + Playwright (what actually works here)
87
+
88
+ - **The installed SEA server usually already holds :4317** (and it 404s `/api/status` — different
89
+ build). Don't fight it: `index.ts` honors `PORT`, so run the dev server on a free port —
90
+ `PORT=4318 FLOLESS_LICENSE_DIR="$LOCALAPPDATA/FlolessApp-data" npx tsx main.ts --serve` (from
91
+ `server/`, background). Without the license dir it serves the **sign-in gate** (no `#routines-btn`)
92
+ — assert `document.getElementById('routines-btn')` exists before driving the panel.
93
+ - **The Playwright MCP browser is flaky/locked here** ("Browser is already in use…"). A **standalone
94
+ Node script is more reliable**: `playwright-core` resolves at
95
+ `C:/Users/Pawel/AppData/Roaming/npm/node_modules/@playwright/cli/node_modules/playwright-core`;
96
+ the registry's default `chrome-headless-shell` is often missing, so pass an explicit
97
+ `executablePath` to an installed chromium build under
98
+ `%LOCALAPPDATA%/ms-playwright/chromium-<rev>/chrome-win64/chrome.exe` (1217/1208 seen). Have the
99
+ script write a `report.txt` + screenshots and `process.exit(pass?0:1)`; **run it as a background
100
+ Bash task** — the run-tool's output buffer here lags badly, but the background-task *completion
101
+ notification* reliably flushes the result. **Exercise real clicks** (e.g. click both
102
+ `.rtn-mode-btn`s and assert the layout toggles) — a default-path-only run misses dead-control bugs
103
+ (an unwired chooser shipped exactly because the first pass relied on the auto-default).
104
+ - **Footgun — a failing Bash command cancels its sibling tool calls in the same assistant turn.**
105
+ Don't batch an `Edit`/`Write` with a `grep -c`/`[ ]` test that can exit non-zero (it cancels the
106
+ edits). Append `; true` to verification one-liners, or isolate risky commands in their own turn.
107
+
108
+ ## Stop / cancel an in-flight run
109
+
110
+ `aware app run` blocks; the escape hatch for a hung/unattached host:
111
+ - `aware-adapter.ts` tracks the active run's child; `cancelActiveRun()` kills it. On Windows kill
112
+ the **whole tree** with `taskkill /pid <pid> /T /F` — `aware` spawns `node` → the host bridge,
113
+ so a bare `child.kill()` orphans the bridge.
114
+ - `run()` throws a distinct `AwareError(..., { cancelled: true })` when killed; `/api/run` surfaces
115
+ it **in-band** (HTTP 200 `{ cancelled:true }`) so it reads as "cancelled", not a fault.
116
+ - `POST /api/run/stop` → `cancelActiveRun()`. `api()` drops the `cancelled` flag (throws a bare
117
+ Error), so the UI keeps a local `cancelRequested` flag to render "Run cancelled" vs "Run failed",
118
+ and `pushTrace` stops painting the canvas once it's set (late fs-watcher events would otherwise
119
+ re-pulse a node "running" after the wipe).
@@ -0,0 +1,104 @@
1
+ # The tekla `exec` contract (Roslyn C# against the live model)
2
+
3
+ The `tekla` agent's `exec` command runs a C# **script** (Roslyn scripting, not a full program)
4
+ inside the host bridge `aware-tekla.exe`, which is connected to a running Tekla Structures
5
+ instance with a model open. Reference impl: `demos/tekla-bom-by-phase/bom-by-phase.exec.cs`.
6
+
7
+ ## Globals injected into the script
8
+
9
+ | Global | Type | Notes |
10
+ |---|---|---|
11
+ | `model` | `dynamic` (`Tekla.Structures.Model.Model`) | `null` if no live model — guard it. |
12
+ | `args` | `IDictionary<string, object?>` | The node's `config.args` block, JSON-decoded. |
13
+
14
+ `args` values are JSON-typed by the bridge: a JSON number arrives as `int`/`long`/`double`, a
15
+ string as `string`, etc. **Read defensively** — a templated `{{ inputs.phase }}` may arrive as a
16
+ number or a string depending on how it was supplied:
17
+
18
+ ```csharp
19
+ int phase = 1;
20
+ if (args != null && args.TryGetValue("phase", out var pv) && pv != null) {
21
+ if (pv is int pi) phase = pi;
22
+ else if (pv is long pl) phase = (int)pl;
23
+ else if (pv is double pd) phase = (int)pd;
24
+ else { int.TryParse(pv.ToString(), out var ps); if (ps != 0) phase = ps; }
25
+ }
26
+ ```
27
+
28
+ ## Return value
29
+
30
+ End the script with `return <object>;`. The bridge serializes it and wraps it under `result`:
31
+
32
+ ```json
33
+ { "ok": true, "result": { "ok": true, "phase": 2, "html": "<!doctype html>…" },
34
+ "host": "tekla", "host_version": "2025.0", "host_pid": 47016 }
35
+ ```
36
+
37
+ For a report node, return the HTML **inline**:
38
+ `return new { ok = true, phase, html = sb.ToString(), modelName, totalParts, … };`
39
+ floless extracts `data.result.html` for the HTML Viewer.
40
+
41
+ ## References available / NOT available
42
+
43
+ - **Available:** `System`, `System.Collections.Generic`, `System.Linq`, `System.Text`,
44
+ `System.IO`, `System.Diagnostics.Debugger`, and the Tekla Open API
45
+ (`Tekla.Structures.Model`, etc. — discovered/probed from the running install).
46
+ - **NOT referenced:** `System.Net` (so **no** `WebUtility.HtmlEncode` — escape manually),
47
+ `System.Diagnostics.Process`. Don't rely on them.
48
+
49
+ Manual HTML escape (since `System.Net` is unavailable):
50
+
51
+ ```csharp
52
+ Func<string,string> E = s => (s ?? "")
53
+ .Replace("&","&amp;").Replace("<","&lt;").Replace(">","&gt;").Replace("\"","&quot;");
54
+ ```
55
+
56
+ ## Enumerating + filtering parts
57
+
58
+ Use the selector and filter with `as Part` (there is no `GetAllObjectsWithType(typeof(Part))`
59
+ overload that takes a `Type` — that's a CS1503; `GetAllObjects()` + `as Part` is the way):
60
+
61
+ ```csharp
62
+ var en = ((Model)model).GetModelObjectSelector().GetAllObjects();
63
+ while (en.MoveNext()) {
64
+ var part = en.Current as Part; if (part == null) continue;
65
+ Phase ph = null; try { part.GetPhase(out ph); } catch { ph = null; }
66
+ if (ph == null || ph.PhaseNumber != phase) continue; // phase filter
67
+ string name = ""; part.GetReportProperty("NAME", ref name); // BEAM/COLUMN/PLATE/…
68
+ string profile = ""; part.GetReportProperty("PROFILE", ref profile);
69
+ string material = "";part.GetReportProperty("MATERIAL", ref material);
70
+ double w = 0; part.GetReportProperty("WEIGHT", ref w);
71
+ double l = 0; part.GetReportProperty("LENGTH", ref l);
72
+ // aggregate: category(NAME) → "profile|material" → {qty,weight,length}
73
+ }
74
+ string modelName; try { modelName = ((Model)model).GetInfo().ModelName; } catch { modelName = "Tekla model"; }
75
+ ```
76
+
77
+ **Categorize by `NAME`, not the .NET class.** The .NET class (`Beam`, `ContourPlate`,
78
+ `PolyBeam`) lumps beams + columns together; the report property `NAME` carries the detailer's
79
+ intent (`BEAM`, `COLUMN`, `CHANNEL`, `EMBED PLATE`, `ANGLE`, `PLATE`, `STIFF. PLATE`, …), which
80
+ is what users want grouped. Group by `NAME` (category) then by `profile|material` within each.
81
+
82
+ ## Verifying against a live model (direct bridge call)
83
+
84
+ To test exec C# without the full app runtime, send it straight to the bridge over its
85
+ JSON-stdin contract:
86
+
87
+ ```bash
88
+ BIN="C:/Users/Pawel/.aware/bridges/aware-tekla.exe" # post-#148 persistent location
89
+ python -c "import json,io; print(json.dumps({'code': io.open('x.cs',encoding='utf-8').read(), 'args':{'phase':2}}))" \
90
+ | "$BIN" exec --version 2025.0 --json-stdin
91
+ ```
92
+
93
+ The bridge reads `version`, `code`, and `args` from the stdin JSON (or `--version` flag). One
94
+ instance serves one model; if the bridge is busy (e.g. a debug session holding it), wait for it
95
+ to free.
96
+
97
+ ## Gotchas observed
98
+
99
+ - A model may have **no phase 1** (College Desert has phases 2 and 3001). A `default: 1` that
100
+ returns an empty report is correct behavior — the user changes the phase per model. Always
101
+ render an empty-state message rather than failing.
102
+ - `GetReportProperty` returns by `ref`; initialize the target first (`string x = "";`).
103
+ - The bridge commits changes only when the script wrote and the connection is live; pure-read
104
+ scripts (like BOM) need no `safety:` block. Write-mode nodes do (`*.create`, `*.update`, …).