@floless/app 0.54.1 → 0.56.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.
@@ -39,6 +39,7 @@
39
39
  "plans": { "type": "array", "items": { "$ref": "#/$defs/plan" } },
40
40
  "filter": { "$ref": "#/$defs/filter", "description": "Layer/thickness/colour filter pre-stage. `sheets[]` holds per-framing-plan geometry; the facets + `mode` + on-flag selection are GLOBAL across sheets. The saved selection scopes which lines the reader binds profile labels to per sheet; replaces the length heuristic, fallback when absent. CONFIDENTIAL: each sheet's `page.bg_b64` is machine-local, never committed." },
41
41
  "connections": { "type": "array", "items": { "$ref": "#/$defs/connection" }, "description": "Vendor-neutral connection library: each row maps a connection type → the firm's design detail# → per-platform component ids. Member ends/columns reference a row by id via `ends[].conn` / `col.conn` (legacy `note`/`detail` = fallback when `conn` is empty)." },
42
+ "joints": { "type": "array", "items": { "$ref": "#/$defs/joint" }, "description": "Placed connections (base plates, …) the connection engine expands into real 3D parts (plate/bolt/weld/cut) bound to their members. A joint may cite a `connections[]` row via `conn` for its type/detail identity; `params` carries the geometry settings (engine defaults when absent)." },
42
43
  "dims3d": { "type": "array", "items": { "$ref": "#/$defs/dim3" }, "description": "Draft-only 3D dimensions (editor annotations, model-global). World-scene mm. NOT baked into the lock / 3D scene / IFC / BOM." }
43
44
  },
44
45
  "$defs": {
@@ -178,6 +179,23 @@
178
179
  "source": { "enum": ["company-rules", "user", "drawing"], "description": "Where the row came from (drives the connection confidence band)." }
179
180
  }
180
181
  },
182
+ "joint": {
183
+ "type": "object",
184
+ "required": ["id", "kind", "main"],
185
+ "additionalProperties": true,
186
+ "description": "A placed connection — the geometry-bearing recipe the connection engine expands into real 3D parts bound to its member(s). Distinct from the `connections[]` library (vendor-neutral type/detail/targets identities a joint can cite). The engine derives all geometry from this recipe + the live member positions, so a joint moves/re-solves with its members. v1 kinds: base-plate, shear-plate.",
187
+ "properties": {
188
+ "id": { "type": "string", "description": "Stable joint id, e.g. j-bp-1 / sp-b3-e0." },
189
+ "kind": { "type": "string", "description": "Connection kind the engine knows how to expand: base-plate, shear-plate (v1). end-plate / cap-plate / … follow." },
190
+ "main": { "type": "string", "description": "Id of the host member this joint hangs off. For a base-plate this is the column; for a shear-plate this is the SUPPORTED BEAM (the joint sits at one of the beam's ends — see `at`)." },
191
+ "secondaries": { "type": "array", "items": { "type": "string" }, "description": "Ids of the other members this joint binds — e.g. the SUPPORT a shear-plate frames into. Optional and inferred by geometry; the shear-plate engine does not require it (the fin plate is placed off the beam end + beam axis). Empty for a base-plate." },
192
+ "at": { "type": "string", "description": "Where on the main member the joint sits (kind-dependent): base-plate = base; shear-plate = end0 | end1 — the beam end framing into the support (end0 ↔ ends[0]/start, end1 ↔ ends[1]/end)." },
193
+ "conn": { "type": "string", "description": "Optional `connections[]` row id this joint cites for its type/detail identity (keeps the connection library + confidence scoring in play)." },
194
+ "params": { "type": "object", "additionalProperties": true, "description": "Geometry settings (mm) with engine defaults; absent = all defaults. base-plate: plateWidth/plateDepth/thickness, boltCols/boltRows/boltDia/edgeDist, weldLeg, anchor kit, levelingNut. shear-plate: plateThickness, plateWidth (along the beam), plateHeight (vertical), boltCols/boltRows/boltDia/boltPitch/edgeDist, weldLeg, clearance (the erection gap so the beam clears the support, ~½–¾\"; pulls the beam box back), webSide (+1/-1 — which face of the beam web the plate laps), boltGrade (A325 | A490 — the AISC bolt length auto-sizes from the grip), stiffener (bool — a full-height stiffener on the far side of a girder web)." },
195
+ "source": { "enum": ["auto", "user"], "description": "auto = proposed by auto-detail from the takeoff; user = placed/edited by hand." },
196
+ "verified": { "type": "boolean", "description": "True = a human confirmed this joint." }
197
+ }
198
+ },
181
199
  "point2": {
182
200
  "type": "array",
183
201
  "items": { "type": "number" },
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: floless-app-new-workflow
3
+ description: This skill should be used to build a brand-new FloLess .flo workflow from scratch, step by step, as the host terminal AI — interviewing the user about what they want, checking that the AWARE agents the workflow needs are installed (installing an available one on consent, or reporting a not-yet-existing agent as an idea), then authoring the .flo node by node and verifying each step with the real toolchain (plain-English descriptions, aware app validate, aware app compile, a real run) before installing it so it appears in the floless.app picker for Compile and Run. Use it on triggers like "build a new workflow", "create a workflow from scratch", "build me a workflow", "new workflow", the floless.app "New workflow…" menu item, a queued new-workflow request, or "/floless-app-new-workflow". It leans on floless-app-workflows for the exec contract and hands back a runnable, approved app.
4
+ metadata:
5
+ version: 0.1.0
6
+ ---
7
+
8
+ # Build a new floless.app workflow from scratch
9
+
10
+ Guide the user from a blank slate to a **runnable, approved** `.flo` app, as the **host terminal
11
+ AI** they are talking to. floless.app is a **thin web UI**; the terminal AI (you) is the brain and
12
+ AWARE is the runtime. This skill makes you the builder — interview the user, confirm the agents the
13
+ workflow needs exist, then author the `.flo` **one node at a time, verifying each step with the real
14
+ toolchain before moving on**, and hand back an installed app the user can Compile and Run in the UI.
15
+
16
+ This is the doing-skill the onboarding tour points to ("want to build your first workflow?"). It is
17
+ the **create-from-scratch sibling** of the per-workflow guide (which explains an *existing* app).
18
+
19
+ The contract details (exec globals, return shape, templating, layout, gotchas) live in
20
+ **floless-app-workflows** — lean on it, do not re-derive it here. This skill owns the *interview →
21
+ preflight → author-and-verify → hand-back* flow; the verification mechanics live in
22
+ `references/build-and-verify.md` (read it when entering Step 3).
23
+
24
+ ## Operating principles (apply throughout)
25
+
26
+ - **One step at a time, verified.** Author a node, then **run the real check that proves it** before
27
+ the next (description prose, `aware app validate`, `aware app compile`, a real `aware app run`).
28
+ Never stack three unverified nodes and hope. A failing check is the signal to fix *that* step.
29
+ - **Drive live, don't lecture.** Probe the user's real state (`/api/health`, `aware agent list`) and
30
+ build against it. A real install→compile→run beats prose.
31
+ - **Mutations need consent.** Installing an agent, writing the draft `.flo`, installing the app —
32
+ each is **offered, then done on an explicit yes**. Read-only probes run freely.
33
+ - **Never invent.** Don't name an agent, command, or node kind that isn't real — verify against
34
+ `aware agent list` / `--available` and floless-app-workflows. A made-up agent fails at validate.
35
+ - **Determinism is AWARE's.** The `.lock` is the approved contract; Run is gated on source-hash
36
+ freshness. Author the source; let `aware app compile` freeze it. Never teach around the gate.
37
+ - **Plain-English descriptions are mandatory.** Every node's leading `//` comment is its Description
38
+ (non-coders read it). Author it as prose as you write each node — it is also a verified step.
39
+
40
+ ## Step 0 — Pick up the ask and orient
41
+
42
+ 1. **If launched from the UI**, a `new-workflow` request is queued. Fetch the authoritative spec and
43
+ clear it when done (the house handshake):
44
+ ```bash
45
+ curl -s http://127.0.0.1:4317/api/requests # find the {"type":"new-workflow", id} entry
46
+ # …do the work below…
47
+ curl -s -X DELETE http://127.0.0.1:4317/api/requests/<id> # only after the app is installed
48
+ ```
49
+ If invoked directly ("/floless-app-new-workflow", "build me a workflow"), skip the queue.
50
+ 2. **Confirm the ground** (read-only): `curl -s http://127.0.0.1:4317/api/health` →
51
+ `{"ok":true,"awareVersion":"<v>", ...}`. No response → the floless.app desktop app isn't running;
52
+ the user can still author from the terminal, but the final Compile/Run happens in the UI, so ask
53
+ them to start it. `license` not `valid` → the UI's Run sits behind a sign-in gate; mention it when
54
+ you reach Run, don't block authoring.
55
+
56
+ ## Step 1 — Interview (what should it do?)
57
+
58
+ Pull the goal out in **one or two questions at a time**, plain English — never a wall:
59
+ - "What do you want this workflow to do?" (e.g. "list parts by phase", "export a BOM", "write a
60
+ greeting"). Get the *outcome* first.
61
+ - Then the shape: what **input(s)** does it take (a phase number? a file? nothing?), what **output**
62
+ the user wants to see (an HTML report in the Viewer? a value?), and which **host** (Tekla? none?).
63
+
64
+ Restate it in one sentence and get a nod before authoring. Keep it small — the first version should
65
+ **run**; extend it after. (Laziness: ship the one-node version that works, then grow it.)
66
+
67
+ ## Step 2 — Agent preflight (before writing any node)
68
+
69
+ A node references an `agent` (e.g. `tekla`, `file`). **Confirm each one exists before authoring it** —
70
+ a workflow built on a missing agent fails at validate. Read `references/build-and-verify.md` →
71
+ "Agent preflight" and run the three-way check: **installed** (`aware agent list`) → use it;
72
+ **available but not installed** (`aware agent describe <id> --available`) → offer to install it
73
+ (consent); **truly absent from the catalogue** → it's a capability gap → offer to **report it as an
74
+ idea** (the reference spells out the report relay + the `aware` label). Never silently substitute or
75
+ invent an agent.
76
+
77
+ ## Step 3 — Author the `.flo`, verifying each step
78
+
79
+ Read `references/build-and-verify.md` and follow the loop: write the draft under
80
+ `~/.floless/drafts/<id>/<id>.flo`, scaffold the **smallest valid shape**, then build up node by node —
81
+ **each node gets its plain-English `//` description and a real check before the next.** The reference
82
+ maps every step to the command that proves it (`validate` → `compile` → `install` → real `run`, read
83
+ the trace). For exec specifics (globals, `return`, no `System.Net`/Regex, no `{{ }}` in code
84
+ comments, `layout: linear` vs `dag`), defer to **floless-app-workflows** — don't reinvent them.
85
+
86
+ ## Step 4 — Hand back to the UI (Compile → Run → approve)
87
+
88
+ Once the app **installs and a real run is green**, hand it to the user in floless.app:
89
+ 1. It now appears in the **workflow picker** (top-right) — have them select it.
90
+ 2. **Set inputs** on the input node (if any), then **Compile** (freezes the approved `.lock`; the Run
91
+ gate opens only when source-hash matches).
92
+ 3. **▶ Run workflow** against the live host — the result renders in the canvas / HTML Viewer.
93
+ 4. The user **approves** what they see. Done — they built a workflow from nothing.
94
+
95
+ Then DELETE the queued request (Step 0) if one launched this.
96
+
97
+ ## Close
98
+
99
+ Recap in two lines what they built and how to grow it ("want a second node that exports a BOM?
100
+ re-run /floless-app-new-workflow, or just tell me"). Offer the obvious next step — bake it into a
101
+ reusable agent (**floless-app-rebake** / Bake), or automate it on a trigger (**floless-app-routines**).
102
+
103
+ ## Guardrails
104
+
105
+ - **Verify every step — no "probably works".** A node isn't done until its check passes (description
106
+ prose, validate, compile, a real run with a green trace). `--simulate` does NOT count — it stubs
107
+ every node and emits no real output.
108
+ - **Read-only by default; mutations on consent.** Installing an agent, writing the draft, installing
109
+ the app — offer, then act on an explicit yes.
110
+ - **Never invent agents/nodes.** Verify against `aware agent list` / `--available`; report a real gap
111
+ as an idea, don't fake it.
112
+ - **Don't contradict the architecture.** The UI never composes the workflow or calls an LLM; you (the
113
+ terminal AI) author it, AWARE compiles/runs it, the `.lock` is the approved contract.
114
+ - **Lean on floless-app-workflows** for the exec contract and the run-trace details — this skill
115
+ orchestrates; it does not duplicate the contract reference.
@@ -0,0 +1,117 @@
1
+ # Build-and-verify loop + agent preflight
2
+
3
+ The mechanics behind Steps 2–3 of `SKILL.md`. Every step here maps to a **real command that proves
4
+ it** — that is the whole point of this skill (build a workflow whose every step is checked, not
5
+ guessed). For the exec contract itself (globals, `return`, templating, layout, gotchas) read
6
+ **floless-app-workflows**; this file does not duplicate it.
7
+
8
+ All paths resolve at runtime — `~` is the user's home; never hardcode an absolute `C:\…` / `D:\…`.
9
+
10
+ ## Where a new draft lives
11
+
12
+ A user's brand-new workflow is **not** a repo demo — author it in a user-owned scratch dir, one
13
+ directory per app (the install/compile/run verbs all take a *directory*, never a bare `.flo`):
14
+
15
+ ```
16
+ ~/.floless/drafts/<id>/<id>.flo
17
+ ```
18
+
19
+ `<id>` is the app id (lowercase, `a–z 0–9 - _`), the same string as the `.flo`'s top-level `app:`
20
+ field. Create the dir, write the `.flo` into it, and run every verb against `~/.floless/drafts/<id>`.
21
+
22
+ ## The smallest valid shape (scaffold, then grow)
23
+
24
+ Start with the **one-node app that runs**, prove it, then add nodes. The minimal valid `.flo`:
25
+
26
+ ```yaml
27
+ app: my-first # required — the id; matches the folder name
28
+ version: 0.1.0 # required — semver
29
+ display-name: My First # required for the picker label (validator doesn't enforce it)
30
+ description: | # required — plain-English paragraph for non-coders
31
+ What this workflow does, in one or two sentences.
32
+ requires: [] # conventional — list the preflighted agents (e.g. [tekla@0.1.x]); every demo carries one
33
+ layout: linear # linear (node-order chain) | dag (explicit connections)
34
+ nodes:
35
+ - id: hello
36
+ agent: tekla # a REAL agent (preflight first); model-free exec needs no open model
37
+ command: exec
38
+ mode: read
39
+ config:
40
+ version: "2025.0" # required for tekla/exec — the Tekla version
41
+ args: {}
42
+ code: |
43
+ // Plain-English description of what this node does — this IS the node's Description.
44
+ return new { ok = true };
45
+ ```
46
+
47
+ Grow by adding nodes one at a time. Inputs → `inputs:` block, templated `{{ inputs.<name> }}`;
48
+ cross-node data → `{{ <node-id>.result.<field> }}`. **Any branch (fan-out/fan-in) needs `layout: dag`
49
+ + a `connections:` block** — a straight chain stays `linear`. (Details + footguns →
50
+ floless-app-workflows. Critical one: **never write `{{` or `}}` inside a `code:` block**, even in a
51
+ `//` comment — it is templated and a stray brace aborts the run.)
52
+
53
+ ## The verify-each-step loop
54
+
55
+ Author a node (with its `//` description), then run the checks **in order** before the next node.
56
+ Each command is the proof for that step:
57
+
58
+ | Step | Command (run from anywhere; dir = the draft) | Proves |
59
+ |---|---|---|
60
+ | 1. Descriptions are plain English | eyeball the leading `//` of every node + the top-level `description:` — prose, ≥3 words, no raw code/markup. (The repo's `npm run verify:flo` enforces this for committed demos; a user draft lives outside `demos/`, so apply the rule yourself.) | non-coders can read the Inspect → Description tab |
61
+ | 2. Structural validity | `aware app validate ~/.floless/drafts/<id>` | YAML parses; required fields present; node topology sound; **every referenced agent exists** (else `E_APP_AGENT_UNAVAILABLE` — back to preflight) |
62
+ | 3. Install | `aware app install ~/.floless/drafts/<id>` (re-installing the same id? `aware app uninstall <id>` first — a same-id install errors `app <id> already installed`) | the app lands in `~/.aware/apps/<id>/` and shows in the floless.app picker |
63
+ | 4. Compile → lock | `aware app compile ~/.floless/drafts/<id>` | resolves agents, stamps `source-hash`, writes `<id>.lock` into the installed app dir — the approved, runnable contract |
64
+ | 5. Real run | `aware app run <id> --input k=v --json` | every node executes for real; trace at `~/.aware/logs/<id>/<instance>/<run>.jsonl` |
65
+ | 6. Read the trace | open that `.jsonl` | every node emits `node-output` with `ok:true` and it ends `run-end · status:ok` |
66
+
67
+ **`--simulate` is not a check** — it stubs every node, emits no real `result.<field>`, and renders
68
+ templates as `undefined`. A green simulate proves nothing. Always do a real run.
69
+
70
+ ### No Tekla open? Still do a real run.
71
+
72
+ A `tekla/exec` node runs Roslyn in the host bridge `aware-tekla.exe` — it needs the **bridge**, not a
73
+ live model. A node that **never dereferences `model`** (builds a string/HTML, reads an arg, returns
74
+ data) runs with nothing open in Tekla. So a from-scratch first workflow that just returns a value or
75
+ builds an HTML report verifies end-to-end with no model. If the bridge is stale,
76
+ `aware sidecar install tekla` refreshes it. (A C# compile error surfaces only as an opaque
77
+ `agent tekla/exec failed (exit Some(2))` — isolate by checking your `code` compiles; no `System.Net`,
78
+ **no `System.Text.RegularExpressions`**, manual HTML-escape. See floless-app-workflows.)
79
+
80
+ When step 6 is green, the app is **installed, compiled, and proven** — hand back to the UI (SKILL.md
81
+ Step 4). If a step fails, fix **that** node/field and re-run from the failed step; don't push past it.
82
+
83
+ ## Agent preflight (Step 2 in full)
84
+
85
+ Before authoring a node on an `agent`, confirm that agent exists. Three outcomes:
86
+
87
+ 1. **Installed** — `aware agent list` shows it → use it. (`tekla` and `file` are the common ones.)
88
+ 2. **Available but not installed** — not in `list`, but `aware agent describe <id> --available` finds
89
+ it in the registry catalogue → **offer to install it** (consent, it's a mutation):
90
+ - host bridges (tekla, rhino, sketchup, revit) → `aware sidecar install <host>`
91
+ - other catalogue agents → `aware agent install <id>`
92
+ Before assuming a gap, `aware agent search <term>` so a **misspelling or a differently-named
93
+ existing agent** isn't mistaken for "missing" (e.g. the user says "spreadsheet" but it's `file`).
94
+ 3. **Truly absent** — not installed, and `aware agent describe <id> --available` reports not-found
95
+ (and `search` turns up nothing) → a genuine **AWARE capability gap**. Don't fake it, don't
96
+ substitute silently. Offer to **report it as an idea** (below), then either pick a different
97
+ approach with the agents that *do* exist, or pause that node until the agent ships.
98
+
99
+ ### Reporting a missing agent as an idea
100
+
101
+ A not-yet-existing agent is a legitimate "missing-but-expected capability" — file it so it can be
102
+ built. Route it through **floless-app-report-issue** (it owns the floless.io relay) as an **idea**:
103
+
104
+ - `POST http://127.0.0.1:4317/api/report-issue` with `category: "idea"`, a title prefixed
105
+ `[AWARE agent] <id> —` and a body that states: the **capability needed** (what host/agent, what the
106
+ workflow would do with it), **why** (the user's real goal from Step 1), and the **proof it's
107
+ absent** (the `aware agent describe <id> --available` not-found / the `E_APP_AGENT_UNAVAILABLE`
108
+ from validate).
109
+ - The relay files it into the **floless.app** issue tracker, tagged `idea` + `user-report` — so it is
110
+ known to come from a real floless user via the licensed relay, not an anonymous open-source report
111
+ (and the public `aware-aeco/aware` tracker stays clean). The maintainer triages these under the
112
+ `aware` label and promotes the real ones into actual AWARE agents.
113
+ - This is **consent-gated and read-only to the user's machine** — it sends a report, it changes
114
+ nothing locally. Confirm the user wants it filed before sending.
115
+
116
+ Discriminator (don't file noise): only a *genuinely absent* agent is an idea. A typo, a wrong
117
+ capability guess, or a validate error caused by your own bad `code` is **not** a gap — fix the usage.
@@ -91,7 +91,8 @@ tell them how to come back:
91
91
  > section ('the agents part', 'Bake & Graft', 'routines') and I'll jump straight there."
92
92
 
93
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).
94
+ workflow?" (hand off to **floless-app-new-workflow**, the step-by-step from-scratch builder or
95
+ **floless-app-workflows** for the raw authoring reference) or "install one of those agents?" (A4).
95
96
 
96
97
  ## Guardrails
97
98
 
@@ -62,10 +62,14 @@ the project folder (a few files, **not** a framework):
62
62
  section / detail / schedule). This is the map the later stages classify against.
63
63
  - **`general-notes.md`** — the General Notes that bind the whole job: design spec, default
64
64
  connection type, bolt/weld standard, material grades, anything that overrides a single sheet.
65
- - **`company-rules.md`** *(ask first)* — **ask whether the firm has company-specific standards**
66
- (e.g. "moment → detail 50S504 → Tekla component 146"). If yes, capture them here. This file feeds
67
- the connection-library pre-fill in stage 6 (the editor's Connections panel reads these as
68
- `source:"company-rules"`). If the firm has none, skip the file don't invent rules.
65
+ - **`company-rules.md`** *(ask first)* — **ask whether the firm has company-specific standards.** Two
66
+ kinds, both captured here: (1) **connection-library mappings** (e.g. "moment → detail 50S504 → Tekla
67
+ component 146") that feed the connection-library pre-fill in stage 6 (the editor's Connections panel
68
+ reads these as `source:"company-rules"`); and (2) **firm DETAILING standards** default bolt grade +
69
+ length-from-grip, usual gages, near/far-side, shear-tab shape/edge/clearance — that the Steel Takeoff
70
+ reader applies to a connection's `joints[].params` when detailing (see `floless-app-steel-takeoff`
71
+ §5.5b). The file is machine-local / the firm's private IP (it may live in the user's own synced Drive)
72
+ — **never committed.** If the firm has none, skip it — don't invent rules.
69
73
 
70
74
  Read the set with the terminal AI's own vision, following the **`reading-structural-drawings`**
71
75
  discipline while skimming (massing + coordinate frame first — it's what makes the sheet
@@ -2,7 +2,7 @@
2
2
  name: floless-app-steel-takeoff
3
3
  description: Read a structural steel drawing into an editable steel.takeoff/v1 contract in floless.app — the "Steel Takeoff" reader. Triggers on a pending floless `rebake` request for the `steel-model` app (queued from its "Re-read & re-bake ▸" button), or asks like "read this framing plan into a takeoff", "turn this structural drawing into a takeoff", "bake my drawing into steel-model", "extract members from this PDF". Teaches the host AI to READ a steel drawing (PDF/image) with its own vision at COMPOSE time, build a steel.takeoff/v1 contract (members + profiles + elevations + weights from AISC lookup), write it to the contract store, and hand control to the editor — no model in the run path, no API key.
4
4
  metadata:
5
- version: 0.1.0
5
+ version: 0.2.0
6
6
  ---
7
7
 
8
8
  # Steel Takeoff — read a drawing into a steel.takeoff/v1 contract
@@ -293,10 +293,93 @@ Key field rules:
293
293
  when `conn` is empty. `source` is `drawing` (read from a callout — best evidence), `company-rules`
294
294
  (the firm's standard), or `user`. Prefer a `conn` reference over free text when the connection is
295
295
  known — the editor's per-end picker and the Connections confidence card both key on it.
296
+ - **Base-plate connections (`joints[]`) — the geometry recipe.** Where the set has a **base-plate
297
+ detail + schedule** (column size → base-plate THK×"N"×"B", anchor Ø×embed, weld "A", washer plate),
298
+ emit a top-level `joints[]` entry per column so FloLess renders the real plate + anchor kit + weld
299
+ in 3D: `{ id, kind:"base-plate", main:"<column member id>", at:"base", source:"auto", params:{…} }`.
300
+ The joint binds to the column by id and may CITE a `connections[]` row via `conn` for its
301
+ type/detail identity (build on the library, don't fork it). Map the schedule straight into `params`:
302
+
303
+ | Schedule callout | `params` key(s) |
304
+ |---|---|
305
+ | base-plate THK · "N" · "B" | `thickness` · `plateWidth` · `plateDepth` |
306
+ | anchor bolt Ø · count | `boltDia` · `boltCols`×`boltRows` |
307
+ | weld size "A" | `weldLeg` |
308
+ | "provide 1/4"×3"×3" PL washer w/ std hole" | `washerPlateThickness` · `washerPlateSide` |
309
+ | "anchor holes oversized: A-bolt Ø + 5/16"" | `holeClearance` (the +amount) |
310
+ | edge distance · embed | `edgeDist` · `projBelow` |
311
+
312
+ ⚠️ **UNITS: the *dimensional* `params` are MILLIMETRES** (the engine works in mm) — unlike the
313
+ contract's display/inch member coords — so convert the schedule's inches ×25.4. (`boltCols` /
314
+ `boltRows` are integer *counts*, not mm; `projBelow` is how far the rod renders below the plate — a
315
+ reasonable proxy for the embed.) Any omitted param falls back to a sensible default. Worked example
316
+ (a `W10x33 → 1"×18"×10", 1"Ø×12", 5/16"` schedule row):
317
+ `params: { thickness:25.4, plateWidth:457.2, plateDepth:254, boltDia:25.4, boltCols:2, boltRows:2, edgeDist:50.8, weldLeg:7.94, washerPlateThickness:6.35, washerPlateSide:76.2, holeClearance:7.94, projBelow:304.8 }`.
318
+ The weld automatically traces the column's **profile outline** (the I-shape for a W; a rectangle for
319
+ an HSS — square only when the section is square). *(The outline is engine-rendered: a wide-flange's
320
+ flange width is still approximate until a real AISC `bf` table lands, so a W renders a touch narrow —
321
+ the plate/anchor/weld sizes you set are exact.)*
322
+ - **Shear-plate (fin-plate) connections (`joints[]`) — the bolted beam-end recipe.** Where a beam end
323
+ reads as a **shear / simple / fin-plate / shear-tab** connection (the per-end `note`/`conn`), emit a
324
+ `joints[]` entry **per beam end** so FloLess renders the vertical fin plate (bolted to the web) + the
325
+ bolt group + the plate-to-support weld in 3D:
326
+ `{ id, kind:"shear-plate", main:"<SUPPORTED BEAM member id>", at:"end0"|"end1", source:"auto", params:{…} }`.
327
+ ⚠️ **`main` is the BEAM** (not the support) — the joint sits at one of the beam's ends; `at:"end0"` =
328
+ the start end (`ends[0]`), `at:"end1"` = the far end (`ends[1]`). Optionally name the support in
329
+ `secondaries:["<column/girder id>"]` (metadata only — the geometry is placed off the beam end + beam
330
+ axis, so it's not required). May CITE a `connections[]` row via `conn` for its type/detail identity.
331
+ Map the schedule into `params`:
332
+
333
+ | Schedule callout | `params` key(s) |
334
+ |---|---|
335
+ | plate THK · width (along beam) · height | `plateThickness` · `plateWidth` · `plateHeight` |
336
+ | bolt Ø · rows×cols · pitch/gauge | `boltDia` · `boltRows`×`boltCols` · `boltPitch` |
337
+ | weld size | `weldLeg` |
338
+ | beam clearance / erection gap off the support (≈½–¾") | `clearance` |
339
+ | which face of the beam web the plate laps | `webSide` (+1 / -1) |
340
+ | bolt grade (AISC) — A325 or A490 | `boltGrade` ("A325" / "A490") |
341
+ | "stiffener / backing plate on the far side" | `stiffener: true` |
342
+
343
+ ⚠️ **UNITS: the *dimensional* `params` are MILLIMETRES** (×25.4 from inches) — `boltCols`/`boltRows`
344
+ are integer counts; `stiffener` is a boolean; `webSide` is +1/-1. The fin plate is VERTICAL (in the
345
+ beam-web plane) and **laps one face of the web** (not centred — `webSide` picks the face), cantilevered
346
+ toward the beam from the support edge; the **`clearance` pulls the beam back** so it clears the support
347
+ (the cope/cut is a later slice). Omitted params fall back to sensible defaults (10 mm plate, 1×3 M20
348
+ bolts at 70 mm, 6 mm weld, ~13 mm clearance; A325 grade). The bolt **length is not a param** — it
349
+ auto-sizes from the grip (clamped plies) per the AISC A325/A490 chart and rides the bolt callout (the
350
+ 3D part label); `boltGrade` only picks A325 vs A490. For a beam-to-beam joint, name the supporting girder in
351
+ `secondaries:["<girderId>"]` so `stiffener:true` draws a full-height stiffener on the girder web.
352
+ Worked example (a 3/8"×4"×8" tab, 3×3/4" bolts @ 3", far end of beam b3): `{ id:"sp-b3-e1",
353
+ kind:"shear-plate", main:"b3", at:"end1", source:"auto", params:{ plateThickness:9.5, plateWidth:101.6,
354
+ plateHeight:203.2, boltCols:1, boltRows:3, boltDia:19.05, boltPitch:76.2, weldLeg:6.35, clearance:12.7 } }`.
296
355
  - **Company-rules pre-fill (§5.5).** If a firm-standard knowledge file is present (e.g.
297
356
  `company-rules.md`: "moment → 50S504 → Tekla 146"), read it and emit those rows in `connections[]`
298
357
  with `source: "company-rules"`, then PUT. The user reviews/edits them in the editor's
299
358
  **Connections** library panel and assigns a row per member end. No new agent verb — it's contract data.
359
+ - **Firm DETAILING standards → shear-plate `joints[].params` (§5.5b).** The same `company-rules.md` is
360
+ also the home for the firm's **detailing** standards (the firm's IP / AISC-*Manual* reference data —
361
+ kept **machine-local, NEVER committed**, and out of the public `aware` AISC agent, which is
362
+ free-source only). When detailing a **shear plate**, read these and set the matching `joints[].params`
363
+ (and say you applied the firm rule), instead of leaning on the engine defaults:
364
+ - **Bolt grade + length** — the firm's default grade → `boltGrade` ("A325"/"A490"). The bolt length
365
+ auto-sizes from grip in the engine per the firm's A325/A490 bolt-length chart; just confirm the
366
+ grade. *(The firm's chart in `company-rules.md` mirrors AISC Manual Table 7-18 add-to-grip — facts
367
+ for the firm's own use; do not republish the table.)*
368
+ - **Usual gages** — the firm's bolt gauge `g` by flange width / leg (W-beams, angles) → `boltPitch`
369
+ and the bolt grid; e.g. a W-beam web gage or a standard 3" pitch. Read the gage table.
370
+ - **Near-side / far-side (NS-FS)** — the firm's default web face the shear plate laps → `webSide`
371
+ (+1 near / -1 far). Many shops default NS unless the drawing says FS.
372
+ - **Knife-plate / shear-tab shape + edge distance + clearance** — the firm's standard plate
373
+ proportions, edge distance, and erection clearance → `plateWidth`/`plateHeight`/`edgeDist`/
374
+ `clearance`. Use the firm's value when it differs from the engine default.
375
+ - **Sizing stays detailing-not-design.** Apply the firm's DEFAULTS + flag a drawing value that
376
+ conflicts; never invent a number. The drawing's explicit callout still wins over the firm default.
377
+ - *(Base plates have their OWN firm keys — `plateWidth`/`plateDepth`/`thickness`/`edgeDist`/
378
+ `boltCols`×`boltRows`/`boltDia`/`weldLeg`/`holeClearance`/`projBelow` — no grade/pitch/webSide;
379
+ apply the firm's base-plate schedule there the same way.)*
380
+ Format in `company-rules.md`: plain headed sections the AI can read (e.g. `## Bolts` with the grade
381
+ default + the length chart, `## Gages`, `## Shear tab (NS-FS, edge, clearance)`). The author keeps it
382
+ private; FloLess just reads + applies it.
300
383
  - **Clickable callouts (`callouts[]`).** Harvest the cross-reference marks drawn ON the plan that
301
384
  point to ANOTHER sheet — **section cuts** (e.g. a circle with `A` over `A` and a sheet ref),
302
385
  **elevation/interior-elevation marks**, and **detail references** — as per-plan
package/dist/web/app.css CHANGED
@@ -460,6 +460,15 @@
460
460
  /* Secondary AI-native pointer (replaces the old raw `aware app install` chip — confusing for a
461
461
  newcomer and off-message against "your terminal AI builds the workflows"). */
462
462
  .onb-foot { font-size: 12px; color: var(--text-muted); margin-top: 16px; line-height: 1.5; }
463
+ /* Quiet secondary first-run action — an inline text button, NOT a second filled
464
+ button, so "Start the guided tour" stays the dominant primary. Existing tokens only. */
465
+ .onb-build-btn {
466
+ background: none; border: none; padding: 0; cursor: pointer;
467
+ font-family: var(--ui); font-size: 12px; font-weight: 600; color: var(--accent);
468
+ letter-spacing: 0.01em;
469
+ }
470
+ .onb-build-btn:hover { color: var(--accent-bright); text-decoration: underline; }
471
+ .onb-build-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
463
472
  /* "Start the guided tour" — the primary first-run action (copies the onboarding launch prompt).
464
473
  Accent-soft region + filled-accent button mirror the .agent-card.selected / #run-btn primary
465
474
  tier; all tokens existing (locked baseline — no new fonts/colors). */
@@ -479,6 +488,152 @@
479
488
  .onb-tour-btn:disabled { cursor: default; opacity: 0.85; box-shadow: none; }
480
489
  .onb-tour-ico { font-size: 11px; opacity: 0.7; margin-left: 6px; }
481
490
 
491
+ /* ===== Per-workflow onboarding beacon ("Start here") ===== */
492
+ /* A header pill in the run cluster. Calm by default (accent-soft, like the
493
+ wf-update pill); pulses while the open workflow is new to the user. Accent —
494
+ NOT warn — because it's positive/new, and warn is already the drift colour. */
495
+ .guide-beacon {
496
+ display: inline-flex;
497
+ align-items: center;
498
+ gap: 5px;
499
+ border: 1px solid var(--accent-dim);
500
+ background: var(--accent-soft);
501
+ color: var(--accent-bright);
502
+ font-family: var(--ui);
503
+ font-size: 11px;
504
+ letter-spacing: 0.02em;
505
+ padding: 3px 11px;
506
+ border-radius: 999px;
507
+ cursor: pointer;
508
+ white-space: nowrap;
509
+ flex-shrink: 0; /* the workflow picker shrinks first; the beacon never squashes */
510
+ transition: border-color 0.15s, color 0.15s, background 0.15s;
511
+ }
512
+ .guide-beacon[hidden] { display: none; }
513
+ .guide-beacon:hover { border-color: var(--accent); color: var(--accent); }
514
+ .guide-beacon:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
515
+ .guide-beacon-ico { font-size: 11px; line-height: 1; }
516
+ /* "New to you": a slow box-shadow bloom (GPU-composited, no layout repaint) reading
517
+ as a calm invitation, not an alarm. Same motion dialect as the rest of the app. */
518
+ .guide-beacon.is-new {
519
+ border-color: var(--accent);
520
+ color: var(--accent-bright);
521
+ animation: guide-pulse 2.8s ease-in-out infinite;
522
+ }
523
+ @keyframes guide-pulse {
524
+ 0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); }
525
+ 50% { box-shadow: 0 0 0 6px rgba(74, 158, 255, 0); }
526
+ }
527
+ @media (prefers-reduced-motion: reduce) {
528
+ /* No pulse — a steady ring (shape + contrast, not colour alone) marks "new". */
529
+ .guide-beacon.is-new { animation: none; box-shadow: 0 0 0 1px var(--accent-dim); }
530
+ }
531
+ /* Spotlit (first-open): raised above the dim, opaque, ringed. The header creates no
532
+ stacking context, so z-index here lifts it above the fixed spotlight overlay. */
533
+ .guide-beacon.spotlit {
534
+ position: relative;
535
+ z-index: 95;
536
+ animation: none;
537
+ background: var(--surface-3);
538
+ border-color: var(--accent);
539
+ color: var(--accent-bright);
540
+ box-shadow: 0 0 0 3px var(--accent-glow), 0 0 20px 4px var(--accent-glow);
541
+ }
542
+
543
+ /* ===== First-open spotlight ===== */
544
+ .guide-spotlight {
545
+ position: fixed;
546
+ inset: 0;
547
+ z-index: 90; /* below modals (100), above the page; the beacon (.spotlit, 95) sits over it */
548
+ background: rgba(7, 10, 15, 0.72);
549
+ backdrop-filter: blur(2px);
550
+ -webkit-backdrop-filter: blur(2px);
551
+ display: none;
552
+ }
553
+ .guide-spotlight:not([hidden]) { display: block; animation: fade-in 0.18s ease-out; }
554
+ .guide-spotlight-bubble {
555
+ position: fixed;
556
+ z-index: 91;
557
+ max-width: 280px;
558
+ background: var(--surface);
559
+ border: 1px solid var(--accent-dim);
560
+ border-radius: 8px;
561
+ padding: 12px 14px;
562
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
563
+ }
564
+ .guide-spotlight-text { font-size: 12.5px; line-height: 1.5; color: var(--text); }
565
+ .guide-spotlight-text strong { color: var(--accent-bright); }
566
+ .guide-spotlight-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 11px; }
567
+ .guide-spotlight-actions button { padding: 5px 13px; font-size: 12px; }
568
+ .guide-spotlight-actions .ghost { background: var(--surface-2); color: var(--text-muted); }
569
+ .guide-spotlight-actions .ghost:hover { color: var(--text); border-color: var(--accent-dim); }
570
+ .guide-spotlight-actions .primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
571
+ .guide-spotlight-actions .primary:hover { background: var(--accent-bright); box-shadow: 0 0 14px var(--accent-glow); }
572
+
573
+ /* ===== Guide panel (modal) ===== */
574
+ .modal.guide {
575
+ width: 520px;
576
+ max-width: 92vw;
577
+ max-height: 82vh;
578
+ display: flex;
579
+ flex-direction: column;
580
+ }
581
+ .guide-eyebrow {
582
+ font-size: 10px;
583
+ text-transform: uppercase;
584
+ letter-spacing: 0.16em;
585
+ color: var(--accent-bright);
586
+ margin-bottom: 6px;
587
+ }
588
+ /* The body scrolls between the pinned title and the pinned actions; theme its
589
+ scrollbar so no native white bar leaks against the dark surface. */
590
+ #guide-body {
591
+ overflow-y: auto;
592
+ flex: 1 1 auto;
593
+ min-height: 0;
594
+ margin-top: 2px;
595
+ scrollbar-width: thin;
596
+ scrollbar-color: var(--border-strong) transparent;
597
+ }
598
+ #guide-body::-webkit-scrollbar { width: 7px; }
599
+ #guide-body::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 7px; }
600
+ #guide-body::-webkit-scrollbar-track { background: transparent; }
601
+ .guide-section-label {
602
+ font-size: 10px;
603
+ text-transform: uppercase;
604
+ letter-spacing: 0.14em;
605
+ color: var(--text-muted);
606
+ margin: 14px 0 7px;
607
+ }
608
+ .guide-section-label:first-child { margin-top: 2px; }
609
+ .guide-desc { font-size: 12.5px; line-height: 1.55; color: var(--text); }
610
+ .guide-desc p { margin: 0 0 8px; }
611
+ .guide-desc p:last-child { margin-bottom: 0; }
612
+ .guide-desc code { font-family: var(--mono); background: var(--surface-2); padding: 1px 5px; border-radius: 3px; color: var(--text-muted); font-size: 11px; }
613
+ .guide-desc-empty { color: var(--text-muted); font-style: italic; }
614
+ .guide-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
615
+ .guide-step { display: flex; gap: 10px; align-items: baseline; }
616
+ .guide-box { font-family: var(--mono); font-size: 13px; min-width: 16px; color: var(--text-dim); line-height: 1.3; }
617
+ .guide-step-label { font-size: 12.5px; color: var(--text); }
618
+ .guide-step-note { font-size: 11px; color: var(--text-dim); font-style: italic; }
619
+ /* Done: ✓ in ok-green + struck/dimmed label, so the current step stands out on
620
+ shape + contrast, not colour alone. Current: → in accent + bold label. */
621
+ .guide-step.done .guide-box { color: var(--ok); }
622
+ .guide-step.done .guide-step-label { color: var(--text-dim); text-decoration: line-through; text-decoration-color: var(--text-dim); }
623
+ .guide-step.current .guide-box { color: var(--accent); }
624
+ .guide-step.current .guide-step-label { color: var(--text); font-weight: 600; }
625
+ .guide-walk-hint {
626
+ font-size: 11px;
627
+ line-height: 1.5;
628
+ color: var(--text-muted);
629
+ margin-top: 14px;
630
+ padding-top: 12px;
631
+ border-top: 1px solid var(--border);
632
+ }
633
+ .guide-walk-hint strong { color: var(--text); font-weight: 600; }
634
+ .modal.guide .modal-actions { margin-top: 16px; }
635
+ .modal.guide .modal-actions #guide-got-it { margin-right: auto; }
636
+
482
637
  .agent-card {
483
638
  background: var(--surface);
484
639
  border: 1px solid var(--border-strong);
package/dist/web/app.js CHANGED
@@ -131,7 +131,7 @@ function onboardingHtml() {
131
131
  step(3, 'Compile', '— freeze the approved lock') +
132
132
  step(4, 'Run', '— against the live host; results render here') +
133
133
  `</ol>` +
134
- `<div class="onb-foot">No workflows yet? Just ask your terminal AI to build one.</div>` +
134
+ `<div class="onb-foot">No workflows yet? <button class="onb-build-btn" type="button" aria-label="Build your first workflow — hands off to your terminal AI to build one from scratch">Build your first workflow →</button></div>` +
135
135
  `</div>`
136
136
  );
137
137
  }
@@ -162,6 +162,11 @@ function wireOnboardingCopy() {
162
162
  }
163
163
  };
164
164
  }
165
+ // Secondary first-run action: "Build your first workflow" — the empty canvas is the
166
+ // highest-intent moment to create one, so surface the same relay the ≡ menu's "New
167
+ // workflow…" fires (handleMenuAction is reassigned in aware.js → requestNewWorkflow).
168
+ const build = $topology.querySelector('.onb-build-btn');
169
+ if (build) build.onclick = () => handleMenuAction('new-workflow');
165
170
  }
166
171
 
167
172
  function nodeIds(p) {