@floless/app 0.42.0 → 0.44.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.
@@ -0,0 +1,86 @@
1
+ app: college-phase-exporter
2
+ version: 0.1.0
3
+ display-name: College — Phase Exporter
4
+ description: |
5
+ Reproduces the desktop "College - Phase Exporter" outcome on AWARE.
6
+ Given a phase number: Tekla BOM report -> SharePoint Excel -> Trimble Connect
7
+ upload -> Teams + email -> HTML summary. Composed against installed curated
8
+ commands only. Write-mode nodes carry the v0.11 safety contract.
9
+ exposes-as-agent: false
10
+ requires:
11
+ - tekla@0.1.x
12
+ - microsoft-365@0.1.x
13
+ - trimble-connect@0.1.x
14
+ - html-report@0.1.x
15
+ requires-permissions:
16
+ filesystem:
17
+ - write: '{{ inputs.report-dir }}'
18
+ network:
19
+ - https://graph.microsoft.com
20
+ - https://app.connect.trimble.com
21
+ layout: dag
22
+ nodes:
23
+ - id: report
24
+ agent: tekla
25
+ command: report-create
26
+ inputs:
27
+ template: '350 Advanced Bill'
28
+ scope: phase
29
+ phase: '{{ inputs.phase }}'
30
+ output-path: '{{ inputs.report-dir }}/BOM-Phase-{{ inputs.phase }}.xlsx'
31
+ - id: excel
32
+ agent: microsoft-365
33
+ command: excel.range.write
34
+ inputs:
35
+ workbook: '{{ inputs.sharepoint-workbook-url }}'
36
+ worksheet: 'Phase {{ inputs.phase }}'
37
+ range: 'A1:B2'
38
+ values:
39
+ - ['Phase', 'Rows']
40
+ - ['{{ inputs.phase }}', '{{ report.row-count }}']
41
+ safety: { transaction-group: phase-export, snapshot: false }
42
+ - id: upload
43
+ agent: trimble-connect
44
+ command: upload
45
+ inputs:
46
+ project-id: '{{ inputs.tc-project-id }}'
47
+ folder-id: '{{ inputs.tc-reports-folder-id }}'
48
+ file-path: '{{ report.path }}'
49
+ - id: summary
50
+ agent: html-report
51
+ command: render
52
+ inputs:
53
+ template: 'phase-export-summary'
54
+ data:
55
+ phase: '{{ inputs.phase }}'
56
+ report: '{{ report.path }}'
57
+ uploaded: '{{ upload.url }}'
58
+ output-path: '{{ inputs.report-dir }}/Phase-{{ inputs.phase }}-summary.html'
59
+ - id: teams
60
+ agent: microsoft-365
61
+ command: teams.channel.post-message
62
+ inputs:
63
+ team-id: '{{ inputs.teams-team-id }}'
64
+ channel-id: '{{ inputs.teams-channel-id }}'
65
+ message: 'BOM Phase {{ inputs.phase }} exported {{ run.date }}. Report uploaded to Trimble Connect.'
66
+ safety: { transaction-group: notify, snapshot: false }
67
+ - id: email
68
+ agent: microsoft-365
69
+ command: outlook.mail.send-with-attachment
70
+ inputs:
71
+ to: ['{{ inputs.notify-email }}']
72
+ subject: 'Done — Phase {{ inputs.phase }} BOM'
73
+ content-type: html
74
+ body: |
75
+ Phase {{ inputs.phase }} bill of materials is attached and uploaded to Trimble Connect.
76
+ attachments:
77
+ - path: '{{ report.path }}'
78
+ filename: 'BOM-Phase-{{ inputs.phase }}.xlsx'
79
+ safety: { transaction-group: notify, snapshot: false }
80
+ connections:
81
+ - { from: report, to: excel }
82
+ - { from: report, to: upload }
83
+ - { from: report, to: summary }
84
+ - { from: upload, to: summary }
85
+ - { from: summary, to: teams }
86
+ - { from: summary, to: email }
@@ -0,0 +1,104 @@
1
+ app: dummy-report
2
+ version: 0.1.0
3
+ display-name: Dummy Report
4
+ description: |
5
+ A self-contained test workflow that produces a styled HTML report — no Tekla
6
+ model required. Neither node touches `model`, so it runs on the host bridge with
7
+ nothing open. Use it to preview the in-app HTML Viewer and the "Made with
8
+ FloLess" report badge end to end without a real model. Two real tekla/exec nodes:
9
+ a title input → a report node that builds the HTML inline and is the terminal the
10
+ viewer renders.
11
+ exposes-as-agent: false
12
+ inputs:
13
+ title:
14
+ type: string
15
+ default: "FloLess Test Report"
16
+ description: Heading shown at the top of the generated report (editable per run).
17
+ requires:
18
+ - tekla@0.1.x
19
+ layout: linear
20
+ nodes:
21
+ # ── node 1: Title input — carries the report title downstream (no model deref) ──
22
+ - id: title-input
23
+ agent: tekla
24
+ command: exec
25
+ mode: read
26
+ config:
27
+ version: "2026.0"
28
+ args:
29
+ title: "{{ inputs.title }}"
30
+ code: |
31
+ // Title input — pure pass-through. Never touches `model`, so it runs even
32
+ // with no Tekla model open. Emits the title for the downstream report node.
33
+ string title = "FloLess Test Report";
34
+ if (args != null && args.TryGetValue("title", out var tv) && tv != null)
35
+ {
36
+ var s = tv.ToString();
37
+ if (!string.IsNullOrWhiteSpace(s)) title = s;
38
+ }
39
+ return new { ok = true, title };
40
+ # ── node 2: Report — build the styled HTML inline; be the terminal the viewer renders ──
41
+ - id: report
42
+ agent: tekla
43
+ command: exec
44
+ mode: read
45
+ config:
46
+ version: "2026.0"
47
+ args:
48
+ title: "{{ title-input.result.title }}"
49
+ code: |
50
+ // Dummy report — builds a styled HTML report inline and returns it as this
51
+ // terminal node's `html`, so the floless.app HTML Viewer renders it on
52
+ // double-click. Touches NO `model` (runs with no Tekla open) and writes no
53
+ // file. Pure string build, manual HTML-escape (no System.Net available).
54
+ using System;
55
+ using System.Text;
56
+
57
+ string title = "FloLess Test Report";
58
+ if (args != null && args.TryGetValue("title", out var tv) && tv != null)
59
+ {
60
+ var s = tv.ToString();
61
+ if (!string.IsNullOrWhiteSpace(s)) title = s;
62
+ }
63
+
64
+ Func<string, string> E = s => (s ?? "").Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
65
+ string now = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
66
+
67
+ var sb = new StringBuilder();
68
+ sb.Append("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\">");
69
+ sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
70
+ sb.Append("<title>" + E(title) + "</title><style>");
71
+ sb.Append("*{margin:0;padding:0;box-sizing:border-box}");
72
+ sb.Append("body{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;background:#f1f5f9;color:#0f172a;padding:2.5rem;line-height:1.6}");
73
+ sb.Append(".wrap{max-width:760px;margin:0 auto;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:2rem 2.25rem;box-shadow:0 10px 30px rgba(2,6,23,.06)}");
74
+ sb.Append(".hd{border-bottom:2px solid #4a9eff;padding-bottom:1rem;margin-bottom:1.5rem}");
75
+ sb.Append(".hd h1{font-size:1.6rem;font-weight:800;color:#0f172a}");
76
+ sb.Append(".hd p{color:#64748b;font-size:.85rem;margin-top:.3rem}");
77
+ sb.Append(".cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.85rem;margin-bottom:1.75rem}");
78
+ sb.Append(".card{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:.9rem 1rem}");
79
+ sb.Append(".card .l{font-size:.68rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8}");
80
+ sb.Append(".card .v{font-size:1.15rem;font-weight:700;color:#0f172a;margin-top:.15rem}");
81
+ sb.Append("table{width:100%;border-collapse:collapse;font-size:.9rem;margin-top:.25rem}");
82
+ sb.Append("th,td{text-align:left;padding:.6rem .75rem;border-bottom:1px solid #eef2f7}");
83
+ sb.Append("th{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8}");
84
+ sb.Append("td:first-child{font-weight:600;color:#334155;width:38%}");
85
+ sb.Append(".ft{margin-top:1.75rem;padding-top:1rem;border-top:1px solid #eef2f7;color:#94a3b8;font-size:.78rem;text-align:center}");
86
+ sb.Append("</style></head><body><div class=\"wrap\">");
87
+ sb.Append("<div class=\"hd\"><h1>" + E(title) + "</h1><p>A sample report generated by FloLess to preview the report viewer.</p></div>");
88
+ sb.Append("<div class=\"cards\">");
89
+ sb.Append("<div class=\"card\"><div class=\"l\">Status</div><div class=\"v\">OK</div></div>");
90
+ sb.Append("<div class=\"card\"><div class=\"l\">Generated</div><div class=\"v\">" + E(now) + "</div></div>");
91
+ sb.Append("<div class=\"card\"><div class=\"l\">Model needed</div><div class=\"v\">No</div></div>");
92
+ sb.Append("</div>");
93
+ sb.Append("<table><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody>");
94
+ sb.Append("<tr><td>Workflow</td><td>dummy-report</td></tr>");
95
+ sb.Append("<tr><td>Status</td><td>OK</td></tr>");
96
+ sb.Append("<tr><td>Source</td><td>Generated by FloLess (no model required)</td></tr>");
97
+ sb.Append("<tr><td>Generated</td><td>" + E(now) + "</td></tr>");
98
+ sb.Append("</tbody></table>");
99
+ sb.Append("<div class=\"ft\">This is a dummy report for testing the FloLess HTML viewer.</div>");
100
+ sb.Append("</div></body></html>");
101
+
102
+ return new { ok = true, title, html = sb.ToString() };
103
+ connections:
104
+ - { from: title-input, to: report }
@@ -0,0 +1,91 @@
1
+ app: hello-world
2
+ version: 0.2.0
3
+ display-name: AWARE Hello World
4
+ description: |
5
+ Smoke test: writes a one-line greeting to Tekla's prompt (status) bar — once in
6
+ Tekla 2025 and once in Tekla 2026 — to prove the AWARE runtime can drive a real
7
+ desktop host end to end. Two real tekla/exec nodes running read-only Roslyn C#
8
+ (Operation.DisplayPrompt; no model mutation, no filesystem write), so it runs on
9
+ any open model with no write permission.
10
+ exposes-as-agent: false
11
+ inputs:
12
+ message:
13
+ type: string
14
+ default: "Hello from floless.app — running end to end on AWARE."
15
+ description: The greeting shown on Tekla's prompt/status bar (editable per run).
16
+ requires:
17
+ - tekla@0.1.x
18
+ layout: dag
19
+ nodes:
20
+ - id: message-input
21
+ agent: tekla
22
+ command: exec
23
+ mode: read
24
+ config:
25
+ version: "2025.0"
26
+ args:
27
+ message: "{{ inputs.message }}"
28
+ code: |
29
+ // Message input — carries the greeting downstream to both status nodes.
30
+ // Pure pass-through: returns the input message; never touches the model,
31
+ // so it runs even with no Tekla open (no model deref). Downstream nodes
32
+ // read it via the message-input.result.message template.
33
+ string message = "Hello from floless.app — running end to end on AWARE.";
34
+ if (args != null && args.TryGetValue("message", out var mv) && mv != null)
35
+ {
36
+ var s = mv.ToString();
37
+ if (!string.IsNullOrWhiteSpace(s)) message = s;
38
+ }
39
+
40
+ return new { ok = true, message };
41
+ - id: tekla-2025-status
42
+ agent: tekla
43
+ command: exec
44
+ mode: read
45
+ config:
46
+ version: "2025.0"
47
+ args:
48
+ message: "{{ message-input.result.message }}"
49
+ code: |
50
+ // Hello World — write a one-line greeting to Tekla's prompt (status) bar.
51
+ // Read-only: a transient prompt; never touches the model or filesystem.
52
+ if (model == null)
53
+ return new { ok = false, error = "No live Tekla 2025 model — open one in Tekla Structures 2025." };
54
+
55
+ string message = "Hello from floless.app — running on AWARE (Tekla 2025).";
56
+ if (args != null && args.TryGetValue("message", out var mv) && mv != null)
57
+ {
58
+ var s = mv.ToString();
59
+ if (!string.IsNullOrWhiteSpace(s)) message = s;
60
+ }
61
+
62
+ Tekla.Structures.Model.Operations.Operation.DisplayPrompt(message);
63
+
64
+ return new { ok = true, host = "tekla", version = "2025.0", message };
65
+ - id: tekla-2026-status
66
+ agent: tekla
67
+ command: exec
68
+ mode: read
69
+ config:
70
+ version: "2026.0"
71
+ args:
72
+ message: "{{ message-input.result.message }}"
73
+ code: |
74
+ // Hello World — write a one-line greeting to Tekla's prompt (status) bar.
75
+ // Read-only: a transient prompt; never touches the model or filesystem.
76
+ if (model == null)
77
+ return new { ok = false, error = "No live Tekla 2026 model — open one in Tekla Structures 2026." };
78
+
79
+ string message = "Hello from floless.app — running on AWARE (Tekla 2026).";
80
+ if (args != null && args.TryGetValue("message", out var mv) && mv != null)
81
+ {
82
+ var s = mv.ToString();
83
+ if (!string.IsNullOrWhiteSpace(s)) message = s;
84
+ }
85
+
86
+ Tekla.Structures.Model.Operations.Operation.DisplayPrompt(message);
87
+
88
+ return new { ok = true, host = "tekla", version = "2026.0", message };
89
+ connections:
90
+ - { from: message-input, to: tekla-2025-status }
91
+ - { from: message-input, to: tekla-2026-status }
@@ -0,0 +1,56 @@
1
+ app: screenshot-to-saved-settings
2
+ version: 0.1.0
3
+ display-name: Screenshot to Saved Settings
4
+ description: |
5
+ Reproduces the desktop "screenshot-to-saved-settings" outcome on AWARE,
6
+ the decalog-#9 way (app-spec "Reading a drawing / PDF / image"):
7
+
8
+ The terminal AI reads a steel connection-schedule screenshot at COMPOSE time
9
+ and bakes the parsed rows into config.yaml as literals. The run is then fully
10
+ deterministic — for-each row writes a Tekla saved-attribute (.j) file via the
11
+ curated save-attributes verb. No runtime think-node reads the drawing.
12
+
13
+ (Optionally gate the writes behind an approve: block so a human eyeballs the
14
+ extraction first — app-spec L325. Omitted here: approve runtime lands in
15
+ aware v0.19.x; the compose-time-extraction + deterministic-write core is the
16
+ point.)
17
+ exposes-as-agent: false
18
+ inputs:
19
+ # The source drawing is BAKED at compose time (its rows live in config.schedule-rows),
20
+ # so this is not a runtime input — declaring it with read-strategy: bake surfaces the
21
+ # "Re-read & re-bake ▸" affordance (B3) so the user can swap the drawing and have the
22
+ # host AI re-extract + re-bake. The deterministic run never reads it.
23
+ drawing:
24
+ type: image
25
+ widget: file
26
+ read-strategy: bake
27
+ description: The connection-schedule screenshot baked into config — swap it to re-read & re-bake.
28
+ requires:
29
+ - tekla@0.1.x
30
+ - html-report@0.1.x
31
+ layout: linear
32
+ nodes:
33
+ # Deterministic write: one .j attribute file per compose-time-extracted row.
34
+ - id: write-attributes
35
+ for-each: '{{ config.schedule-rows }}'
36
+ do:
37
+ - id: save
38
+ agent: tekla
39
+ command: save-attributes
40
+ config:
41
+ joint-id: '{{ item.joint-id }}'
42
+ filename: '{{ item.preset }}'
43
+ safety:
44
+ transaction-group: schedule-import
45
+ snapshot: true
46
+
47
+ - id: summary
48
+ agent: html-report
49
+ command: render
50
+ config:
51
+ template: 'schedule-import-summary'
52
+ data:
53
+ presets: '{{ config.schedule-rows }}'
54
+ output-path: '{{ inputs.report-dir }}/schedule-import.html'
55
+ connections:
56
+ - { from: write-attributes, to: summary }
@@ -0,0 +1,73 @@
1
+ app: steel-from-drawings
2
+ version: 0.1.0
3
+ display-name: Steel from Drawings
4
+ description: |
5
+ Turn a structural steel drawing into an interactive, licence-free 3D model — no Tekla, no
6
+ modelling, no AI API key. Your terminal AI (Claude Code / Codex) reads the drawing once and
7
+ bakes a schematic 3D scene into this app; the viewer-3d agent renders it as an interactive
8
+ document anyone can orbit, click to inspect, and take off (counts + length by section) — so an
9
+ estimator, PM, fabricator or client can understand and price the steel before opening a seat.
10
+
11
+ The reading is a COMPOSE-TIME act — the FloLess way: the app is the prompt in your terminal, so
12
+ the brain is your AI terminal, never this server, and there is never a model in the run path.
13
+ Attach a drawing and use "Re-read & re-bake ▸" (or just ask your terminal AI to read it). The AI
14
+ reads the grids, members and sizes and bakes them straight into the `view` node's `scene` below
15
+ (the viewer-3d scene schema), recompiles, and asks you to approve; the deterministic run then
16
+ only renders the approved scene. Ships with a small example scene so it renders out of the box —
17
+ the `floless-app-steel-from-drawings` skill teaches your terminal AI how to read your own drawing in.
18
+ exposes-as-agent: false
19
+ inputs:
20
+ # BAKED at compose time: the terminal AI reads this drawing and writes the `view.scene` block
21
+ # below (the run never reads the drawing). read-strategy: bake surfaces "Re-read & re-bake ▸" so
22
+ # you can swap the drawing and have your AI re-read it. No runtime input node.
23
+ drawing:
24
+ type: image
25
+ widget: file
26
+ accept: [pdf, png, jpg]
27
+ read-strategy: bake
28
+ description: A structural steel drawing (framing plan / GA / elevation) — baked into the scene below; swap it to re-read & re-bake.
29
+ requires:
30
+ - viewer-3d@0.1.x
31
+ layout: linear
32
+ nodes:
33
+ # The viewer-3d render node IS the terminal. Ships with a small EXAMPLE scene (a sample 3x2 bay
34
+ # frame, schematic) so the app renders out of the box. Re-bake — or the
35
+ # floless-app-steel-from-drawings skill — replaces this whole `scene` with a reading of YOUR drawing.
36
+ - id: view
37
+ agent: viewer-3d
38
+ command: render
39
+ config:
40
+ scene:
41
+ meta: { name: "Example — steel framing (sample, not your drawing)", units: mm, up: z }
42
+ groups:
43
+ - { key: column, label: "Columns (HSS6x6x3/8)", color: "#60a5fa" }
44
+ - { key: beam, label: "Beams (W10x33)", color: "#94a3b8" }
45
+ elements:
46
+ # 6 columns (z 0 -> 4500) on a 3 x 2 grid
47
+ - { id: COL-1A, group: column, kind: box, from: [0,0,0], to: [0,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "1-A" } }
48
+ - { id: COL-2A, group: column, kind: box, from: [6000,0,0], to: [6000,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "2-A" } }
49
+ - { id: COL-3A, group: column, kind: box, from: [12000,0,0], to: [12000,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "3-A" } }
50
+ - { id: COL-1B, group: column, kind: box, from: [0,6000,0], to: [0,6000,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "1-B" } }
51
+ - { id: COL-2B, group: column, kind: box, from: [6000,6000,0], to: [6000,6000,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "2-B" } }
52
+ - { id: COL-3B, group: column, kind: box, from: [12000,6000,0],to: [12000,6000,4500],section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "3-B" } }
53
+ # roof beams at z 4500 (W10x33): along each row, then tie beams along each grid line
54
+ - { id: BM-12A, group: beam, kind: box, from: [0,0,4500], to: [6000,0,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "A" } }
55
+ - { id: BM-23A, group: beam, kind: box, from: [6000,0,4500], to: [12000,0,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "A" } }
56
+ - { id: BM-12B, group: beam, kind: box, from: [0,6000,4500], to: [6000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "B" } }
57
+ - { id: BM-23B, group: beam, kind: box, from: [6000,6000,4500],to: [12000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "B" } }
58
+ - { id: BM-1AB, group: beam, kind: box, from: [0,0,4500], to: [0,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "1" } }
59
+ - { id: BM-2AB, group: beam, kind: box, from: [6000,0,4500], to: [6000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "2" } }
60
+ - { id: BM-3AB, group: beam, kind: box, from: [12000,0,4500], to: [12000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "3" } }
61
+ grids:
62
+ - { label: "1", at: [0,-3000,0] }
63
+ - { label: "2", at: [6000,-3000,0] }
64
+ - { label: "3", at: [12000,-3000,0] }
65
+ - { label: "A", at: [-3000,0,0] }
66
+ - { label: "B", at: [-3000,6000,0] }
67
+ panels:
68
+ - title: Takeoff
69
+ note: "EXAMPLE — not your drawing. A sample steel frame on a 1-3 x A-B grid; sizes & heights are illustrative. Use 'Re-read & re-bake ▸' (or ask your terminal AI) to read your own drawing."
70
+ columns: [Section, "No.", Length]
71
+ rows:
72
+ - ["HSS6x6x3/8", 6, "27.0 m"]
73
+ - ["W10x33", 7, "42.0 m"]
@@ -0,0 +1,151 @@
1
+ app: steel-takeoff
2
+ version: 0.2.0
3
+ display-name: Steel Takeoff
4
+ publisher: floless
5
+ module: steel-detailer
6
+ description: |
7
+ Turn a structural steel drawing into a verifiable takeoff — overlay + properties + a confidence
8
+ report you can judge — then an interactive 3D model (bake to IFC/Tekla) and a formatted BOM, all
9
+ in one workflow. The reading is a COMPOSE-TIME act (the FloLess way): your terminal AI reads the
10
+ drawing and bakes the takeoff; the deterministic run only renders the approved result. Attach a
11
+ drawing and use "Re-read & re-bake ▸" (or ask your terminal AI). Double-click the takeoff node to
12
+ open the editor; Approve bakes the lock; freeze a stage once it's right so Run never recomputes it.
13
+ exposes-as-agent: false
14
+ # Workflow changelog (FloLess-published). Newest first; the app shows entries newer than the
15
+ # installed version when offering an update. Plain-English — it surfaces to the user.
16
+ changelog:
17
+ - version: 0.2.0
18
+ summary: Filter your drawing down to the steel first
19
+ changes:
20
+ - "Added: a Filter step at the start that keeps only the real steel lines of your drawing — by layer, line weight, or colour, or by clicking an example — so the takeoff reads a clean drawing."
21
+ - version: 0.1.0
22
+ summary: Initial steel takeoff
23
+ inputs:
24
+ drawing:
25
+ type: image
26
+ widget: file
27
+ accept: [pdf, png, jpg]
28
+ read-strategy: bake
29
+ description: A structural steel drawing — read by your terminal AI into the takeoff; swap to re-read & re-bake.
30
+ target_confidence:
31
+ type: number
32
+ default: 70
33
+ description: Minimum aggregate confidence the AI read aims for before handing you the editor (%).
34
+ requires:
35
+ - html-report@0.2.x
36
+ - viewer-3d@0.1.x
37
+ layout: linear
38
+ nodes:
39
+ # ── node 1: filter — isolate the steel linework (FIRST stage) ─────────────────
40
+ # The pre-stage. Your terminal AI reads the drawing once (PyMuPDF, deterministic —
41
+ # no LLM) and bakes every element's layer / thickness / colour into the contract's
42
+ # `filter` block. Double-click this node to open the filter view: keep only the real
43
+ # steel lines — by facet (layer / thickness / colour) or the eyedropper (click a line
44
+ # or label → Add its layer/thickness/colour) — then Save. That saved selection is what
45
+ # the takeoff reader binds profile labels to (it replaces the length heuristic; falls
46
+ # back to it when no filter is saved). Ships with a tiny example summary so it renders
47
+ # out of the box; "Re-read & re-bake ▸" replaces it. The run renders a kept-layers
48
+ # summary (a baked read-only overlay is a later refinement).
49
+ - id: filter
50
+ agent: html-report
51
+ command: render
52
+ config:
53
+ surface: steel-filter
54
+ data:
55
+ - { Layer: "S-ANNO-BEAM-STEL", Elements: 374, Kept: "yes (steel)" }
56
+ - { Layer: "S-ANNO-GRID", Elements: 319, Kept: "no" }
57
+ - { Layer: "S-ANNO-DIMS", Elements: 575, Kept: "no" }
58
+
59
+ # ── node 2: read — takeoff summary ───────────────────────────────────────────
60
+ # config.contract is the FloLess editor discriminator (renderers.js contractTypeOf).
61
+ # config.takeoff is the SINGLE SOURCE the editor + Approve-bake + render all key on:
62
+ # server/contract-bake.ts writes the approved contract under config.takeoff (finding
63
+ # this node by config.contract === type), and GET /api/contract falls back to it so
64
+ # the editor opens with the baked contract. It is BAKED at compose time by the terminal
65
+ # AI from the drawing above; the run re-renders it as an HTML summary. Ships with a tiny
66
+ # EXAMPLE so it renders out of the box — "Re-read & re-bake ▸" replaces config.takeoff.
67
+ - id: read
68
+ agent: html-report
69
+ command: render
70
+ config:
71
+ contract: steel.takeoff/v1
72
+ takeoff:
73
+ type: steel.takeoff/v1
74
+ source: { name: "Example — sample framing (not a real drawing)" }
75
+ weights:
76
+ W10X33: 49.1
77
+ HSS6X6X3/8: 27.48
78
+ plans:
79
+ - sheet: EX-1
80
+ title: Example Framing Plan
81
+ members:
82
+ - { id: m1, profile: W10X33, role: beam, wp: [[0,0],[6000,0]] }
83
+ - { id: m2, profile: HSS6X6X3/8, role: column, wp: [[0,0],[0,4500]] }
84
+ template: steel-takeoff-summary
85
+ # NOTE: html-report does NOT template a sibling config key — `data: '{{ config.takeoff }}'`
86
+ # collapses to one opaque item (verified: subtitle "1 item", no member data). So we mirror
87
+ # config.takeoff inline here for the render. config.takeoff above remains the canonical
88
+ # source (editor + Approve-bake + GET /api/contract); a later task can wire the render
89
+ # to it once html-report (or the BOM formatter) consumes the structured contract.
90
+ data:
91
+ type: steel.takeoff/v1
92
+ source: { name: "Example — sample framing (not a real drawing)" }
93
+ weights:
94
+ W10X33: 49.1
95
+ HSS6X6X3/8: 27.48
96
+ plans:
97
+ - sheet: EX-1
98
+ title: Example Framing Plan
99
+ members:
100
+ - { id: m1, profile: W10X33, role: beam, wp: [[0,0],[6000,0]] }
101
+ - { id: m2, profile: HSS6X6X3/8, role: column, wp: [[0,0],[0,4500]] }
102
+
103
+ # ── node 3: view — interactive 3D model ──────────────────────────────────────
104
+ # scene is BAKED at compose time (matches config.takeoff above).
105
+ # Ships with a minimal EXAMPLE scene matching the takeoff above so it renders
106
+ # out of the box. The terminal AI replaces the whole scene when re-baking.
107
+ - id: view
108
+ agent: viewer-3d
109
+ command: render
110
+ config:
111
+ scene:
112
+ meta: { name: "Example — sample framing (not a real drawing)", units: mm, up: z }
113
+ groups:
114
+ - { key: column, label: "Columns (HSS6x6x3/8)", color: "#60a5fa" }
115
+ - { key: beam, label: "Beams (W10x33)", color: "#94a3b8" }
116
+ elements:
117
+ - { id: m1, group: beam, kind: box, from: [0,0,0], to: [6000,0,0], section: { w: 150, d: 250 }, meta: { profile: "W10X33", sheet: "EX-1" } }
118
+ - { id: m2, group: column, kind: box, from: [0,0,0], to: [0,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6X6X3/8", sheet: "EX-1" } }
119
+ panels:
120
+ - title: Takeoff
121
+ note: "EXAMPLE — not your drawing. Swap the drawing input and ask your terminal AI to re-read & re-bake."
122
+ columns: [Section, "No.", "Length / Height"]
123
+ rows:
124
+ - ["W10X33", 1, "6.0 m"]
125
+ - ["HSS6X6X3/8", 1, "4.5 m"]
126
+
127
+ # ── node 4: bom — bill of materials ──────────────────────────────────────────
128
+ # Stub: renders the same takeoff data as a formatted BOM table.
129
+ # Replace with a real BOM formatter skill (later task).
130
+ - id: bom
131
+ agent: html-report
132
+ command: render
133
+ config:
134
+ template: steel-takeoff-bom
135
+ data:
136
+ type: steel.takeoff/v1
137
+ source: { name: "Example — sample framing (not a real drawing)" }
138
+ weights:
139
+ W10X33: 49.1
140
+ HSS6X6X3/8: 27.48
141
+ plans:
142
+ - sheet: EX-1
143
+ title: Example Framing Plan
144
+ members:
145
+ - { id: m1, profile: W10X33, role: beam, wp: [[0,0],[6000,0]] }
146
+ - { id: m2, profile: HSS6X6X3/8, role: column, wp: [[0,0],[0,4500]] }
147
+
148
+ connections:
149
+ - { from: filter, to: read }
150
+ - { from: read, to: view }
151
+ - { from: view, to: bom }