stream_weaver 0.3.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.
- checksums.yaml +7 -0
- data/.beads/.gitignore +77 -0
- data/.beads/README.md +81 -0
- data/.beads/config.yaml +54 -0
- data/.beads/metadata.json +7 -0
- data/.rspec +3 -0
- data/AGENTS.md +91 -0
- data/CHANGELOG.md +294 -0
- data/CLAUDE.md +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +565 -0
- data/Rakefile +13 -0
- data/assets/streamweaver-hero.jpg +0 -0
- data/bench/baselines/ledger.rb +58 -0
- data/bench/baselines/warroom.rb +51 -0
- data/bench/fixtures/ledger.rb +76 -0
- data/bench/fixtures/warroom.rb +60 -0
- data/bench/results/6daf8b8.md +102 -0
- data/bench/results/c8d749b.md +102 -0
- data/bench/results/eb06ab9.md +102 -0
- data/bench/run.rb +151 -0
- data/bench/support.rb +84 -0
- data/copy_this/dto_run_sample_run_7631.html +220 -0
- data/docs/SERVICE_MODE.md +302 -0
- data/docs/architecture/how_streamweaver_works.md +772 -0
- data/docs/blog/2026-01-16-streamweaver-introduction.md +515 -0
- data/docs/bug-2026-08-31-canvas-bridge-socket-collision.md +83 -0
- data/docs/canvas-ipc-session-summary.md +127 -0
- data/docs/canvas-panel-workflow.md +172 -0
- data/docs/canvas-read.md +147 -0
- data/docs/canvas-roadmap.md +275 -0
- data/docs/case-studies/2026-09-03-didx-canvas-read-shelf.md +147 -0
- data/docs/claude-code-companion-skill-spec.md +567 -0
- data/docs/components_reference.md +1223 -0
- data/docs/crud-patterns.md +265 -0
- data/docs/endpoints.md +122 -0
- data/docs/for_llms.md +1 -0
- data/docs/form-patterns.md +165 -0
- data/docs/frontend-only.md +169 -0
- data/docs/html-artifact-audit.md +238 -0
- data/docs/ideas/2025-01-01-as-a-service.md +203 -0
- data/docs/ideas/2026-01-16-charm-tui-exploration.md +542 -0
- data/docs/ideas/2026-08-27-static-doc-shelf-export.md +153 -0
- data/docs/ideas/dhh-review-future-refactors.md +142 -0
- data/docs/opal-jamstack.md +47 -0
- data/docs/opal-npm-direction.md +184 -0
- data/docs/opal-spike-findings.md +194 -0
- data/docs/plans/2025-12-16-form-blocks-design.md +124 -0
- data/docs/plans/2025-12-30-examples-browser-design.md +152 -0
- data/docs/plans/2026-01-02-charts-design.md +288 -0
- data/docs/plans/2026-01-10-canvas-ipc-design.md +247 -0
- data/docs/plans/2026-01-19-cabinet-control-components.md +176 -0
- data/docs/plans/2026-01-26-gem-release-and-panel.md +140 -0
- data/docs/plans/canvas-claude-project-design.md +238 -0
- data/docs/plans/canvas-doc-location-and-discovery.md +32 -0
- data/docs/plans/org-doc-preview-surfaces.md +293 -0
- data/docs/plans/shadcn-polish-plan.md +417 -0
- data/docs/porting-artifacts.md +252 -0
- data/docs/reference/agent-skills-comparison.md +369 -0
- data/docs/reference/travel-state-prd.artifact.html +780 -0
- data/docs/research/2026-08-17-hotwire-alike-landscape.md +268 -0
- data/docs/research/2026-08-17-hotwire-concept-map.md +404 -0
- data/docs/research/2026-08-22-lazy-fragments-trigger-decision.md +246 -0
- data/docs/research/2026-08-22-learnhotwire-syllabus-coverage.md +239 -0
- data/docs/research/frontend-only-matrix.md +278 -0
- data/docs/research/streamweaver-way-spike-findings.md +529 -0
- data/docs/resource-dsl.md +342 -0
- data/docs/routing.md +302 -0
- data/docs/ruby-ui-comparison.md +319 -0
- data/docs/shared-dsl-fragments.md +356 -0
- data/docs/streamweaver-for-ai-agents.md +401 -0
- data/docs/streamweaver-frontend-vision.md +123 -0
- data/docs/streamweaver_canvas/glimmer_initial_final_layer.rb +77 -0
- data/docs/streamweaver_canvas/save_example.rb +9 -0
- data/docs/templates.md +213 -0
- data/docs/testing.md +155 -0
- data/docs/theming-hooks.md +281 -0
- data/docs/tutorials/the-streamweaver-way.md +882 -0
- data/docs/university/capability-inventory.md +137 -0
- data/docs/university/dependency-survey.md +114 -0
- data/docs/university/design-spec.md +217 -0
- data/docs/university/mockups/code-block-doc-theme-dark.png +0 -0
- data/docs/university/mockups/code-block-doc-theme-light.png +0 -0
- data/docs/university/mockups/course-list-dark.png +0 -0
- data/docs/university/mockups/course-list-light.png +0 -0
- data/docs/university/mockups/course_canvas_mockup.rb +741 -0
- data/docs/university/mockups/step-screen-dark.png +0 -0
- data/docs/university/mockups/step-screen-light.png +0 -0
- data/docs/university/roadmap.md +72 -0
- data/docs/university/send-to-coworker.md +77 -0
- data/docs/university/worker-session-mining.md +109 -0
- data/docs/visual/browser-rendering-spike.md +173 -0
- data/docs/visual/builder-visual-plan-analysis.md +378 -0
- data/docs/visual/sw-plan-format-exploration.md +511 -0
- data/docs/visual/sw-plan-rendering-deep-dive.md +453 -0
- data/docs/visual-skills/PROGRESS.md +60 -0
- data/docs/visual-skills/SESSION-CONTEXT.md +207 -0
- data/docs/visual-skills/analysis/components.md +317 -0
- data/docs/visual-skills/analysis/overlap.md +360 -0
- data/docs/visual-skills/analysis/pi-design-deck.md +1102 -0
- data/docs/visual-skills/analysis/streamweaver-inventory.md +93 -0
- data/docs/visual-skills/analysis/unified-specs.feature +1003 -0
- data/docs/visual-skills/analysis/visual-explainer.md +964 -0
- data/docs/visual-skills/blog/blog-series-outline.md +39 -0
- data/docs/visual-skills/blog/token-efficiency.md +73 -0
- data/docs/visual-skills/design/architecture.md +1725 -0
- data/docs/visual-skills/design/codex-review.md +340 -0
- data/docs/visual-skills/design/dhh-review.md +209 -0
- data/docs/visual-skills/design/evolution.md +100 -0
- data/docs/visual-skills/design/gemini-review.md +55 -0
- data/docs/visual-skills/design/review-synthesis.md +149 -0
- data/docs/visual-skills/implementation/STATE.md +22 -0
- data/docs/visual-skills/implementation/plan.md +347 -0
- data/docs/visual-skills/implementation/spike-findings.md +237 -0
- data/docs/visual-skills/implementation/tasks.md +702 -0
- data/docs/visual-skills/lessons-learned/process.md +145 -0
- data/examples/README.md +96 -0
- data/examples/advanced/all_components.rb +177 -0
- data/examples/advanced/examples_browser.rb +394 -0
- data/examples/advanced/teachables_browser.rb +261 -0
- data/examples/advanced/theme_tweaker.rb +366 -0
- data/examples/advanced/tutorial.rb +1528 -0
- data/examples/agentic/agentic_form.rb +30 -0
- data/examples/agentic/agentic_form_autoclose.rb +30 -0
- data/examples/agentic/cultivation_tracker.rb +69 -0
- data/examples/basic/hello_world.rb +22 -0
- data/examples/basic/opal_tabs_table.rb +44 -0
- data/examples/basic/todo_list.rb +39 -0
- data/examples/button_loading_test.rb +36 -0
- data/examples/canvas/mermaid_canvas_demo.sh +151 -0
- data/examples/charts/bar_chart_demo.rb +57 -0
- data/examples/charts/line_chart_demo.rb +73 -0
- data/examples/charts/pie_area_demo.rb +87 -0
- data/examples/charts/stacked_bar_chart_demo.rb +69 -0
- data/examples/claude_code/README.md +85 -0
- data/examples/claude_code/codebreaker/.claude/commands/infiltrate.md +632 -0
- data/examples/claude_code/codebreaker/.claude/settings.json +11 -0
- data/examples/claude_code/codebreaker/.claude/settings.local.json +17 -0
- data/examples/claude_code/codebreaker/.claude/skills/infiltrate.md +0 -0
- data/examples/claude_code/codebreaker/README.md +213 -0
- data/examples/claude_code/tutorial/.claude/commands/learn.md +453 -0
- data/examples/claude_code/tutorial/README.md +102 -0
- data/examples/claude_code/verification_flow/.claude/commands/verify.md +171 -0
- data/examples/claude_code/verification_flow/README.md +126 -0
- data/examples/components/annotated_code_demo.rb +54 -0
- data/examples/components/callout_demo.rb +33 -0
- data/examples/components/checkbox_group_demo.rb +46 -0
- data/examples/components/design_review.css +468 -0
- data/examples/components/design_review_demo.rb +44 -0
- data/examples/components/design_review_dsl.rb +300 -0
- data/examples/components/diff_block_demo.rb +112 -0
- data/examples/components/events_demo.rb +184 -0
- data/examples/components/form_demo.rb +62 -0
- data/examples/components/lesson_demo.rb +53 -0
- data/examples/components/markdown_demo.rb +110 -0
- data/examples/components/mermaid_demo.rb +166 -0
- data/examples/components/pareto_set.rb +64 -0
- data/examples/components/prd_demo.rb +21 -0
- data/examples/components/prd_dsl.rb +385 -0
- data/examples/components/quiz_demo.rb +47 -0
- data/examples/components/run_viewer_demo.rb +100 -0
- data/examples/components/score_and_collapsible_demo.rb +82 -0
- data/examples/components/select_stale_value_smoke_test.rb +39 -0
- data/examples/components/table_demo.rb +247 -0
- data/examples/components/timer_with_state_demo.rb +58 -0
- data/examples/components/todo_due_dates.rb +63 -0
- data/examples/components/uat_gaps_demo.rb +112 -0
- data/examples/dashboard/feed_simulator.rb +60 -0
- data/examples/dashboard/live_dashboard.rb +80 -0
- data/examples/dashboard_components.rb +108 -0
- data/examples/deferred_fragments_demo.rb +62 -0
- data/examples/generate_more_spike/README.md +63 -0
- data/examples/generate_more_spike/app.rb +441 -0
- data/examples/git_health.sh +401 -0
- data/examples/layout/layout_components_demo.rb +174 -0
- data/examples/layout/modal_demo.rb +215 -0
- data/examples/layout/navigation_demo.rb +227 -0
- data/examples/layout/route_tabs_demo.rb +52 -0
- data/examples/layout/routing_demo.rb +52 -0
- data/examples/layout/scroll_box_demo.rb +154 -0
- data/examples/lazy_fragments_demo.rb +151 -0
- data/examples/my_todos/README.md +29 -0
- data/examples/my_todos/my_todos.rb +312 -0
- data/examples/my_todos/store.rb +86 -0
- data/examples/opal/reactive_demo.rb +62 -0
- data/examples/opal/scenarios/s1_counter.rb +15 -0
- data/examples/opal/scenarios/s2_search_filter.rb +17 -0
- data/examples/opal/scenarios/s3_sibling_tabs.rb +25 -0
- data/examples/opal/scenarios/s4_shopping_cart.rb +34 -0
- data/examples/opal/scenarios/s5_watch.rb +39 -0
- data/examples/opal/scenarios/s6_on_start.rb +20 -0
- data/examples/opal/scenarios/s7_wizard.rb +33 -0
- data/examples/opal/scenarios/s8_loan_calculator.rb +21 -0
- data/examples/opal/scenarios/s9_dashboard.rb +27 -0
- data/examples/operations_dashboard_demo.rb +167 -0
- data/examples/panel_demo.sh +447 -0
- data/examples/parity/assets/tyrion_slice.css +86 -0
- data/examples/parity/rivet_people_slice.rb +182 -0
- data/examples/parity/tyrion_components.css +777 -0
- data/examples/parity/tyrion_warroom_components.rb +414 -0
- data/examples/parity/tyrion_warroom_slice.rb +549 -0
- data/examples/puma_dev/README.md +124 -0
- data/examples/puma_dev/config.ru +34 -0
- data/examples/puma_dev/standalone_app.rb +34 -0
- data/examples/scaffolding/blog.rb +49 -0
- data/examples/scaffolding/utf_lite.rb +105 -0
- data/examples/styling/feedback_demo.rb +229 -0
- data/examples/styling/style_showcase.rb +172 -0
- data/examples/styling/theme_demo.rb +508 -0
- data/examples/timer_health_checker.rb +111 -0
- data/examples/timer_showcase.rb +131 -0
- data/examples/tutorials/render_markdown.rb +112 -0
- data/examples/tutorials/streamweaver_way_tutorial.rb +44 -0
- data/examples/tutorials/tutorial_content.rb +1082 -0
- data/examples/visual_skills/design_deck_demo.rb +314 -0
- data/examples/visual_skills/explainer_demo.rb +499 -0
- data/exe/streamweaver +9 -0
- data/gsd/ROADMAP-1.0.md +167 -0
- data/gsd/STATE.md +13 -0
- data/gsd/research/market-positioning-research.md +139 -0
- data/gsd/research/production-patterns-research.md +270 -0
- data/gsd/research/repo-audit-1.0.md +274 -0
- data/lib/stream_weaver/action_token.rb +58 -0
- data/lib/stream_weaver/adapter/alpinejs.rb +8249 -0
- data/lib/stream_weaver/adapter/base.rb +591 -0
- data/lib/stream_weaver/adapter/opal.rb +334 -0
- data/lib/stream_weaver/adapter/static.rb +1118 -0
- data/lib/stream_weaver/admin.rb +176 -0
- data/lib/stream_weaver/app.rb +1821 -0
- data/lib/stream_weaver/assets/js/sw-copy.js +50 -0
- data/lib/stream_weaver/assets/js/sw-heredoc-rewrite.js +68 -0
- data/lib/stream_weaver/assets/js/sw-keyboard.js +165 -0
- data/lib/stream_weaver/assets/js/sw-mermaid-zoom.js +621 -0
- data/lib/stream_weaver/assets/js/sw-route-tabs.js +53 -0
- data/lib/stream_weaver/assets/js/sw-sidebar-toc.js +116 -0
- data/lib/stream_weaver/assets/js/sw-slide-nav.js +103 -0
- data/lib/stream_weaver/canvas/bridge.rb +239 -0
- data/lib/stream_weaver/canvas/bridge_server.rb +646 -0
- data/lib/stream_weaver/canvas/client.rb +298 -0
- data/lib/stream_weaver/canvas/doc_roots.rb +233 -0
- data/lib/stream_weaver/canvas/doc_store.rb +252 -0
- data/lib/stream_weaver/canvas/gist_publisher.rb +264 -0
- data/lib/stream_weaver/canvas/gist_save_handler.rb +89 -0
- data/lib/stream_weaver/canvas/gist_store.rb +135 -0
- data/lib/stream_weaver/canvas/helpers.rb +109 -0
- data/lib/stream_weaver/canvas/history.rb +90 -0
- data/lib/stream_weaver/canvas/protocol.rb +86 -0
- data/lib/stream_weaver/canvas/reader.rb +853 -0
- data/lib/stream_weaver/canvas/save_doc_widget.rb +457 -0
- data/lib/stream_weaver/canvas/scroll_top_hint.rb +21 -0
- data/lib/stream_weaver/canvas/session.rb +132 -0
- data/lib/stream_weaver/cli.rb +3235 -0
- data/lib/stream_weaver/component_assets.rb +70 -0
- data/lib/stream_weaver/component_registry.rb +67 -0
- data/lib/stream_weaver/component_renderer.rb +48 -0
- data/lib/stream_weaver/components/annotated_code.rb +53 -0
- data/lib/stream_weaver/components/api_endpoint.rb +42 -0
- data/lib/stream_weaver/components/callout.rb +59 -0
- data/lib/stream_weaver/components/chart.rb +84 -0
- data/lib/stream_weaver/components/code_block.rb +77 -0
- data/lib/stream_weaver/components/comparison.rb +39 -0
- data/lib/stream_weaver/components/decision.rb +38 -0
- data/lib/stream_weaver/components/deck/close_overlay.rb +84 -0
- data/lib/stream_weaver/components/deck/confirmation_bar.rb +60 -0
- data/lib/stream_weaver/components/deck/deck_option.rb +61 -0
- data/lib/stream_weaver/components/deck/deck_slide.rb +77 -0
- data/lib/stream_weaver/components/deck/deck_state.rb +469 -0
- data/lib/stream_weaver/components/deck/deck_summary.rb +73 -0
- data/lib/stream_weaver/components/deck/design_deck.rb +60 -0
- data/lib/stream_weaver/components/deck/generate_more_controls.rb +84 -0
- data/lib/stream_weaver/components/deck/model_selector.rb +84 -0
- data/lib/stream_weaver/components/deck/skeleton_placeholder.rb +36 -0
- data/lib/stream_weaver/components/diff_block.rb +125 -0
- data/lib/stream_weaver/components/doc_header.rb +57 -0
- data/lib/stream_weaver/components/image_block.rb +70 -0
- data/lib/stream_weaver/components/implementation_map.rb +33 -0
- data/lib/stream_weaver/components/keyboard_shortcuts.rb +94 -0
- data/lib/stream_weaver/components/kpi_dashboard.rb +78 -0
- data/lib/stream_weaver/components/mermaid.rb +79 -0
- data/lib/stream_weaver/components/pipeline.rb +63 -0
- data/lib/stream_weaver/components/sidebar_toc.rb +46 -0
- data/lib/stream_weaver/components/slide_container.rb +141 -0
- data/lib/stream_weaver/components/timeline_event.rb +58 -0
- data/lib/stream_weaver/components/wireframe.rb +29 -0
- data/lib/stream_weaver/components/wireframe_block.rb +34 -0
- data/lib/stream_weaver/components.rb +2711 -0
- data/lib/stream_weaver/css.rb +230 -0
- data/lib/stream_weaver/dev_fallback_overlay.rb +61 -0
- data/lib/stream_weaver/display_dsl.rb +1004 -0
- data/lib/stream_weaver/export/html_exporter.rb +478 -0
- data/lib/stream_weaver/feed.rb +34 -0
- data/lib/stream_weaver/feed_builder.rb +31 -0
- data/lib/stream_weaver/fonts.rb +33 -0
- data/lib/stream_weaver/interaction_runner.rb +487 -0
- data/lib/stream_weaver/iterm.rb +460 -0
- data/lib/stream_weaver/layout_registry.rb +92 -0
- data/lib/stream_weaver/opal/bridge.rb +52 -0
- data/lib/stream_weaver/opal/builder.rb +193 -0
- data/lib/stream_weaver/opal/env.rb +29 -0
- data/lib/stream_weaver/opal/reactive_state.rb +75 -0
- data/lib/stream_weaver/opal/regexp_anchor_patch.rb +62 -0
- data/lib/stream_weaver/opal/renderer.rb +77 -0
- data/lib/stream_weaver/opal/runtime.rb +250 -0
- data/lib/stream_weaver/opal/shell.rb +145 -0
- data/lib/stream_weaver/opal/string_bridge.rb +50 -0
- data/lib/stream_weaver/opal/stubs/diff.min.js +1 -0
- data/lib/stream_weaver/opal/stubs/digest.rb +15 -0
- data/lib/stream_weaver/opal/stubs/marked.umd.js +79 -0
- data/lib/stream_weaver/opal/stubs/md5.rb +3 -0
- data/lib/stream_weaver/opal/stubs/morphdom.min.js +775 -0
- data/lib/stream_weaver/opal/stubs/prism-tomorrow.min.css +1 -0
- data/lib/stream_weaver/opal/stubs/prism.min.js +1967 -0
- data/lib/stream_weaver/opal_entry.rb +136 -0
- data/lib/stream_weaver/org/inline.rb +95 -0
- data/lib/stream_weaver/org/reader.rb +561 -0
- data/lib/stream_weaver/org/recording_context.rb +86 -0
- data/lib/stream_weaver/org/source_splitter.rb +63 -0
- data/lib/stream_weaver/org/writer.rb +300 -0
- data/lib/stream_weaver/page_shell.rb +530 -0
- data/lib/stream_weaver/portfile.rb +79 -0
- data/lib/stream_weaver/pushable.rb +42 -0
- data/lib/stream_weaver/resource/default_views.rb +111 -0
- data/lib/stream_weaver/resource/field_input.rb +25 -0
- data/lib/stream_weaver/resource/state_keys.rb +16 -0
- data/lib/stream_weaver/resource/store.rb +18 -0
- data/lib/stream_weaver/resource.rb +104 -0
- data/lib/stream_weaver/server.rb +1471 -0
- data/lib/stream_weaver/service.rb +1240 -0
- data/lib/stream_weaver/service_client.rb +104 -0
- data/lib/stream_weaver/session_store.rb +186 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/SKILL.md +66 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/examples/canvas-safe-showcase.rb +98 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/actions-and-buttons.md +47 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/charts-and-diagrams.md +38 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/deck.md +32 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/inputs-and-forms.md +65 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/tabs-and-navigation.md +53 -0
- data/lib/stream_weaver/skills/streamweaver-doc-builder/SKILL.md +298 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/SKILL.md +130 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.css +468 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.rb +39 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example_dsl.rb +300 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example.rb +24 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example_dsl.rb +385 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/checkpoints-and-forms.md +28 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/cleanup-and-panel.md +44 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/example-gallery.md +8 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/persistence.md +58 -0
- data/lib/stream_weaver/skills/streamweaver-way/SKILL.md +396 -0
- data/lib/stream_weaver/skills/visual-plan/SKILL.md +201 -0
- data/lib/stream_weaver/skills/visual-recap/SKILL.md +244 -0
- data/lib/stream_weaver/streamer.rb +63 -0
- data/lib/stream_weaver/templates/choices.rb +149 -0
- data/lib/stream_weaver/templates/code.rb +157 -0
- data/lib/stream_weaver/templates/confirm.rb +110 -0
- data/lib/stream_weaver/templates/diff.rb +195 -0
- data/lib/stream_weaver/templates/info.rb +142 -0
- data/lib/stream_weaver/templates/table.rb +169 -0
- data/lib/stream_weaver/templates/wizard.rb +271 -0
- data/lib/stream_weaver/theme/auto_mode.rb +116 -0
- data/lib/stream_weaver/theme/presets.rb +555 -0
- data/lib/stream_weaver/theme.rb +639 -0
- data/lib/stream_weaver/university/canvas.rb +924 -0
- data/lib/stream_weaver/university/course.rb +699 -0
- data/lib/stream_weaver/university/demos/counter.rb +33 -0
- data/lib/stream_weaver/university/demos/dashboard.rb +147 -0
- data/lib/stream_weaver/university/demos/decision_form.rb +143 -0
- data/lib/stream_weaver/university/demos.rb +50 -0
- data/lib/stream_weaver/university/listener.rb +467 -0
- data/lib/stream_weaver/university/progress.rb +201 -0
- data/lib/stream_weaver/university/runner.rb +138 -0
- data/lib/stream_weaver/university/scripts/growing_doc.rb +609 -0
- data/lib/stream_weaver/university/scripts/growing_doc_state.rb +91 -0
- data/lib/stream_weaver/utils.rb +31 -0
- data/lib/stream_weaver/version.rb +5 -0
- data/lib/stream_weaver/views/canvas/reader_layout.erb +800 -0
- data/lib/stream_weaver/views.rb +3940 -0
- data/lib/stream_weaver.rb +122 -0
- data/llms.txt +1269 -0
- data/sig/stream_weaver.rbs +4 -0
- metadata +598 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module University
|
|
5
|
+
# Curriculum data: what `streamweaver get-started` teaches, and what's
|
|
6
|
+
# coming next. Plain data only -- no rendering, no ledger knowledge. The
|
|
7
|
+
# `prompt:` on each Getting Started step is exactly what
|
|
8
|
+
# `driver-worker-runner`'s Runner sends to a worker session, so it stays
|
|
9
|
+
# a plain string here rather than markup or DSL. `why_it_matters:` and
|
|
10
|
+
# `what_you_should_see:` are the step screen's other two content slots
|
|
11
|
+
# (design-spec section 2, "Step screen") -- a short motivation
|
|
12
|
+
# (rendered as two paragraphs via `md`) and the payoff checklist
|
|
13
|
+
# (rendered as a bulleted list), respectively.
|
|
14
|
+
#
|
|
15
|
+
# Rules every step's content obeys (Forrest, after the live UAT rounds
|
|
16
|
+
# of 2026-09-03 -- see features/university-getting-started.context.md,
|
|
17
|
+
# "Course content law"):
|
|
18
|
+
#
|
|
19
|
+
# 1. Never show what a TUI already does, and does faster. Each step has
|
|
20
|
+
# to earn its minute with a capability delta -- live arbitrary UI,
|
|
21
|
+
# charts, diagrams, a blocking visual decision, a growing styled doc,
|
|
22
|
+
# portability. "The agent could have just printed that" means the
|
|
23
|
+
# step failed.
|
|
24
|
+
# 2. CANNED ARTIFACTS, NARRATING AGENT (round 5). The worker never
|
|
25
|
+
# concocts demo DSL live and never reads a source checkout: every
|
|
26
|
+
# demo ships finished inside the gem and is reached through
|
|
27
|
+
# `streamweaver university-demo <name>`. Round 5 measured ~5 minutes
|
|
28
|
+
# to first paint with the agent composing; the fix is to run first
|
|
29
|
+
# and narrate after, so the explanation lands on something the user
|
|
30
|
+
# can already see.
|
|
31
|
+
# 3. Verifying and presenting are different jobs (VERIFY_RULE vs
|
|
32
|
+
# PRESENT_RULE). Round 5 curl-verified step 2 and then never showed
|
|
33
|
+
# it to the user at all.
|
|
34
|
+
# 4. Every prompt opens with previous-step cleanup as ONE backgrounded
|
|
35
|
+
# command that nothing waits on (`cleanup_line`), so housekeeping
|
|
36
|
+
# never sits between the user and the demo. The demo sessions are
|
|
37
|
+
# `dashboard` (step 1), `decision` (step 3) and `doc-demo` (step 4);
|
|
38
|
+
# `university` is the controller canvas the user drives the course
|
|
39
|
+
# from and is never closed.
|
|
40
|
+
module Course
|
|
41
|
+
# Mined from the two real worker sessions
|
|
42
|
+
# (docs/university/worker-session-mining.md): both reached for `curl`
|
|
43
|
+
# and the app's own action log before any browser, and one burned a
|
|
44
|
+
# tool call plus a visible reasoning detour reflexively reaching for a
|
|
45
|
+
# browser MCP its own config forbids. Same line on every step so
|
|
46
|
+
# neither is rediscovered per step. Interpolated into each prompt
|
|
47
|
+
# rather than repeated, so the five can never drift.
|
|
48
|
+
VERIFY_RULE = "VERIFY (for you, silently): prove it with `curl` and the app's own " \
|
|
49
|
+
"logs -- a POST to `.../action/...` returning 200, plus the expected " \
|
|
50
|
+
"text in the HTML that comes back, is proof the DSL block re-ran " \
|
|
51
|
+
"server-side. Verification is yours alone: it opens nothing and shows " \
|
|
52
|
+
"me nothing. Use a headless/automation browser only if this session is " \
|
|
53
|
+
"already configured with one, and never fetch or install a new browser " \
|
|
54
|
+
"tool mid-course."
|
|
55
|
+
|
|
56
|
+
# Round-5 UAT's biggest content failure was not a bug: step 2 was
|
|
57
|
+
# verified with curl and then never shown to the user at all, and step
|
|
58
|
+
# 3 made the user find and open a page themselves. Verifying and
|
|
59
|
+
# presenting are two different jobs with two different tools, so they
|
|
60
|
+
# are now two different rules and every prompt carries both.
|
|
61
|
+
#
|
|
62
|
+
# The second mining pass (docs/university/worker-session-mining.md,
|
|
63
|
+
# "Round-5 latency + portability pass") added the portability half: a
|
|
64
|
+
# real session shelled a personal skill path to show the user a page,
|
|
65
|
+
# which would not exist on a coworker's machine, and baked a
|
|
66
|
+
# dynamically-discovered bridge port into commands it printed.
|
|
67
|
+
PRESENT_RULE = "PRESENT (for me): when something is meant for my eyes, open it in MY " \
|
|
68
|
+
"own default browser -- `open <url>` on macOS, `xdg-open <url>` on " \
|
|
69
|
+
"Linux -- or let `streamweaver panel` open the pane itself. Never use " \
|
|
70
|
+
"browser automation to show me something; that renders into your " \
|
|
71
|
+
"session, not mine. Never set SW_NO_OPEN on a run I am meant to " \
|
|
72
|
+
"interact with. And never hand me a command with a discovered port " \
|
|
73
|
+
"baked into it -- ports move between runs and machines; give me the " \
|
|
74
|
+
"URL that was printed, or a command that resolves the port itself."
|
|
75
|
+
|
|
76
|
+
# One backgrounded command, at the very start, that nothing waits on.
|
|
77
|
+
# Round-5 UAT measured ~5 minutes to first paint; serialized
|
|
78
|
+
# housekeeping in front of the demo was part of it, and none of it is
|
|
79
|
+
# anything the user came to watch.
|
|
80
|
+
def self.cleanup_line(*sessions)
|
|
81
|
+
closes = sessions.map { |s| "streamweaver canvas-close #{s}" }.join('; ')
|
|
82
|
+
"Housekeeping, backgrounded, first line, waited on by nothing: " \
|
|
83
|
+
"`( #{closes} ) >/dev/null 2>&1 &`. Kill any background app task from an earlier " \
|
|
84
|
+
"step the same way. Never close `university` -- that is the controller canvas I am " \
|
|
85
|
+
"driving this course from. Then go straight to the demo; do not report on the cleanup."
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Referenced by `closing_ritual` below while GETTING_STARTED_STEPS
|
|
89
|
+
# itself is still being built (its own step hashes call
|
|
90
|
+
# `closing_ritual`, so the array constant does not exist yet at that
|
|
91
|
+
# point) -- mirrors why VERIFY_RULE/PRESENT_RULE/cleanup_line are also
|
|
92
|
+
# plain methods/constants rather than reaching into the array.
|
|
93
|
+
TOTAL_STEPS = 5
|
|
94
|
+
|
|
95
|
+
# Round-8 UAT: even with the port-discovery advice llms.txt already
|
|
96
|
+
# carries, a worker still spent real time hunting for the port a
|
|
97
|
+
# plain launch picked, then booted a SECOND instance trying to "find"
|
|
98
|
+
# it. Step 2 now pins one port instead of discovering anything --
|
|
99
|
+
# env-overridable so a machine with 4570 already taken doesn't block
|
|
100
|
+
# the course. Falls back to the default rather than raising on a
|
|
101
|
+
# malformed override -- this loads on every `streamweaver` command
|
|
102
|
+
# (cli.rb requires the university listener, which requires this
|
|
103
|
+
# file), so a typo'd env var must not break the whole CLI.
|
|
104
|
+
STEP2_PORT = begin
|
|
105
|
+
Integer(ENV.fetch('SW_UNIVERSITY_APP_PORT', 4570))
|
|
106
|
+
rescue ArgumentError
|
|
107
|
+
4570
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Round-6 UAT: with no fixed sign-off, a worker either kept talking
|
|
111
|
+
# past a finished demo or went quiet with the user unsure the step was
|
|
112
|
+
# actually done. Every prompt now ends on the exact same shape, so
|
|
113
|
+
# "done" always looks the same.
|
|
114
|
+
#
|
|
115
|
+
# Round-8 UAT: the sign-off used to hand a "click Mark done yourself"
|
|
116
|
+
# click back to the user -- real minutes of ambiguity about whether a
|
|
117
|
+
# step had actually finished. The worker closes the loop itself: run
|
|
118
|
+
# `streamweaver university-done N` (same ledger write as the button,
|
|
119
|
+
# plus it brings the University window forward on its own), THEN tell
|
|
120
|
+
# the user in one line what just happened. The manual Mark-done button
|
|
121
|
+
# still works exactly as before; this just means the worker never has
|
|
122
|
+
# to ask for a click. Step 5 has no next step to advance to, so its
|
|
123
|
+
# own variant below points at the course recap instead of "step N+1".
|
|
124
|
+
#
|
|
125
|
+
# Round-9 UAT: that hand-off used to fire the INSTANT the demo ended --
|
|
126
|
+
# too abrupt, with the user still reading the worker's closing words
|
|
127
|
+
# when the window jumped forward under them. The ritual now has an
|
|
128
|
+
# explicit middle step: print the closing words, then WAIT for the
|
|
129
|
+
# user's own typed "done" (or the same in other words), and only THEN
|
|
130
|
+
# run university-done.
|
|
131
|
+
#
|
|
132
|
+
# `done_signal:` is the one per-step exception to that generic wait --
|
|
133
|
+
# a step whose own canvas form already has a "done" option (step 4's
|
|
134
|
+
# picker "done, move on" choice) can already be holding that signal by
|
|
135
|
+
# the time it reaches this ritual, and must not ask the user to type
|
|
136
|
+
# "done" a second time. Kept as a parameter, not baked into the
|
|
137
|
+
# shared text, because only ONE of the five steps has such a form:
|
|
138
|
+
# code review caught the first pass naming step 4's picker inside
|
|
139
|
+
# every step's own wait paragraph, including steps that have no
|
|
140
|
+
# picker to be talking about.
|
|
141
|
+
def self.closing_ritual(number, done_signal: nil)
|
|
142
|
+
intro = "✅ Step #{number} demo complete -- play with it as long as you like."
|
|
143
|
+
generic_wait = "Do not run the next line yet -- WAIT here for my explicit signal that " \
|
|
144
|
+
"I am done: me typing \"done\" (or the same in other words) here in " \
|
|
145
|
+
"this Claude session. No signal yet means no next line: keep waiting, " \
|
|
146
|
+
"and keep answering whatever I ask about this step in the meantime."
|
|
147
|
+
wait_for_signal = done_signal ? "#{generic_wait} #{done_signal}" : generic_wait
|
|
148
|
+
run_it = "The MOMENT that signal lands, run `streamweaver university-done #{number}` " \
|
|
149
|
+
"yourself -- it marks step #{number} done (same as clicking Mark done) and " \
|
|
150
|
+
"brings the University window forward so I see it, with no click required " \
|
|
151
|
+
"from me."
|
|
152
|
+
report = if number >= TOTAL_STEPS
|
|
153
|
+
"Tell me: \"I've marked step #{number} done and brought University forward -- " \
|
|
154
|
+
"that's the whole course; take a look at the recap.\""
|
|
155
|
+
else
|
|
156
|
+
"Tell me: \"I've marked step #{number} done and brought University forward -- " \
|
|
157
|
+
"click Run on step #{number + 1} when ready.\""
|
|
158
|
+
end
|
|
159
|
+
"#{intro}\n\n#{wait_for_signal}\n\n#{run_it}\n\n#{report}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Step 4's own exception to the generic wait-for-"done" text above --
|
|
163
|
+
# its picker already has a "done, move on" choice, so submitting that
|
|
164
|
+
# already IS the signal; asking the worker to also wait for a typed
|
|
165
|
+
# "done" here would be a second ask for the one thing already in hand.
|
|
166
|
+
STEP4_DONE_SIGNAL = "But on THIS step, my picking \"done, move on\" on the doc's own " \
|
|
167
|
+
"picker form already IS that signal -- if that is how you got here, " \
|
|
168
|
+
"the wait is already over, so continue immediately without also " \
|
|
169
|
+
"waiting for me to type \"done\" here."
|
|
170
|
+
|
|
171
|
+
GETTING_STARTED_STEPS = [
|
|
172
|
+
{
|
|
173
|
+
number: 1,
|
|
174
|
+
title: "A dashboard in your pane",
|
|
175
|
+
payoff: "Stat tiles, a chart and a diagram appear beside your terminal -- then change while you watch.",
|
|
176
|
+
why_it_matters: <<~WHY.strip,
|
|
177
|
+
A terminal can print a table faster than any browser can draw one. What it
|
|
178
|
+
cannot do is show you a row of KPI tiles, a chart and a diagram at the same
|
|
179
|
+
time -- and then update all three in place while you keep typing.
|
|
180
|
+
|
|
181
|
+
That is the whole pitch in sixty seconds. Your agent pushes arbitrary UI into
|
|
182
|
+
a pane it controls, and re-pushes as the numbers move. No HTML, no server to
|
|
183
|
+
stand up by hand, no page reload.
|
|
184
|
+
WHY
|
|
185
|
+
prompt: <<~PROMPT.strip,
|
|
186
|
+
#{cleanup_line('dashboard', 'decision', 'doc-demo')}
|
|
187
|
+
|
|
188
|
+
Then run these two commands immediately, before you explain anything. Getting
|
|
189
|
+
something on my screen inside the first minute is the point of this step; the
|
|
190
|
+
explanation is worth more once I can see what you are explaining.
|
|
191
|
+
|
|
192
|
+
streamweaver panel dashboard
|
|
193
|
+
ruby "$(streamweaver university-demo dashboard)" dashboard
|
|
194
|
+
|
|
195
|
+
The demo is a finished file that ships inside the stream_weaver gem. Do not
|
|
196
|
+
write your own, and do not go looking for a source checkout -- there may not
|
|
197
|
+
be one on this machine.
|
|
198
|
+
|
|
199
|
+
NOW narrate what is on my screen: KPI tiles, a Chart.js bar chart and a
|
|
200
|
+
rendered mermaid sequence diagram, all in one push. Point out that the diagram
|
|
201
|
+
is a picture of the command you just ran -- you, the CLI, the bridge, this
|
|
202
|
+
pane. Then read the demo file (`streamweaver university-demo dashboard` prints
|
|
203
|
+
its path) and show me the eight or so lines of DSL that produced all three.
|
|
204
|
+
|
|
205
|
+
Then mutate it while I watch, a few seconds apart, saying what changes before
|
|
206
|
+
each one:
|
|
207
|
+
|
|
208
|
+
ruby "$(streamweaver university-demo dashboard)" dashboard 2
|
|
209
|
+
ruby "$(streamweaver university-demo dashboard)" dashboard 3
|
|
210
|
+
|
|
211
|
+
Same layout, new numbers, in place -- no reload, no flicker, no page I had to
|
|
212
|
+
open.
|
|
213
|
+
|
|
214
|
+
That third push was the last one for this step -- say so plainly: "That was
|
|
215
|
+
the final push -- the dashboard is done." Then stop moving the pane for a
|
|
216
|
+
moment and invite me to actually look at it: the mermaid diagram is not
|
|
217
|
+
decoration, it is the mechanism -- it names you, the CLI, the bridge and this
|
|
218
|
+
pane, the exact chain that just drew everything else on screen.
|
|
219
|
+
|
|
220
|
+
#{VERIFY_RULE}
|
|
221
|
+
|
|
222
|
+
#{PRESENT_RULE}
|
|
223
|
+
|
|
224
|
+
#{closing_ritual(1)}
|
|
225
|
+
PROMPT
|
|
226
|
+
what_you_should_see: [
|
|
227
|
+
"Something is on your screen within seconds -- your agent ran the demo first and explained it after, instead of composing one while you waited.",
|
|
228
|
+
"One push, and the pane holds KPI tiles, a chart and a diagram at once -- three things a terminal cannot draw.",
|
|
229
|
+
"The mermaid diagram describes the canvas-push that put it there: agent, CLI, bridge, pane.",
|
|
230
|
+
"Two more pushes a few seconds later change the numbers in place -- no reload, no flicker.",
|
|
231
|
+
"The demo came out of the installed gem, not out of this repo -- your agent ran `streamweaver university-demo dashboard` and never went looking for a checkout."
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
number: 2,
|
|
236
|
+
title: "Six lines of Ruby",
|
|
237
|
+
payoff: "Run a real app and watch the block re-run on every click.",
|
|
238
|
+
why_it_matters: <<~WHY.strip,
|
|
239
|
+
StreamWeaver's whole trick is one idea: your DSL block re-executes on
|
|
240
|
+
every interaction. There is no event-handler code to write -- state
|
|
241
|
+
changes, the block runs again, the page reflects it.
|
|
242
|
+
|
|
243
|
+
Once that model clicks, everything else in StreamWeaver -- forms,
|
|
244
|
+
canvas, the doc theme -- is just this same loop wearing different clothes.
|
|
245
|
+
WHY
|
|
246
|
+
prompt: <<~PROMPT.strip,
|
|
247
|
+
#{cleanup_line('dashboard')}
|
|
248
|
+
|
|
249
|
+
Then start the app immediately, as a BACKGROUND task, before explaining
|
|
250
|
+
anything, on a PINNED port -- no discovery, no guessing:
|
|
251
|
+
|
|
252
|
+
PORT=#{STEP2_PORT} ruby "$(streamweaver university-demo counter)"
|
|
253
|
+
|
|
254
|
+
That is a finished eight-line file inside the stream_weaver gem. Do not write
|
|
255
|
+
your own version and do not go looking for a source checkout. NEVER boot a
|
|
256
|
+
second instance of it for any reason -- if port #{STEP2_PORT} is already bound,
|
|
257
|
+
it is always a stale course app left over from an earlier run, never something
|
|
258
|
+
else worth investigating: find and kill it first (`lsof -i :#{STEP2_PORT}
|
|
259
|
+
-sTCP:LISTEN`), then start this one. StreamWeaver's own port-scan means
|
|
260
|
+
launching a second instance on a busy port just starts an orphaned duplicate on
|
|
261
|
+
the NEXT free port -- a terrible first impression that tells you nothing about
|
|
262
|
+
the one already running.
|
|
263
|
+
|
|
264
|
+
Setting PORT suppresses the app's own auto-open, by design -- so confirm it is
|
|
265
|
+
up (curl it, or check your captured stdout for the app booting), then open it
|
|
266
|
+
for me yourself:
|
|
267
|
+
|
|
268
|
+
open http://127.0.0.1:#{STEP2_PORT}
|
|
269
|
+
|
|
270
|
+
Say out loud, as part of handing it over, exactly what the app's own callout
|
|
271
|
+
already tells me on screen: when I'm done playing, I come back to this Claude
|
|
272
|
+
session and say "done" -- that is the signal the closing ritual below is waiting
|
|
273
|
+
for, so say it now, not only leave it for me to notice in the callout.
|
|
274
|
+
|
|
275
|
+
NOW narrate. Print the file -- nine lines now, `streamweaver university-demo
|
|
276
|
+
counter` prints its path -- and walk me through it:
|
|
277
|
+
|
|
278
|
+
- The mechanism itself is still exactly six lines, in an eight-line file: four
|
|
279
|
+
statements wrapped by `app "Counter" do` and `end.run!`. The two hidden
|
|
280
|
+
bookends are `require 'stream_weaver'` on line 1, and `.run!` chained onto
|
|
281
|
+
the block's closing `end`. Without the first you get `NoMethodError:
|
|
282
|
+
undefined method 'app'`; without the second the process builds the app,
|
|
283
|
+
starts no server, and exits silently. Both are in the file, on purpose.
|
|
284
|
+
- The ninth line is new, and it is not part of the mechanism: a `callout` at
|
|
285
|
+
the top of the block that introduces the app to whoever it opens for. Point
|
|
286
|
+
at it on my screen and read it to me instead of paraphrasing it -- I may tab
|
|
287
|
+
over here before your narration reaches me.
|
|
288
|
+
- There is no event handler anywhere in it. Tell me to click `+1` a few times,
|
|
289
|
+
and say what actually happens: the whole block re-executes, `state[:count]`
|
|
290
|
+
is one higher when it does, so the `text` line renders a different number.
|
|
291
|
+
That is the entire mechanism, and everything else in StreamWeaver is this
|
|
292
|
+
loop wearing different clothes.
|
|
293
|
+
|
|
294
|
+
Say plainly that the app is running as a background task and that you will
|
|
295
|
+
kill it. When I say I have seen it, kill it and confirm it is gone -- never
|
|
296
|
+
leave a course demo server listening.
|
|
297
|
+
|
|
298
|
+
For any DSL syntax question, run `streamweaver llm` FIRST -- one command, the
|
|
299
|
+
canonical reference, and its counter example is exactly this app. Do not
|
|
300
|
+
dispatch a search agent to grep a repo for it.
|
|
301
|
+
|
|
302
|
+
#{VERIFY_RULE}
|
|
303
|
+
|
|
304
|
+
#{PRESENT_RULE}
|
|
305
|
+
|
|
306
|
+
#{closing_ritual(2)}
|
|
307
|
+
PROMPT
|
|
308
|
+
what_you_should_see: [
|
|
309
|
+
"The app comes up on a pinned port -- no port to guess, no second instance booted to \"find\" one -- and your agent opens it for you within seconds instead of quietly verifying it with curl and moving on without showing you.",
|
|
310
|
+
"Each click updates the count immediately, with no page reload or spinner.",
|
|
311
|
+
"The Ruby block ran again on every click; nothing else touched the page.",
|
|
312
|
+
"Your agent printed all nine lines of the file it actually ran, and named `require 'stream_weaver'` and `end.run!` as the two the \"six-line app\" framing hides -- the two that cost real sessions a debug cycle each.",
|
|
313
|
+
"A callout at the top of the app explains itself the moment it opens -- what it is and what to click -- in case you tab over here before the narration reaches you.",
|
|
314
|
+
"It said plainly that the app runs as a background task, offered to kill it, and did kill it at the end -- no orphaned server left behind."
|
|
315
|
+
]
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
number: 3,
|
|
319
|
+
title: "Claude asks you a real question",
|
|
320
|
+
payoff: "Your agent stops, shows you two architectures, and blocks until you decide.",
|
|
321
|
+
why_it_matters: <<~WHY.strip,
|
|
322
|
+
An agent that needs a decision from you has exactly one move in a terminal:
|
|
323
|
+
print the question and wait for text. It cannot show you the two designs it
|
|
324
|
+
is choosing between, lay them side by side, and let you pick one.
|
|
325
|
+
|
|
326
|
+
Here it can. The same form has two modes -- blocking, where your agent's
|
|
327
|
+
terminal freezes until you submit, and non-blocking, where the form stays
|
|
328
|
+
live in the pane and the agent reads your answer whenever it needs it. This
|
|
329
|
+
is the step where the canvas stops being a display and becomes an input.
|
|
330
|
+
WHY
|
|
331
|
+
prompt: <<~PROMPT.strip,
|
|
332
|
+
#{cleanup_line('dashboard', 'decision')}
|
|
333
|
+
|
|
334
|
+
PART ONE -- the founding loop. Run this now, as a BACKGROUND task, before
|
|
335
|
+
explaining anything:
|
|
336
|
+
|
|
337
|
+
ruby "$(streamweaver university-demo decision-form)"
|
|
338
|
+
|
|
339
|
+
That is a finished file inside the stream_weaver gem: a mermaid diagram of two
|
|
340
|
+
candidate architectures, a comparison table, a `radio_group` and a rationale
|
|
341
|
+
`text_field`. Do not write your own and do not look for a source checkout.
|
|
342
|
+
|
|
343
|
+
It runs through `run_once!`, which means it finds a free port, OPENS my browser
|
|
344
|
+
itself, and BLOCKS until I submit. So: do not ask me to open anything, do not
|
|
345
|
+
print a URL for me to click, and do not ask me to tell you when I am done. The
|
|
346
|
+
page is already in front of me. Say one line -- "answer that, I am waiting" --
|
|
347
|
+
and then wait for the process to exit.
|
|
348
|
+
|
|
349
|
+
The INSTANT that process exits -- before you read its output, before you print
|
|
350
|
+
anything -- run `streamweaver focus-me`. It brings your own terminal tab back in
|
|
351
|
+
front of me (a fire-and-forget iTerm2 activate, silent everywhere else), and it
|
|
352
|
+
has to be the very first thing you do, because my attention is still on the
|
|
353
|
+
browser tab that form just closed; run it after your reaction and I would already
|
|
354
|
+
be looking at the reaction I never saw land.
|
|
355
|
+
|
|
356
|
+
Only THEN print my submitted state as JSON on stdout, verbatim, and REACT to it:
|
|
357
|
+
name the option I picked, quote the rationale I typed, and say what you would
|
|
358
|
+
actually do differently because of it. That round trip -- your process blocked
|
|
359
|
+
on a human, and resumed with their answer in hand -- is the whole step.
|
|
360
|
+
|
|
361
|
+
PART TWO -- the same file, a second surface. Say that plainly, because it is
|
|
362
|
+
the lesson: not a second form, the same one.
|
|
363
|
+
|
|
364
|
+
streamweaver panel decision
|
|
365
|
+
ruby "$(streamweaver university-demo decision-form)" canvas decision
|
|
366
|
+
|
|
367
|
+
Same context, same `radio_group`, same `text_field` -- now on the canvas,
|
|
368
|
+
with a callout saying you are frozen, and an explicit submit button.
|
|
369
|
+
|
|
370
|
+
Then immediately: `streamweaver canvas-raise decision`. PART ONE's blocking
|
|
371
|
+
form pulled my attention to its own browser tab, and a push you make mid-
|
|
372
|
+
response -- unlike a Run submit -- raises nothing on its own; without this
|
|
373
|
+
I would never see the canvas surface unprompted. This is PRESENT-mode
|
|
374
|
+
discipline, same as `open`/`streamweaver panel` elsewhere in this course:
|
|
375
|
+
for my eyes, never automation.
|
|
376
|
+
|
|
377
|
+
Then wait on it -- and how you wait is itself worth saying out loud. Run
|
|
378
|
+
`streamweaver canvas-wait decision` AS A BACKGROUND TASK from the start, not
|
|
379
|
+
in the foreground. A foreground wait on a human will blow past your harness's
|
|
380
|
+
foreground-block ceiling, get demoted to a background task anyway, and cost
|
|
381
|
+
minutes in polling lag on the way back. A blocking wait on a human is a
|
|
382
|
+
background job with a completion notification, and that is how an agent should
|
|
383
|
+
wait on a person. The moment the notification lands, read the result and react
|
|
384
|
+
to it immediately -- do not sit in a poll loop.
|
|
385
|
+
|
|
386
|
+
Close on one beat: the same form left live with no `canvas-wait` behind it
|
|
387
|
+
keeps its state in the pane, and you can read my answer back whenever you need
|
|
388
|
+
it. Blocking is a choice, not a limitation.
|
|
389
|
+
|
|
390
|
+
Once you have said that, the teaching for this step is DONE -- both surfaces
|
|
391
|
+
are demonstrated, full stop. Declare it plainly and move straight to the
|
|
392
|
+
closing ritual below; do not keep role-playing with the form as though there
|
|
393
|
+
is more to teach. If I want to keep clicking around in it afterward, that is
|
|
394
|
+
fine -- offer it as a pure aside, optional, in words like "the form still
|
|
395
|
+
works -- keep playing if you like; the course has moved on", never folded in
|
|
396
|
+
as more of the lesson.
|
|
397
|
+
|
|
398
|
+
#{VERIFY_RULE}
|
|
399
|
+
|
|
400
|
+
#{PRESENT_RULE}
|
|
401
|
+
|
|
402
|
+
#{closing_ritual(3)}
|
|
403
|
+
PROMPT
|
|
404
|
+
what_you_should_see: [
|
|
405
|
+
"The browser opened by itself with a real question in it -- you never opened a page, never copied a URL, and never had to tell your agent you were done.",
|
|
406
|
+
"The form carries a rendered diagram of both designs and a comparison table right next to the choice; a terminal prompt can carry neither.",
|
|
407
|
+
"The moment you submitted, your agent printed the JSON verbatim and reacted to it -- naming your choice and quoting the rationale you typed.",
|
|
408
|
+
"Then the same file, unchanged, appeared on the canvas -- your agent said so out loud: one artifact, two surfaces.",
|
|
409
|
+
"It waited on the canvas form as a background task and said why: a blocking wait on a human is a background job with a notification, not a frozen foreground prompt.",
|
|
410
|
+
"The closing beat: the same form left live without `canvas-wait` becomes state the agent can query later."
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
number: 4,
|
|
415
|
+
title: "A doc that writes itself",
|
|
416
|
+
payoff: "Watch a script write a document, save it, then ask you what else it should say.",
|
|
417
|
+
why_it_matters: <<~WHY.strip,
|
|
418
|
+
A canvas isn't limited to one static push -- a script can keep adding to the
|
|
419
|
+
same session over time, and the pane updates each time without you touching
|
|
420
|
+
anything. What grows is a real document: an outline, a two-column section,
|
|
421
|
+
syntax-highlighted code, a callout, a rendered diagram, a table.
|
|
422
|
+
|
|
423
|
+
Then the interesting half. The script saves the doc itself and tells you where
|
|
424
|
+
it went -- and your agent asks what else it should say, blocks on your answer,
|
|
425
|
+
adds it, and saves again. That loop is the point: co-editing a document with an
|
|
426
|
+
agent, in a pane, with no file open on either side.
|
|
427
|
+
WHY
|
|
428
|
+
prompt: <<~PROMPT.strip,
|
|
429
|
+
#{cleanup_line('decision')}
|
|
430
|
+
|
|
431
|
+
Then start the doc growing immediately, before explaining anything:
|
|
432
|
+
|
|
433
|
+
streamweaver panel doc-demo --theme=doc
|
|
434
|
+
ruby "$(streamweaver university-demo doc)" doc-demo
|
|
435
|
+
|
|
436
|
+
Narrate it WHILE it runs -- it takes about twenty seconds and the pacing is
|
|
437
|
+
the point. Each push carries everything before it plus one more section, so
|
|
438
|
+
the page grows instead of blinking: a doc header, a two-column comparison, a
|
|
439
|
+
syntax-highlighted code block and a callout, a mermaid diagram, a table of
|
|
440
|
+
what survives an export, and a closing section. Six outline entries, which is
|
|
441
|
+
what makes the sidebar nav in step 5 worth looking at.
|
|
442
|
+
|
|
443
|
+
The script narrates its own progress to stdout so you don't have to guess when
|
|
444
|
+
a push landed: a `stage N/7 pushing: <name>` line right before each one, and a
|
|
445
|
+
`stage N/7 pushed: <name>` line right after. Watch that stream and say
|
|
446
|
+
something to me the moment each stage lands -- do not wait for the whole run
|
|
447
|
+
to finish and then summarize it in one breath at the end. That is what "while
|
|
448
|
+
it runs" means here.
|
|
449
|
+
|
|
450
|
+
The script saves the document itself, through the same `save-doc` endpoint the
|
|
451
|
+
floating button calls, under the deterministic name `university-doc`, and
|
|
452
|
+
prints the exact path it landed at. Read that path back to me verbatim -- step
|
|
453
|
+
5 uses that file, and you are not to make me transcribe anything.
|
|
454
|
+
|
|
455
|
+
THEN, and only then, invite me to do it by hand: there is a floating "Save as
|
|
456
|
+
doc" button at the bottom right of the pane. Tell me to click it, and explain
|
|
457
|
+
the dialog before I do -- typing a name and pressing Save writes a permanent,
|
|
458
|
+
git-tracked `docs/streamweaver_canvas/<name>.rb`; "Save as Org" writes the
|
|
459
|
+
same content as a plain-text `.org` sibling. Say what the two formats are for,
|
|
460
|
+
not just what they're named: `.rb` is full fidelity -- StreamWeaver can
|
|
461
|
+
re-render and extend it again later, exactly as it looked here. `.org` is the
|
|
462
|
+
portable half -- plain text, human-readable anywhere with nothing to install,
|
|
463
|
+
and the StreamWeaver Doc Viewer extension (step 5) makes that same file
|
|
464
|
+
beautiful again without needing StreamWeaver at all. Mine is a bonus lap. Step
|
|
465
|
+
5 uses yours.
|
|
466
|
+
|
|
467
|
+
THE TWEAK LOOP. Now tell me the doc is mine to change for as long as I like,
|
|
468
|
+
and offer to extend it -- and say what you are doing as you do it: "I am using
|
|
469
|
+
the same blocking form from step 3 to ask you a real question. This is how an
|
|
470
|
+
agent and a human co-edit a document."
|
|
471
|
+
|
|
472
|
+
ruby "$(streamweaver university-demo doc)" doc-demo --picker
|
|
473
|
+
|
|
474
|
+
That appends a picker to the bottom of the doc: a `radio_group` of sections
|
|
475
|
+
not yet in it (the visible choice IS the exact `--extend` key -- read a
|
|
476
|
+
legend line above the choices for what each one means), a free-text field,
|
|
477
|
+
and a "done, move on" option. Wait on it with `streamweaver canvas-wait
|
|
478
|
+
doc-demo` AS A BACKGROUND TASK -- same rule as step 3, never a foreground
|
|
479
|
+
block on a human -- and react the moment the notification lands.
|
|
480
|
+
|
|
481
|
+
If I pick a canned section, add it and re-save in one command, using the key
|
|
482
|
+
exactly as it appeared on the radio choice -- do not paraphrase or shorten it:
|
|
483
|
+
|
|
484
|
+
ruby "$(streamweaver university-demo doc)" doc-demo --extend=<key>
|
|
485
|
+
|
|
486
|
+
(Keys accumulate: `--extend=timeline,cheatsheet` for two -- and so do repeat
|
|
487
|
+
invocations, on their own: the command remembers every key you have already
|
|
488
|
+
applied to this session, so a later `--picker` round never loses one.) The
|
|
489
|
+
command tells you plainly -- one line per key, `OK <key> → section '<exact
|
|
490
|
+
rendered header>'` for a key it recognized, `FAILED` for one it did not -- and
|
|
491
|
+
it exits non-zero if anything failed. Every rendered header BEGINS with its
|
|
492
|
+
own key, capitalized, so that OK line is something you can act on directly.
|
|
493
|
+
Still VERIFY before telling me anything landed: curl the `doc-demo` session
|
|
494
|
+
(or read the command's own OK/FAILED output) and grep for that exact header
|
|
495
|
+
text. Only then say it is there -- a push that silently no-op'd once looked
|
|
496
|
+
identical to a successful one.
|
|
497
|
+
|
|
498
|
+
If I typed a description instead, IMMEDIATELY -- before you write a word of
|
|
499
|
+
the real section -- push a placeholder to the `doc-demo` session so the pane
|
|
500
|
+
never just sits there while you work: a bare `callout` reading "⏳ Building
|
|
501
|
+
your <my request, verbatim> -- hold on" is enough. THEN write the section
|
|
502
|
+
itself, and keep it to `doc_section_header`, `md`, `table headers:/rows:`,
|
|
503
|
+
`comparison`, `code_block`, `callout` and `mermaid`. Those are exactly the
|
|
504
|
+
components `streamweaver org-export` recognizes; anything else looks right in
|
|
505
|
+
the pane and then leaves as an unrecognized placeholder, silently, which is
|
|
506
|
+
the failure step 5 exists to disprove.
|
|
507
|
+
|
|
508
|
+
Before you push that section anywhere, VALIDATE it locally by actually
|
|
509
|
+
rendering it -- a syntax check is not enough. A live run once hit "wrong number
|
|
510
|
+
of arguments" from a component call that was perfectly valid Ruby and only
|
|
511
|
+
failed at render time, and I sat looking at that bare error for minutes with no
|
|
512
|
+
sign anything was happening:
|
|
513
|
+
|
|
514
|
+
ruby -e "require 'stream_weaver'; StreamWeaver::CLI.render_dsl_to_html(File.read(ARGV[0]))" /path/to/your/snippet.rb
|
|
515
|
+
|
|
516
|
+
If that errors, do not leave the placeholder sitting there unexplained --
|
|
517
|
+
immediately push an updated callout to `doc-demo`: "⚠️ Hit an error building
|
|
518
|
+
<my request, verbatim> -- fixing it now", then fix the section and re-validate.
|
|
519
|
+
Once your `--add-custom` push below lands, the real section replaces whatever
|
|
520
|
+
placeholder or error callout was showing -- that IS the success update, nothing
|
|
521
|
+
further to push.
|
|
522
|
+
|
|
523
|
+
A hand-written section is not a canned `--extend` key, so on its own it has
|
|
524
|
+
nowhere to persist -- the NEXT `--picker` round would clobber it right back
|
|
525
|
+
out. (A live run lost a user's own Star Wars chart section exactly this way.)
|
|
526
|
+
Save your finished, validated DSL to a scratch file and hand it to the script
|
|
527
|
+
instead of pushing it yourself:
|
|
528
|
+
|
|
529
|
+
ruby "$(streamweaver university-demo doc)" doc-demo --add-custom <a-short-key> <path-to-your-snippet-file>
|
|
530
|
+
|
|
531
|
+
That persists the section alongside everything else already in the doc, so
|
|
532
|
+
every later rebuild includes it automatically, and pushes + re-saves the
|
|
533
|
+
finished document in one command -- do not push a hand-written section any
|
|
534
|
+
other way, or the persistence never happens.
|
|
535
|
+
|
|
536
|
+
Then push the picker again and loop, until I choose "done".
|
|
537
|
+
|
|
538
|
+
The MOMENT I choose "done", before saying anything else, run:
|
|
539
|
+
|
|
540
|
+
ruby "$(streamweaver university-demo doc)" doc-demo --finish
|
|
541
|
+
|
|
542
|
+
That re-pushes the finished document -- base plus every section you added,
|
|
543
|
+
with NO picker form this time -- and saves it. Skipping this leaves the pane
|
|
544
|
+
showing whatever the canvas-wait adapter puts up the instant a form is
|
|
545
|
+
submitted (a plain "Submitted -- you can close this window" screen, not the
|
|
546
|
+
document) -- nothing else re-pushes on its own once I pick "done", so this
|
|
547
|
+
command is the only thing that puts the doc back on screen. Then say the saved
|
|
548
|
+
path once more at the end.
|
|
549
|
+
|
|
550
|
+
#{VERIFY_RULE}
|
|
551
|
+
|
|
552
|
+
#{PRESENT_RULE}
|
|
553
|
+
|
|
554
|
+
#{closing_ritual(4, done_signal: STEP4_DONE_SIGNAL)}
|
|
555
|
+
PROMPT
|
|
556
|
+
what_you_should_see: [
|
|
557
|
+
"The pane grows a new section every few seconds -- you watch it happen, you don't just see the end state.",
|
|
558
|
+
"What grows is a document, not three cards: a header and outline, a two-column section, a code block, a callout, a rendered diagram and a table.",
|
|
559
|
+
"The outline on the left fills in as each section arrives, and ends up long enough to be worth navigating.",
|
|
560
|
+
"Your agent saved the doc itself and read you the exact path -- no dialog to fill in, nothing to transcribe. The floating \"Save as doc\" button is then offered to you as a bonus lap, with the dialog explained before you click.",
|
|
561
|
+
"Then it asks what else the doc should say, using the same blocking form from step 3, and says so -- adds your pick, re-pushes, re-saves, and asks again until you say you're done. That is co-editing a document with an agent, with no file open on either side."
|
|
562
|
+
]
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
number: 5,
|
|
566
|
+
title: "Take the doc with you",
|
|
567
|
+
payoff: "Export to org, drop it in a gist, read it anywhere.",
|
|
568
|
+
why_it_matters: <<~WHY.strip,
|
|
569
|
+
A canvas doc only exists while StreamWeaver is running -- until you
|
|
570
|
+
export it. `.org` is a plain-text format GitHub already renders
|
|
571
|
+
reasonably, and a gist is the fastest way to hand it to someone who
|
|
572
|
+
has never heard of StreamWeaver.
|
|
573
|
+
|
|
574
|
+
The last step is seeing the difference for yourself: plain GitHub
|
|
575
|
+
rendering versus the same file through the StreamWeaver Doc Viewer
|
|
576
|
+
extension.
|
|
577
|
+
WHY
|
|
578
|
+
prompt: <<~PROMPT.strip,
|
|
579
|
+
Before anything else, run `gh auth status`. If the `gh` CLI is missing, not
|
|
580
|
+
logged in, or logged in without the `gist` scope, say exactly which of those
|
|
581
|
+
it is, and hand it back friendly and concrete: `brew install gh`, then `gh auth
|
|
582
|
+
login`, or `gh auth refresh -s gist` if it is only the scope that is missing.
|
|
583
|
+
Then stop and wait for me. Do not fake the gist half of this step, and do not
|
|
584
|
+
press on hoping it works.
|
|
585
|
+
|
|
586
|
+
Then, with the doc still open in the `doc-demo` pane, state the plan before you
|
|
587
|
+
point at anything -- one breath, up front: "Look at two things -- the outline
|
|
588
|
+
nav and the mermaid popout. Say 'go' when you're done and I'll export it and
|
|
589
|
+
push it to a gist." That is the whole CTA, said FIRST, not stitched on after
|
|
590
|
+
you've already started pointing things out -- so I always know exactly what
|
|
591
|
+
you are waiting on me for and that nothing is running yet.
|
|
592
|
+
|
|
593
|
+
Before you point at anything, VERIFY the pane actually shows the document:
|
|
594
|
+
`curl` the `doc-demo` session and grep for a heading you know is in it (e.g.
|
|
595
|
+
"Terminal vs canvas", the first section's title). If it is missing -- the
|
|
596
|
+
picker's own submitted-form screen is showing instead, or anything else that
|
|
597
|
+
isn't the doc -- restore it first: `ruby "$(streamweaver university-demo
|
|
598
|
+
doc)" doc-demo --finish` re-pushes the finished document with no picker. Only
|
|
599
|
+
once the heading actually greps do you move on. Never narrate UI I cannot see
|
|
600
|
+
on my screen.
|
|
601
|
+
|
|
602
|
+
THEN point at the two things themselves, the ones I would otherwise walk past:
|
|
603
|
+
|
|
604
|
+
- The outline. Six-plus sections is what earns a sidebar; in a pane this
|
|
605
|
+
narrow the doc theme moves that nav to the top instead of the side, and it
|
|
606
|
+
is the same nav either way. Scroll it, or click an entry, so I see it work.
|
|
607
|
+
- The mermaid diagram's popout control -- a rendered diagram in a 750px pane
|
|
608
|
+
is a thumbnail until you open it full size.
|
|
609
|
+
|
|
610
|
+
Wait for me to say "go" (or the same in other words). Only then take it with
|
|
611
|
+
you. Step 4's script saved the doc under the deterministic
|
|
612
|
+
name `university-doc` and printed the path; use that path. If you no longer
|
|
613
|
+
have it, look in `docs/streamweaver_canvas/` AND `~/.streamweaver/canvas/`
|
|
614
|
+
(the bridge's working directory is often not this shell's) and tell me where
|
|
615
|
+
it actually is -- but do not make me name it.
|
|
616
|
+
|
|
617
|
+
Before you touch org-export or gh, push a small status so the doc-demo pane
|
|
618
|
+
says something is happening instead of just sitting there while gist creation
|
|
619
|
+
runs: `streamweaver canvas-toast doc-demo "⏳ pushing to gist..." --variant info`.
|
|
620
|
+
|
|
621
|
+
Run `streamweaver org-export <that file>` to write the sibling `.org`, and show
|
|
622
|
+
me the first few lines so I can see it is plain text with no server behind it.
|
|
623
|
+
Then `gh gist create --public <that .org file>` -- public on purpose, because
|
|
624
|
+
this step ends with the file being opened and compared in a browser -- and give
|
|
625
|
+
me the gist URL. Open it in MY browser, not yours. The moment you have that URL,
|
|
626
|
+
replace the status: `streamweaver canvas-toast doc-demo "✅ Pushed: <gist URL>"
|
|
627
|
+
--variant success` -- the pending toast never lingers once the real work is
|
|
628
|
+
actually done. If either command fails instead, replace it the same way with
|
|
629
|
+
what actually happened: `streamweaver canvas-toast doc-demo "⚠️ <what failed> --
|
|
630
|
+
fixing it now" --variant warning`, same honesty rule as step 4's error card --
|
|
631
|
+
never leave "pushing to gist..." sitting there past the point where it stopped
|
|
632
|
+
being true.
|
|
633
|
+
|
|
634
|
+
The last beat is NOT yours. Stop and hand it to me, and say why in plain words:
|
|
635
|
+
no automated or headless browser can install a Chrome Web Store extension or
|
|
636
|
+
see my logged-in Chrome, so this part is structurally out of your reach no
|
|
637
|
+
matter which browser tool you have. Do not attempt it and do not skip it
|
|
638
|
+
silently. Give me the StreamWeaver Doc Viewer link from this project's README,
|
|
639
|
+
tell me to install it, reload the gist and click "View rendered", and then wait
|
|
640
|
+
for me to tell you what I saw -- the same file, now carrying the outline, the
|
|
641
|
+
two-column section, the code block, the callout and the diagram it had in the
|
|
642
|
+
canvas.
|
|
643
|
+
|
|
644
|
+
Finish by cleaning up after the whole course, backgrounded and in one line:
|
|
645
|
+
`( streamweaver canvas-close dashboard; streamweaver canvas-close decision;
|
|
646
|
+
streamweaver canvas-close doc-demo ) >/dev/null 2>&1 &`. Kill any background
|
|
647
|
+
task still running. Then `streamweaver canvas-list` and confirm to me that
|
|
648
|
+
only `university` -- the controller -- is left.
|
|
649
|
+
|
|
650
|
+
#{VERIFY_RULE}
|
|
651
|
+
|
|
652
|
+
#{PRESENT_RULE}
|
|
653
|
+
|
|
654
|
+
#{closing_ritual(5)}
|
|
655
|
+
PROMPT
|
|
656
|
+
what_you_should_see: [
|
|
657
|
+
"Your agent checked `gh auth status` first and told you up front if gh was missing, logged out, or short the gist scope -- with the exact command to fix it -- instead of failing halfway through.",
|
|
658
|
+
"It pointed out the doc's own navigation before exporting it: the outline the six sections earned, moved to the top in a narrow pane, and the mermaid diagram's popout.",
|
|
659
|
+
"It used the doc step 4 saved for you, at the path step 4 printed -- you never had to name a file.",
|
|
660
|
+
"`org-export` writes a .org file next to the doc -- no network access, no server -- and `gh gist create --public` prints a gist URL that opens in your own browser.",
|
|
661
|
+
"Then it stops and hands the extension step back to you, saying why: no automated or headless browser can install a Web Store extension or reach your logged-in Chrome. It gives you the link and waits -- it does not try, and it does not quietly skip.",
|
|
662
|
+
"Plain GitHub already reads the .org file close to markdown; once you install it, the extension's \"View rendered\" button brings back the outline, two-column section, code block, callout and diagram exactly as they looked in the canvas.",
|
|
663
|
+
"Every demo session from steps 1-4 is closed at the end -- `streamweaver canvas-list` shows only `university`, the controller you have been driving from."
|
|
664
|
+
]
|
|
665
|
+
}
|
|
666
|
+
].freeze
|
|
667
|
+
|
|
668
|
+
def self.step(number)
|
|
669
|
+
GETTING_STARTED_STEPS.find { |s| s[:number] == number.to_i }
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
# What the runner sends to the worker session, and what the canvas
|
|
673
|
+
# offers for copying when it can't send. nil for a step number the
|
|
674
|
+
# course doesn't have (the runner reports that as :unknown_step);
|
|
675
|
+
# `fetch` rather than `dig` so a curriculum entry written without a
|
|
676
|
+
# `prompt:` raises for its author instead of masquerading as one.
|
|
677
|
+
def self.prompt_for(number)
|
|
678
|
+
step(number)&.fetch(:prompt)
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Rendered dormant on the course-list shelf, no controls -- names and
|
|
682
|
+
# blurbs only, per docs/university/design-spec.md section 2.
|
|
683
|
+
FUTURE_COURSES = [
|
|
684
|
+
{
|
|
685
|
+
name: "Docs deep dive",
|
|
686
|
+
blurb: "Org export, gists, and the reader extension, end to end."
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
name: "Canvas modes",
|
|
690
|
+
blurb: "Stateful, blocking, and streaming — when to reach for each."
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: "Skills and panels",
|
|
694
|
+
blurb: "Teach your agent to drive the canvas without you."
|
|
695
|
+
}
|
|
696
|
+
].freeze
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
end
|