```
├── .cargo/
├── config.toml (100 tokens)
├── .config/
├── nextest.toml
├── .cursor/
├── rules/
├── docs.mdc (400 tokens)
├── skills/
├── doc_explanation.md (300 tokens)
├── doc_howto.md (200 tokens)
├── doc_reference.md (200 tokens)
├── doc_tutorial.md (400 tokens)
├── doc_workflow.md (800 tokens)
├── .githooks/
├── pre-commit (100 tokens)
├── .github/
├── assets/
├── iii-intro-preview.gif
├── workers-add.gif
├── zero-integration.png
├── pull_request_template.md
├── scripts/
├── build_engine_publish_payload.py (1700 tokens)
├── build_skills_payload.py (1100 tokens)
├── bump_manifests.py (1400 tokens)
├── calculate_release_version.py (2.4k tokens)
├── collect_engine_worker_interface.py (1400 tokens)
├── discover_engine_worker_skills.py (700 tokens)
├── license-agreement-check.mjs (2.2k tokens)
├── license-agreement-check.test.mjs (1000 tokens)
├── pin_docs.py (2.1k tokens)
├── test_bump_manifests.py (2.3k tokens)
├── test_calculate_release_version.py (7.5k tokens)
├── test_collect_engine_worker_interface.py (500 tokens)
├── test_pin_docs.py (3.2k tokens)
├── validate_release_worker_matrix.py (600 tokens)
├── workflows/
├── WORKFLOWS.md (3.4k tokens)
├── _go.yml (1200 tokens)
├── _homebrew.yml (2.1k tokens)
├── _npm.yml (700 tokens)
├── _publish-engine-workers.yml (1900 tokens)
├── _publish-worker-skills.yml (500 tokens)
├── _py.yml (600 tokens)
├── _rust-binary.yml (2.4k tokens)
├── _rust-cargo.yml (600 tokens)
├── alpha-release.yml (3.3k tokens)
├── bench-release.yml (600 tokens)
├── checklist-checker.yml (500 tokens)
├── ci.yml (6k tokens)
├── cloudfront-functions-test.yml (100 tokens)
├── create-tag.yml (2.7k tokens)
├── deploy-website.yml (1500 tokens)
├── docker-engine.yml (1700 tokens)
├── generate-api-docs.yml (800 tokens)
├── install-sh.yml (500 tokens)
├── license-check.yml (100 tokens)
├── publish-worker-skills.yml (100 tokens)
├── release-iii.yml (6.9k tokens)
├── skill-check-recheck.yml (200 tokens)
├── skill-check.yml (200 tokens)
├── smoke.yml (700 tokens)
├── test-scripts.yml (200 tokens)
├── tf-apply.yml (500 tokens)
├── tf-plan.yml (500 tokens)
├── .gitignore (200 tokens)
├── .prettierignore (omitted)
├── .prettierrc
├── .skill-check.yaml (100 tokens)
├── AGENTS.md (1200 tokens)
├── CONTRIBUTING.md (1900 tokens)
├── Cargo.lock (omitted)
├── Cargo.toml (300 tokens)
├── LICENSE.spdx (200 tokens)
├── Makefile (1600 tokens)
├── README.md (2.1k tokens)
├── biome.json (1100 tokens)
├── blog/
├── .gitignore
├── .nvmrc
├── README.md (400 tokens)
├── astro.config.mjs (100 tokens)
├── package.json (100 tokens)
├── src/
├── assets/
├── blog/
├── add-a-worker/
├── banner.png
├── orchestrator-network.png
├── orders-worker-chain.png
├── building-agents-for-real-world/
├── banner.png
├── how-to-build-your-own-agent-harness/
├── cover.png
├── loop-engineering-is-just-software-engineering/
├── banner.png
├── harness-traces-console.png
├── harness-turn-waterfall.png
├── loop-engineering-meme.png
├── provider-configuration.png
├── the-harness-is-the-backend/
├── point-to-point-vs-shared-runtime.png
├── the-substrate-is-installable/
├── banner.png
├── why-agent-sandboxes-are-converging-on-libkrun-not-firecracker/
├── banner.png
├── components/
├── AnalyticsHead.astro (400 tokens)
├── CookieBanner.astro (700 tokens)
├── Footer.astro (300 tokens)
├── FormattedDate.astro
├── Logo.astro (100 tokens)
├── ThemeInit.astro (300 tokens)
├── ThemeToggle.astro (700 tokens)
├── content.config.ts (100 tokens)
├── content/
├── blog/
├── add-a-worker.md (2.3k tokens)
├── building-agents-for-real-world.md (2.8k tokens)
├── how-to-build-your-own-agent-harness.md (4.7k tokens)
├── loop-engineering-is-just-software-engineering.md (2.7k tokens)
├── the-harness-is-the-backend.md (3.8k tokens)
├── the-substrate-is-installable.md (3.3k tokens)
├── why-agent-sandboxes-are-converging-on-libkrun-not-firecracker.md (4.5k tokens)
├── layouts/
├── BaseLayout.astro (400 tokens)
├── BlogPost.astro (200 tokens)
├── pages/
├── [...slug].astro (100 tokens)
├── index.astro (200 tokens)
├── rss.xml.ts (200 tokens)
├── styles/
├── global.css (1900 tokens)
├── tests/
├── build.test.ts (900 tokens)
├── tsconfig.json
├── console/
├── .gitignore (200 tokens)
├── LICENSE (2.2k tokens)
├── README.md (700 tokens)
├── install.sh (4.5k tokens)
├── lefthook.yml
├── package.json (200 tokens)
├── packages/
├── console-frontend/
├── DESIGN.md (1200 tokens)
├── README.md (1100 tokens)
├── biome.json (300 tokens)
├── index.html (300 tokens)
├── package.json (400 tokens)
├── postcss.config.mjs
├── public/
├── favicon/
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── site.webmanifest (100 tokens)
├── file.svg (100 tokens)
├── globe.svg (200 tokens)
├── iii-black1.1.svg (100 tokens)
├── iii-white1.1.svg (200 tokens)
├── next.svg (300 tokens)
├── vercel.svg
├── window.svg (100 tokens)
├── src/
├── api/
├── alerts/
├── alerts.ts (300 tokens)
├── sampling.ts (300 tokens)
├── config-provider.tsx (500 tokens)
├── config.ts (200 tokens)
├── events/
├── functions.ts (800 tokens)
├── invocation.ts (700 tokens)
├── flows/
├── flows.ts (2.4k tokens)
├── types.ts (300 tokens)
├── index.ts (700 tokens)
├── observability/
├── logs.ts (800 tokens)
├── metrics.ts (800 tokens)
├── traces.ts (700 tokens)
├── queries.ts (1300 tokens)
├── queues/
├── index.ts
├── queues.ts (600 tokens)
├── state/
├── state.ts (500 tokens)
├── streams.ts (300 tokens)
├── system/
├── adapters.ts (100 tokens)
├── status.ts (500 tokens)
├── workers.test.ts (300 tokens)
├── workers.ts (400 tokens)
├── types/
├── shared.ts (400 tokens)
├── utils.ts (400 tokens)
├── websocket.ts (1100 tokens)
├── components/
├── command-palette.tsx (1200 tokens)
├── filters/
├── TimeRangeFilter.tsx (2.5k tokens)
├── flow/
├── base-edge.tsx (300 tokens)
├── flow-page.tsx (1400 tokens)
├── flow-selector.tsx (400 tokens)
├── flow-view.tsx (600 tokens)
├── node-organizer.tsx (500 tokens)
├── nodes/
├── api-node.tsx (200 tokens)
├── base-handle.tsx (200 tokens)
├── base-node.tsx (400 tokens)
├── cron-node.tsx (200 tokens)
├── event-node.tsx (100 tokens)
├── node-header.tsx (600 tokens)
├── noop-node.tsx (100 tokens)
├── queue-node.tsx (200 tokens)
├── state-node.tsx (100 tokens)
├── trigger-icon.tsx (100 tokens)
├── trigger-list.tsx (300 tokens)
├── use-flow-state.ts (900 tokens)
├── keyboard-shortcut-overlay.tsx (300 tokens)
├── layout/
├── Sidebar.tsx (2.5k tokens)
├── queues/
├── QueueDlqTab.test.ts (500 tokens)
├── QueueDlqTab.tsx (3.5k tokens)
├── QueueOverviewTab.tsx (1100 tokens)
├── dlq-error.ts (500 tokens)
├── traces/
├── AttributesFilter.tsx (1100 tokens)
├── FlameGraph.tsx (4.7k tokens)
├── FlowView.tsx (3.1k tokens)
├── ServiceBreakdown.tsx (1000 tokens)
├── SpanBaggageTab.tsx (600 tokens)
├── SpanErrorsTab.tsx (1100 tokens)
├── SpanInfoTab.tsx (1300 tokens)
├── SpanLinksTab.tsx (400 tokens)
├── SpanLogsTab.tsx (700 tokens)
├── SpanOtelLogsTab.tsx (2.1k tokens)
├── SpanPanel.tsx (2.2k tokens)
├── SpanTagsTab.tsx (1600 tokens)
├── TraceFilters.tsx (5.6k tokens)
├── TraceHeader.tsx (1500 tokens)
├── TraceMap.tsx (2.5k tokens)
├── ViewSwitcher.tsx (300 tokens)
├── WaterfallChart.tsx (3.3k tokens)
├── WorkflowChain.tsx (700 tokens)
├── ui/
├── button.tsx (300 tokens)
├── card.tsx (1300 tokens)
├── command.tsx (1000 tokens)
├── dialog.tsx (800 tokens)
├── empty-state.tsx (300 tokens)
├── error-boundary.tsx (200 tokens)
├── json-viewer.tsx (1100 tokens)
├── page-header.tsx (200 tokens)
├── pagination.tsx (700 tokens)
├── search-bar.tsx (200 tokens)
├── skeleton.tsx (100 tokens)
├── tabs.tsx (400 tokens)
├── tooltip.tsx (300 tokens)
├── hooks/
├── useKeyboard.ts (500 tokens)
├── useResizablePanels.ts (900 tokens)
├── useResizableSplitPane.ts (600 tokens)
├── useTheme.ts (300 tokens)
├── useTraceData.ts (700 tokens)
├── useTraceFilters.ts (1600 tokens)
├── lib/
├── otel-utils.ts (300 tokens)
├── timeRangeUtils.ts (200 tokens)
├── traceColors.ts (300 tokens)
├── traceGroups.test.ts (800 tokens)
├── traceGroups.ts (500 tokens)
├── traceTransform.ts (1800 tokens)
├── traceUtils.ts (500 tokens)
├── utils.ts
├── main.tsx (300 tokens)
├── routeTree.gen.ts (1600 tokens)
├── routes/
├── __root.tsx (600 tokens)
├── config.tsx (7.7k tokens)
├── dead-letter.tsx
├── flow.tsx
├── functions.tsx (4.2k tokens)
├── index.tsx (4.4k tokens)
├── logs.tsx (8.4k tokens)
├── queues.tsx (3.2k tokens)
├── states.tsx (7.2k tokens)
├── streams.tsx (7.6k tokens)
├── traces.tsx (4.4k tokens)
├── triggers.tsx (11k tokens)
├── workers.tsx (5k tokens)
├── styles/
├── globals.css (1000 tokens)
├── vite-env.d.ts (omitted)
├── tsconfig.json (100 tokens)
├── tsconfig.node.json
├── vite.config.ts (300 tokens)
├── vitest.config.ts (100 tokens)
├── console-rust/
├── Cargo.lock (omitted)
├── Cargo.toml (200 tokens)
├── build.rs (1000 tokens)
├── src/
├── bridge/
├── error.rs (200 tokens)
├── functions.rs (10.2k tokens)
├── mod.rs
├── triggers.rs (1000 tokens)
├── lib.rs
├── main.rs (1300 tokens)
├── proxy/
├── http.rs (800 tokens)
├── mod.rs
├── ws.rs (800 tokens)
├── server.rs (1600 tokens)
├── tests/
├── proxy_test.rs (1100 tokens)
├── contributors.md (100 tokens)
├── crates/
├── iii-clap-docs/
├── Cargo.toml (100 tokens)
├── src/
├── lib.rs (6.7k tokens)
├── iii-filesystem/
├── Cargo.toml (100 tokens)
├── build.rs (500 tokens)
├── src/
├── backends/
├── mod.rs
├── passthroughfs/
├── builder.rs (1400 tokens)
├── create_ops.rs (1300 tokens)
├── dir_ops.rs (2.4k tokens)
├── file_ops.rs (1300 tokens)
├── inode.rs (4.7k tokens)
├── metadata.rs (1300 tokens)
├── mod.rs (5.2k tokens)
├── remove_ops.rs (1300 tokens)
├── special.rs (600 tokens)
├── shared/
├── handle_table.rs (100 tokens)
├── init_binary.rs (1200 tokens)
├── inode_table.rs (1500 tokens)
├── mod.rs (100 tokens)
├── name_validation.rs (400 tokens)
├── platform.rs (5.2k tokens)
├── init.rs (200 tokens)
├── lib.rs (100 tokens)
├── tests/
├── filesystem_integration.rs (3.9k tokens)
├── iii-init/
├── Cargo.toml (300 tokens)
├── src/
├── child_exits.rs (1100 tokens)
├── error.rs (300 tokens)
├── fs_handler.rs (600 tokens)
├── fs_handler/
├── ops.rs (6.5k tokens)
├── streaming.rs (2.3k tokens)
├── tests.rs (7.6k tokens)
├── lib.rs (300 tokens)
├── main.rs (400 tokens)
├── mount.rs (3k tokens)
├── network.rs (2.3k tokens)
├── parse.rs (900 tokens)
├── rlimit.rs (500 tokens)
├── root_pivot.rs (6.3k tokens)
├── shell_dispatcher.rs (12.9k tokens)
├── supervisor.rs (3.5k tokens)
├── tests/
├── init_integration.rs (1100 tokens)
├── iii-network/
├── Cargo.toml (100 tokens)
├── src/
├── backend.rs (1300 tokens)
├── config.rs (100 tokens)
├── conn.rs (2.6k tokens)
├── device.rs (1700 tokens)
├── dns.rs (1600 tokens)
├── lib.rs (200 tokens)
├── network.rs (1100 tokens)
├── proxy.rs (1100 tokens)
├── shared.rs (700 tokens)
├── stack.rs (4.1k tokens)
├── udp_relay.rs (3.5k tokens)
├── wake_pipe.rs (800 tokens)
├── tests/
├── network_integration.rs (2.2k tokens)
├── iii-shell-client/
├── Cargo.toml (200 tokens)
├── src/
├── lib.rs (10.2k tokens)
├── tests/
├── fake_fs_relay.rs (4.1k tokens)
├── fake_relay.rs (4.1k tokens)
├── iii-shell-proto/
├── Cargo.toml (100 tokens)
├── src/
├── lib.rs (7.4k tokens)
├── iii-supervisor/
├── Cargo.toml (200 tokens)
├── src/
├── child.rs (6k tokens)
├── control.rs (2.2k tokens)
├── lib.rs (100 tokens)
├── protocol.rs (1200 tokens)
├── shell_protocol.rs (100 tokens)
├── tests/
├── socketpair_integration.rs (1200 tokens)
├── iii-worker/
├── Cargo.toml (700 tokens)
├── build.rs (300 tokens)
├── examples/
├── dump_worker_env.rs (200 tokens)
├── gen-golden.rs (300 tokens)
├── images/
├── node/
├── Dockerfile (100 tokens)
├── README.md (300 tokens)
├── python/
├── Dockerfile (200 tokens)
├── README.md (300 tokens)
├── src/
├── cli/
├── app.rs (4.2k tokens)
├── binary_download.rs (4.7k tokens)
├── builtin_defaults.rs (2.4k tokens)
├── bundle_download.rs (16.2k tokens)
├── config_file.rs (13.3k tokens)
├── download.rs (5.1k tokens)
├── erofs/
├── builder.rs (2.8k tokens)
├── crc32c.rs (300 tokens)
├── format.rs (700 tokens)
├── mod.rs (400 tokens)
├── reader.rs (7.3k tokens)
├── tree.rs (6.9k tokens)
├── writer.rs (9.1k tokens)
├── firmware/
├── constants.rs (1200 tokens)
├── download.rs (2.4k tokens)
├── libkrunfw_bytes.rs (100 tokens)
├── mod.rs (200 tokens)
├── resolve.rs (1600 tokens)
├── symlinks.rs (500 tokens)
├── gen_docs.rs (300 tokens)
├── host_shim.rs (10.2k tokens)
├── init.rs (3.9k tokens)
├── lifecycle.rs (800 tokens)
├── local_worker.rs (16.6k tokens)
├── lockfile.rs (7.9k tokens)
├── managed.rs (51.9k tokens)
├── mod.rs (200 tokens)
├── oci_ref.rs (700 tokens)
├── overlay.rs (1500 tokens)
├── pidfile.rs (2.2k tokens)
├── project.rs (10.7k tokens)
├── registry.rs (7.4k tokens)
├── rootfs.rs (500 tokens)
├── rootfs_cache.rs (1600 tokens)
├── sandbox.rs (5k tokens)
├── sandbox_daemon.rs (300 tokens)
├── shell_client.rs (7.1k tokens)
├── shell_relay.rs (11.8k tokens)
├── source_watcher.rs (10.1k tokens)
├── spinner.rs (1100 tokens)
├── status.rs (15.5k tokens)
├── stderr_sink.rs (300 tokens)
├── supervisor_ctl.rs (3.5k tokens)
├── sync.rs (3.4k tokens)
├── test_support.rs (400 tokens)
├── upper.rs (2.4k tokens)
├── vm_boot.rs (11.4k tokens)
├── worker_manager/
├── adapter.rs (200 tokens)
├── libkrun.rs (8.3k tokens)
├── mod.rs (100 tokens)
├── oci.rs (12k tokens)
├── platform.rs (700 tokens)
├── state.rs (1000 tokens)
├── worker_manager_daemon.rs (8.7k tokens)
├── worker_manifest.rs (8.7k tokens)
├── worker_manifest_deps.rs (1100 tokens)
├── worker_trigger.rs (5.4k tokens)
├── core/
├── add.rs (2.2k tokens)
├── clear.rs (700 tokens)
├── error.rs (4k tokens)
├── events.rs (600 tokens)
├── host.rs (1000 tokens)
├── list.rs (600 tokens)
├── logs.rs (3k tokens)
├── mod.rs (200 tokens)
├── project.rs (1600 tokens)
├── remove.rs (1000 tokens)
├── start.rs (900 tokens)
├── stop.rs (900 tokens)
├── types.rs (3.6k tokens)
├── update.rs (800 tokens)
├── daemon_exit.rs (6.2k tokens)
├── lib.rs (200 tokens)
├── main.rs (5.4k tokens)
├── sandbox_daemon/
├── README.md (4.4k tokens)
├── adapters.rs (6.7k tokens)
├── auto_install.rs (500 tokens)
├── catalog.rs (2.3k tokens)
├── config.rs (1800 tokens)
├── create.rs (2.3k tokens)
├── errors.rs (5.7k tokens)
├── events.rs (300 tokens)
├── exec.rs (3.8k tokens)
├── fs/
├── adapter.rs (2.5k tokens)
├── chmod.rs (1400 tokens)
├── grep.rs (1700 tokens)
├── ls.rs (1200 tokens)
├── mkdir.rs (1300 tokens)
├── mod.rs (300 tokens)
├── mv.rs (1300 tokens)
├── read.rs (2.7k tokens)
├── rm.rs (1200 tokens)
├── sed.rs (2k tokens)
├── stat.rs (1400 tokens)
├── write.rs (4.4k tokens)
├── iii.worker.yaml (100 tokens)
├── list.rs (700 tokens)
├── mod.rs (3.7k tokens)
├── overlay.rs (500 tokens)
├── reaper.rs (600 tokens)
├── registry.rs (1300 tokens)
├── run.rs (3k tokens)
├── skills/
├── SKILL.md (1400 tokens)
├── stop.rs (1200 tokens)
├── tests/
├── README.md (800 tokens)
├── binary_worker_integration.rs (3.1k tokens)
├── bundle_worker_integration.rs (11.8k tokens)
├── common/
├── assertions.rs (200 tokens)
├── fixtures.rs (600 tokens)
├── isolation.rs (300 tokens)
├── mod.rs (100 tokens)
├── sandbox_fakes.rs (2k tokens)
├── common_fixtures_integration.rs (700 tokens)
├── config_clear_integration.rs (500 tokens)
├── config_crossplatform_integration.rs (1200 tokens)
├── config_crud_integration.rs (2k tokens)
├── config_force_integration.rs (700 tokens)
├── config_managed_integration.rs (4.8k tokens)
├── config_path_type_integration.rs (1000 tokens)
├── daemon_orphan_exit_integration.rs (4.4k tokens)
├── engine_death_cascade_integration.rs (5.9k tokens)
├── fake_sandbox_relay.rs (1900 tokens)
├── firmware_integration.rs (2.6k tokens)
├── fixtures/
├── sandbox_error_codes.json (700 tokens)
├── templates/
├── default-gitignore
├── template.yaml (100 tokens)
├── worker-bare/
├── Cargo.toml
├── iii.worker.js.yaml
├── iii.worker.py.yaml
├── iii.worker.rust.yaml
├── iii.worker.ts.yaml
├── package.js.json
├── package.ts.json (100 tokens)
├── pyproject.toml
├── src/
├── index.js (100 tokens)
├── index.ts (100 tokens)
├── main.py (100 tokens)
├── main.rs (200 tokens)
├── template.yaml (300 tokens)
├── tsconfig.json
├── local_add_dependencies_integration.rs (1600 tokens)
├── local_add_manifest_validation_integration.rs (1700 tokens)
├── local_worker_integration.rs (6.3k tokens)
├── oci_gate_smoke.rs (100 tokens)
├── oci_worker_integration.rs (3.7k tokens)
├── project_detection_integration.rs (2.4k tokens)
├── sandbox_docs_anchor_stability.rs (600 tokens)
├── sandbox_error_codes_integration.rs (1500 tokens)
├── sandbox_fs_decode_error_wire.rs (500 tokens)
├── sandbox_fs_integration.rs (7.9k tokens)
├── sandbox_fs_read_buffer_boundary.rs (1300 tokens)
├── sandbox_integration_e2e.rs (1000 tokens)
├── sandbox_lifecycle_integration.rs (3.7k tokens)
├── sandbox_workflow_integration.rs (1300 tokens)
├── sync_drift_adversarial.rs (2.9k tokens)
├── vm_args_integration.rs (4.3k tokens)
├── vm_gate_smoke.rs (100 tokens)
├── vm_integration.rs (3.2k tokens)
├── vm_lifecycle_integration.rs (1100 tokens)
├── worker_init_e2e.rs (2.6k tokens)
├── worker_integration.rs (3.5k tokens)
├── worker_list_discovery_integration.rs (4k tokens)
├── worker_manager_integration.rs (2.3k tokens)
├── worker_trigger_integration.rs (6.8k tokens)
├── vendor/
├── .gitignore
├── regen-golden.sh (400 tokens)
├── test-regen-golden.sh (300 tokens)
├── upper-golden.iiu.gz
├── motia-tools/
├── Cargo.toml (100 tokens)
├── src/
├── main.rs (800 tokens)
├── scaffolder-core/
├── Cargo.toml (200 tokens)
├── src/
├── cli.rs (1100 tokens)
├── iii.rs (400 tokens)
├── lib.rs (200 tokens)
├── product.rs (300 tokens)
├── runtime/
├── check.rs (1200 tokens)
├── mod.rs (100 tokens)
├── tool.rs (1400 tokens)
├── telemetry.rs (5.2k tokens)
├── templates/
├── copier.rs (1700 tokens)
├── fetcher.rs (2.3k tokens)
├── manifest.rs (1800 tokens)
├── mod.rs (100 tokens)
├── version.rs (1500 tokens)
├── tui/
├── mod.rs
├── prompts.rs (4.2k tokens)
├── tests/
├── cli_helpers.rs (400 tokens)
├── fetcher.rs (1700 tokens)
├── docs/
├── .gitignore
├── .mintignore
├── .prettierrc (100 tokens)
├── 0-10-0/
├── advanced/
├── adapters.mdx (1000 tokens)
├── architecture.mdx (1700 tokens)
├── custom-modules.mdx (4.5k tokens)
├── deployment.mdx (1800 tokens)
├── protocol.mdx (800 tokens)
├── telemetry.mdx (2.2k tokens)
├── api-reference/
├── disable-telemetry.mdx (200 tokens)
├── sdk-node.mdx (5.5k tokens)
├── sdk-python.mdx (5.2k tokens)
├── sdk-rust.mdx (5.2k tokens)
├── console/
├── index.mdx (3.6k tokens)
├── examples/
├── conditions.mdx (2.2k tokens)
├── cron.mdx (2k tokens)
├── hello-world.mdx (2k tokens)
├── multi-trigger.mdx (2.6k tokens)
├── observability.mdx (2.9k tokens)
├── state-management.mdx (2.7k tokens)
├── todo-app.mdx (3.4k tokens)
├── how-to/
├── configure-engine.mdx (5.3k tokens)
├── create-custom-trigger-type.mdx (1800 tokens)
├── create-ephemeral-worker.mdx (1100 tokens)
├── dead-letter-queues.mdx (1100 tokens)
├── define-request-response-formats.mdx (1400 tokens)
├── expose-http-endpoint.mdx (800 tokens)
├── manage-state.mdx (2000 tokens)
├── react-to-state-changes.mdx (1000 tokens)
├── schedule-cron-task.mdx (700 tokens)
├── stream-realtime-data.mdx (1600 tokens)
├── trigger-actions.mdx (7.1k tokens)
├── trigger-functions-from-cli.mdx (1300 tokens)
├── use-channels.mdx (4.7k tokens)
├── use-functions-and-triggers.mdx (6.4k tokens)
├── use-queues.mdx (7.1k tokens)
├── use-trigger-conditions.mdx (800 tokens)
├── index.mdx (700 tokens)
├── modules/
├── index.mdx (500 tokens)
├── module-bridge.mdx (800 tokens)
├── module-cron.mdx (1100 tokens)
├── module-exec.mdx (400 tokens)
├── module-http.mdx (1700 tokens)
├── module-observability.mdx (4.2k tokens)
├── module-pubsub.mdx (1700 tokens)
├── module-queue.mdx (2.3k tokens)
├── module-state.mdx (3.2k tokens)
├── module-stream.mdx (4.6k tokens)
├── primitives-and-concepts/
├── discovery.mdx (600 tokens)
├── functions-triggers-workers.mdx (500 tokens)
├── quickstart.mdx (500 tokens)
├── tutorials/
├── index.mdx
├── 0-11-0/
├── advanced/
├── adapters.mdx (800 tokens)
├── architecture.mdx (1700 tokens)
├── custom-modules.mdx (4.5k tokens)
├── deployment.mdx (1700 tokens)
├── protocol.mdx (800 tokens)
├── telemetry.mdx (2.2k tokens)
├── api-reference/
├── disable-telemetry.mdx (200 tokens)
├── sandbox.mdx (12.2k tokens)
├── sdk-browser.mdx (3.3k tokens)
├── sdk-node.mdx (5.3k tokens)
├── sdk-python.mdx (4.6k tokens)
├── sdk-rust.mdx (4.4k tokens)
├── architecture/
├── channels.mdx (4.8k tokens)
├── engine.mdx (1400 tokens)
├── external-workers.mdx (1200 tokens)
├── index.mdx (500 tokens)
├── queues.mdx (2.7k tokens)
├── trigger-types.mdx (8.2k tokens)
├── workers.mdx (800 tokens)
├── assets/
├── console-config-overview.png
├── console-functions-invoke.png
├── console-logs-detail.png
├── console-queues-overview.png
├── console-states-detail.png
├── console-traces-flame-graph.png
├── console-traces-flow.png
├── console-traces-map.png
├── console-traces-waterfall.png
├── console-triggers-http-test.png
├── console-workers-detail.png
├── console-workers-list.png
├── context7.json
├── todo-app.png
├── console/
├── index.mdx (4.3k tokens)
├── examples/
├── conditions.mdx (2.2k tokens)
├── cron.mdx (2.1k tokens)
├── hello-world.mdx (2000 tokens)
├── multi-trigger.mdx (2.7k tokens)
├── observability.mdx (3k tokens)
├── state-management.mdx (2.9k tokens)
├── todo-app.mdx (2.6k tokens)
├── how-to/
├── configure-engine.mdx (5.2k tokens)
├── create-custom-trigger-type.mdx (1800 tokens)
├── create-ephemeral-worker.mdx (1100 tokens)
├── dead-letter-queues.mdx (1100 tokens)
├── define-request-response-formats.mdx (1400 tokens)
├── developing-sandbox-workers.mdx (1900 tokens)
├── expose-http-endpoint.mdx (800 tokens)
├── manage-state.mdx (2.8k tokens)
├── managing-container-workers.mdx (1900 tokens)
├── observability-and-logs.mdx (2.5k tokens)
├── react-to-state-changes.mdx (900 tokens)
├── reproduce-worker-installs.mdx (900 tokens)
├── schedule-cron-task.mdx (700 tokens)
├── stream-realtime-data.mdx (2.3k tokens)
├── trigger-actions.mdx (6.9k tokens)
├── trigger-functions-from-cli.mdx (1300 tokens)
├── use-channels.mdx (4.6k tokens)
├── use-console.mdx (1200 tokens)
├── use-functions-and-triggers.mdx (6.5k tokens)
├── use-http-middleware.mdx (1400 tokens)
├── use-iii-in-the-browser.mdx (1400 tokens)
├── use-named-queues.mdx (5.2k tokens)
├── use-topic-queues.mdx (3k tokens)
├── use-trigger-conditions.mdx (800 tokens)
├── worker-rbac.mdx (4.6k tokens)
├── images/
├── architecture-dark.png
├── architecture-light.png
├── console/
├── config-overview-dark.png
├── config-overview-light.png
├── config-overview.png
├── console-logs.png
├── dashboard-dark.png
├── dashboard-light.png
├── dashboard.png
├── flow-view-dark.png
├── flow-view-light.png
├── flow-view.png
├── functions-dark.png
├── functions-invoke-dark.png
├── functions-invoke-light.png
├── functions-invoke.png
├── functions-light.png
├── functions.png
├── logs-dark.png
├── logs-detail-dark.png
├── logs-detail-light.png
├── logs-detail.png
├── logs-light.png
├── logs-list-dark.png
├── logs-list-light.png
├── logs-list.png
├── logs.png
├── states-dark.png
├── states-detail-dark.png
├── states-detail-light.png
├── states-detail.png
├── states-light.png
├── states.png
├── streams-dark.png
├── streams-detail-dark.png
├── streams-detail-light.png
├── streams-detail.png
├── streams-light.png
├── streams.png
├── traces-dark.png
├── traces-detail-dark.png
├── traces-detail-light.png
├── traces-detail.png
├── traces-light.png
├── traces-waterfall-dark.png
├── traces-waterfall-light.png
├── traces-waterfall.png
├── traces.png
├── triggers-dark.png
├── triggers-detail-dark.png
├── triggers-detail-light.png
├── triggers-detail.png
├── triggers-light.png
├── triggers.png
├── favicon/
├── apple-touch-icon.png
├── favicon-96x96.png
├── favicon.ico
├── favicon.svg (3.6k tokens)
├── site.webmanifest (100 tokens)
├── web-app-manifest-192x192.png
├── web-app-manifest-512x512.png
├── icons/
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── lsp.mp4
├── index.mdx (800 tokens)
├── install.mdx (400 tokens)
├── primitives-and-concepts/
├── discovery.mdx (700 tokens)
├── functions-triggers-workers.mdx (500 tokens)
├── quickstart.mdx (1900 tokens)
├── scripts/
├── generate-api-docs.mts (700 tokens)
├── parsers/
├── __tests__/
├── parse-griffe.test.mts (7.2k tokens)
├── parse-rustdoc.test.mts (8.7k tokens)
├── parse-griffe.mts (2.1k tokens)
├── parse-rustdoc.mts (4k tokens)
├── parse-typedoc.mts (1800 tokens)
├── renderers/
├── __tests__/
├── components.test.mts (700 tokens)
├── render-mdx.test.mts (700 tokens)
├── components.mts (700 tokens)
├── render-mdx.mts (900 tokens)
├── types.mts (200 tokens)
├── tutorials/
├── index.mdx
├── workers/
├── iii-bridge.mdx (800 tokens)
├── iii-cron.mdx (1200 tokens)
├── iii-exec.mdx (400 tokens)
├── iii-http.mdx (1800 tokens)
├── iii-observability.mdx (4.2k tokens)
├── iii-pubsub.mdx (1700 tokens)
├── iii-queue.mdx (3.4k tokens)
├── iii-state.mdx (4.7k tokens)
├── iii-stream.mdx (5.9k tokens)
├── iii-worker-manager.mdx (3.3k tokens)
├── index.mdx (400 tokens)
├── managed-worker-lockfile.mdx (1000 tokens)
├── worker-management-triggers.mdx (1600 tokens)
├── 0-12-0/
├── assets/
├── favicon.svg (3.6k tokens)
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── creating-workers/
├── functions.mdx (1300 tokens)
├── functions.mdx.skill.md (1300 tokens)
├── triggers.mdx (1400 tokens)
├── triggers.mdx.skill.md (1400 tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (400 tokens)
├── workers.mdx (1100 tokens)
├── workers.mdx.skill.md (1100 tokens)
├── how-to/
├── build-a-realtime-todo-app.mdx (300 tokens)
├── build-a-realtime-todo-app.mdx.skill.md (300 tokens)
├── index.mdx (1000 tokens)
├── index.mdx.skill.md (1000 tokens)
├── install.mdx (600 tokens)
├── install.mdx.skill.md (600 tokens)
├── patterns/
├── adapter-pattern.mdx (200 tokens)
├── adapter-pattern.mdx.skill.md (200 tokens)
├── reactive-state-pattern.mdx (300 tokens)
├── reactive-state-pattern.mdx.skill.md (300 tokens)
├── quickstart.mdx (1400 tokens)
├── quickstart.mdx.skill.md (1400 tokens)
├── sdk-reference/
├── browser-sdk.mdx (900 tokens)
├── browser-sdk.mdx.skill.md (900 tokens)
├── engine-sdk.mdx (2.2k tokens)
├── engine-sdk.mdx.skill.md (2.2k tokens)
├── node-sdk.mdx (1100 tokens)
├── node-sdk.mdx.skill.md (1100 tokens)
├── python-sdk.mdx (1200 tokens)
├── python-sdk.mdx.skill.md (1200 tokens)
├── rust-sdk.mdx (1100 tokens)
├── rust-sdk.mdx.skill.md (1100 tokens)
├── tutorials/
├── build-an-agent/
├── define-tools.mdx
├── define-tools.mdx.skill.md
├── memory-and-state.mdx
├── memory-and-state.mdx.skill.md
├── orchestration-loop.mdx
├── orchestration-loop.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (100 tokens)
├── incremental-adoption/
├── migrate-persistence.mdx
├── migrate-persistence.mdx.skill.md
├── offload-to-queue.mdx
├── offload-to-queue.mdx.skill.md
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── wrap-existing-api.mdx
├── wrap-existing-api.mdx.skill.md
├── reactive-crud/
├── crud-endpoints.mdx
├── crud-endpoints.mdx.skill.md
├── model-and-store.mdx
├── model-and-store.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (200 tokens)
├── realtime-subscriptions.mdx
├── realtime-subscriptions.mdx.skill.md
├── understanding-iii/
├── channels.mdx (600 tokens)
├── channels.mdx.skill.md (700 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── functions.mdx (500 tokens)
├── functions.mdx.skill.md (500 tokens)
├── index.mdx (1500 tokens)
├── index.mdx.skill.md (1500 tokens)
├── triggers.mdx (900 tokens)
├── triggers.mdx.skill.md (900 tokens)
├── workers.mdx (300 tokens)
├── workers.mdx.skill.md (300 tokens)
├── using-iii/
├── channels.mdx (1100 tokens)
├── channels.mdx.skill.md (1100 tokens)
├── cli.mdx (600 tokens)
├── cli.mdx.skill.md (600 tokens)
├── console.mdx (1500 tokens)
├── console.mdx.skill.md (1500 tokens)
├── deployment.mdx (700 tokens)
├── deployment.mdx.skill.md (700 tokens)
├── engine.mdx (500 tokens)
├── engine.mdx.skill.md (500 tokens)
├── functions.mdx (600 tokens)
├── functions.mdx.skill.md (600 tokens)
├── triggers.mdx (1200 tokens)
├── triggers.mdx.skill.md (1200 tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (300 tokens)
├── workers.mdx (1400 tokens)
├── workers.mdx.skill.md (1300 tokens)
├── 0-13-0/
├── assets/
├── favicon.svg (3.6k tokens)
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── creating-workers/
├── functions.mdx (1300 tokens)
├── functions.mdx.skill.md (1300 tokens)
├── triggers.mdx (1400 tokens)
├── triggers.mdx.skill.md (1400 tokens)
├── workers-registry.mdx (1200 tokens)
├── workers-registry.mdx.skill.md (1200 tokens)
├── workers.mdx (1100 tokens)
├── workers.mdx.skill.md (1100 tokens)
├── how-to/
├── build-a-realtime-todo-app.mdx (300 tokens)
├── build-a-realtime-todo-app.mdx.skill.md (300 tokens)
├── index.mdx (1000 tokens)
├── index.mdx.skill.md (1000 tokens)
├── install.mdx (600 tokens)
├── install.mdx.skill.md (600 tokens)
├── patterns/
├── adapter-pattern.mdx (200 tokens)
├── adapter-pattern.mdx.skill.md (200 tokens)
├── reactive-state-pattern.mdx (300 tokens)
├── reactive-state-pattern.mdx.skill.md (300 tokens)
├── quickstart.mdx (1400 tokens)
├── quickstart.mdx.skill.md (1400 tokens)
├── sdk-reference/
├── browser-sdk.mdx (900 tokens)
├── browser-sdk.mdx.skill.md (900 tokens)
├── engine-sdk.mdx (2.2k tokens)
├── engine-sdk.mdx.skill.md (2.2k tokens)
├── node-sdk.mdx (1100 tokens)
├── node-sdk.mdx.skill.md (1100 tokens)
├── python-sdk.mdx (1200 tokens)
├── python-sdk.mdx.skill.md (1200 tokens)
├── rust-sdk.mdx (1100 tokens)
├── rust-sdk.mdx.skill.md (1100 tokens)
├── tutorials/
├── build-an-agent/
├── define-tools.mdx
├── define-tools.mdx.skill.md
├── memory-and-state.mdx
├── memory-and-state.mdx.skill.md
├── orchestration-loop.mdx
├── orchestration-loop.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (100 tokens)
├── incremental-adoption/
├── migrate-persistence.mdx
├── migrate-persistence.mdx.skill.md
├── offload-to-queue.mdx
├── offload-to-queue.mdx.skill.md
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── wrap-existing-api.mdx
├── wrap-existing-api.mdx.skill.md
├── reactive-crud/
├── crud-endpoints.mdx
├── crud-endpoints.mdx.skill.md
├── model-and-store.mdx
├── model-and-store.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (200 tokens)
├── realtime-subscriptions.mdx
├── realtime-subscriptions.mdx.skill.md
├── understanding-iii/
├── channels.mdx (600 tokens)
├── channels.mdx.skill.md (700 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── functions.mdx (500 tokens)
├── functions.mdx.skill.md (500 tokens)
├── index.mdx (1500 tokens)
├── index.mdx.skill.md (1500 tokens)
├── triggers.mdx (1200 tokens)
├── triggers.mdx.skill.md (1200 tokens)
├── workers.mdx (300 tokens)
├── workers.mdx.skill.md (300 tokens)
├── using-iii/
├── channels.mdx (1100 tokens)
├── channels.mdx.skill.md (1100 tokens)
├── cli.mdx (600 tokens)
├── cli.mdx.skill.md (600 tokens)
├── console.mdx (1500 tokens)
├── console.mdx.skill.md (1500 tokens)
├── deployment.mdx (700 tokens)
├── deployment.mdx.skill.md (700 tokens)
├── engine.mdx (500 tokens)
├── engine.mdx.skill.md (500 tokens)
├── functions.mdx (600 tokens)
├── functions.mdx.skill.md (600 tokens)
├── triggers.mdx (1200 tokens)
├── triggers.mdx.skill.md (1200 tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (300 tokens)
├── workers.mdx (1400 tokens)
├── workers.mdx.skill.md (1300 tokens)
├── 0-16-0/
├── assets/
├── favicon.svg (3.6k tokens)
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── creating-workers/
├── channels.mdx (2.1k tokens)
├── channels.mdx.skill.md (2.1k tokens)
├── functions.mdx (2.5k tokens)
├── functions.mdx.skill.md (2.5k tokens)
├── index.mdx (300 tokens)
├── index.mdx.skill.md (300 tokens)
├── triggers.mdx (3.7k tokens)
├── triggers.mdx.skill.md (3.7k tokens)
├── workers-registry.mdx (1200 tokens)
├── workers-registry.mdx.skill.md (1200 tokens)
├── workers.mdx (3.5k tokens)
├── workers.mdx.skill.md (3.5k tokens)
├── how-to/
├── build-a-realtime-todo-app.mdx (300 tokens)
├── build-a-realtime-todo-app.mdx.skill.md (300 tokens)
├── schedule-cron-task.mdx (100 tokens)
├── schedule-cron-task.mdx.skill.md (100 tokens)
├── use-trigger-conditions.mdx (100 tokens)
├── use-trigger-conditions.mdx.skill.md (100 tokens)
├── index.mdx (900 tokens)
├── index.mdx.skill.md (900 tokens)
├── install.mdx (500 tokens)
├── install.mdx.skill.md (500 tokens)
├── patterns/
├── adapter-pattern.mdx (200 tokens)
├── adapter-pattern.mdx.skill.md (200 tokens)
├── reactive-state-pattern.mdx (300 tokens)
├── reactive-state-pattern.mdx.skill.md (300 tokens)
├── quickstart.mdx (1400 tokens)
├── quickstart.mdx.skill.md (1400 tokens)
├── sdk-reference/
├── browser-sdk.mdx (900 tokens)
├── browser-sdk.mdx.skill.md (900 tokens)
├── engine-sdk.mdx (2.2k tokens)
├── engine-sdk.mdx.skill.md (2.2k tokens)
├── node-sdk.mdx (1100 tokens)
├── node-sdk.mdx.skill.md (1100 tokens)
├── python-sdk.mdx (1200 tokens)
├── python-sdk.mdx.skill.md (1200 tokens)
├── rust-sdk.mdx (1200 tokens)
├── rust-sdk.mdx.skill.md (1200 tokens)
├── tutorials/
├── build-an-agent/
├── define-tools.mdx
├── define-tools.mdx.skill.md
├── memory-and-state.mdx
├── memory-and-state.mdx.skill.md
├── orchestration-loop.mdx
├── orchestration-loop.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (100 tokens)
├── incremental-adoption/
├── migrate-persistence.mdx
├── migrate-persistence.mdx.skill.md
├── offload-to-queue.mdx
├── offload-to-queue.mdx.skill.md
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── wrap-existing-api.mdx
├── wrap-existing-api.mdx.skill.md
├── reactive-crud/
├── crud-endpoints.mdx
├── crud-endpoints.mdx.skill.md
├── model-and-store.mdx
├── model-and-store.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (200 tokens)
├── realtime-subscriptions.mdx
├── realtime-subscriptions.mdx.skill.md
├── understanding-iii/
├── channels.mdx (500 tokens)
├── channels.mdx.skill.md (500 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── index.mdx (2.9k tokens)
├── index.mdx.skill.md (2.9k tokens)
├── using-iii/
├── cli.mdx (600 tokens)
├── cli.mdx.skill.md (600 tokens)
├── console.mdx (1500 tokens)
├── console.mdx.skill.md (1500 tokens)
├── deployment.mdx (700 tokens)
├── deployment.mdx.skill.md (700 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── functions.mdx (1400 tokens)
├── functions.mdx.skill.md (1400 tokens)
├── triggers.mdx (2.3k tokens)
├── triggers.mdx.skill.md (2.3k tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (300 tokens)
├── workers.mdx (1500 tokens)
├── workers.mdx.skill.md (1400 tokens)
├── 0-17-0/
├── assets/
├── favicon.svg (3.6k tokens)
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── creating-workers/
├── channels.mdx (2000 tokens)
├── channels.mdx.skill.md (2000 tokens)
├── functions.mdx (2.5k tokens)
├── functions.mdx.skill.md (2.5k tokens)
├── index.mdx (300 tokens)
├── index.mdx.skill.md (300 tokens)
├── triggers.mdx (3.7k tokens)
├── triggers.mdx.skill.md (3.7k tokens)
├── workers-registry.mdx (1200 tokens)
├── workers-registry.mdx.skill.md (1200 tokens)
├── workers.mdx (3.5k tokens)
├── workers.mdx.skill.md (3.5k tokens)
├── how-to/
├── build-a-realtime-todo-app.mdx (300 tokens)
├── build-a-realtime-todo-app.mdx.skill.md (300 tokens)
├── schedule-cron-task.mdx (100 tokens)
├── schedule-cron-task.mdx.skill.md (100 tokens)
├── use-trigger-conditions.mdx (100 tokens)
├── use-trigger-conditions.mdx.skill.md (100 tokens)
├── index.mdx (900 tokens)
├── index.mdx.skill.md (900 tokens)
├── install.mdx (500 tokens)
├── install.mdx.skill.md (500 tokens)
├── patterns/
├── adapter-pattern.mdx (200 tokens)
├── adapter-pattern.mdx.skill.md (200 tokens)
├── reactive-state-pattern.mdx (300 tokens)
├── reactive-state-pattern.mdx.skill.md (300 tokens)
├── quickstart.mdx (1400 tokens)
├── quickstart.mdx.skill.md (1400 tokens)
├── sdk-reference/
├── browser-sdk.mdx (900 tokens)
├── browser-sdk.mdx.skill.md (900 tokens)
├── engine-sdk.mdx (2.2k tokens)
├── engine-sdk.mdx.skill.md (2.2k tokens)
├── node-sdk.mdx (1100 tokens)
├── node-sdk.mdx.skill.md (1100 tokens)
├── python-sdk.mdx (1200 tokens)
├── python-sdk.mdx.skill.md (1200 tokens)
├── rust-sdk.mdx (1100 tokens)
├── rust-sdk.mdx.skill.md (1100 tokens)
├── tutorials/
├── build-an-agent/
├── define-tools.mdx
├── define-tools.mdx.skill.md
├── memory-and-state.mdx
├── memory-and-state.mdx.skill.md
├── orchestration-loop.mdx
├── orchestration-loop.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (100 tokens)
├── incremental-adoption/
├── migrate-persistence.mdx
├── migrate-persistence.mdx.skill.md
├── offload-to-queue.mdx
├── offload-to-queue.mdx.skill.md
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── wrap-existing-api.mdx
├── wrap-existing-api.mdx.skill.md
├── reactive-crud/
├── crud-endpoints.mdx
├── crud-endpoints.mdx.skill.md
├── model-and-store.mdx
├── model-and-store.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (200 tokens)
├── realtime-subscriptions.mdx
├── realtime-subscriptions.mdx.skill.md
├── understanding-iii/
├── channels.mdx (500 tokens)
├── channels.mdx.skill.md (500 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── index.mdx (2.9k tokens)
├── index.mdx.skill.md (2.9k tokens)
├── using-iii/
├── cli.mdx (600 tokens)
├── cli.mdx.skill.md (600 tokens)
├── console.mdx (1500 tokens)
├── console.mdx.skill.md (1500 tokens)
├── deployment.mdx (700 tokens)
├── deployment.mdx.skill.md (700 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── functions.mdx (1400 tokens)
├── functions.mdx.skill.md (1400 tokens)
├── triggers.mdx (2.3k tokens)
├── triggers.mdx.skill.md (2.3k tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (300 tokens)
├── workers.mdx (1500 tokens)
├── workers.mdx.skill.md (1400 tokens)
├── 0-18-0/
├── assets/
├── favicon.svg (3.6k tokens)
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── creating-workers/
├── channels.mdx (2000 tokens)
├── channels.mdx.skill.md (2000 tokens)
├── functions.mdx (2.5k tokens)
├── functions.mdx.skill.md (2.5k tokens)
├── index.mdx (300 tokens)
├── index.mdx.skill.md (300 tokens)
├── triggers.mdx (3.7k tokens)
├── triggers.mdx.skill.md (3.7k tokens)
├── workers-registry.mdx (1200 tokens)
├── workers-registry.mdx.skill.md (1200 tokens)
├── workers.mdx (3.5k tokens)
├── workers.mdx.skill.md (3.5k tokens)
├── how-to/
├── build-a-realtime-todo-app.mdx (300 tokens)
├── build-a-realtime-todo-app.mdx.skill.md (300 tokens)
├── schedule-cron-task.mdx (100 tokens)
├── schedule-cron-task.mdx.skill.md (100 tokens)
├── use-trigger-conditions.mdx (100 tokens)
├── use-trigger-conditions.mdx.skill.md (100 tokens)
├── index.mdx (1000 tokens)
├── index.mdx.skill.md (1000 tokens)
├── install.mdx (500 tokens)
├── install.mdx.skill.md (500 tokens)
├── patterns/
├── adapter-pattern.mdx (200 tokens)
├── adapter-pattern.mdx.skill.md (200 tokens)
├── reactive-state-pattern.mdx (300 tokens)
├── reactive-state-pattern.mdx.skill.md (300 tokens)
├── quickstart.mdx (1500 tokens)
├── quickstart.mdx.skill.md (1500 tokens)
├── sdk-reference/
├── browser-sdk.mdx (900 tokens)
├── browser-sdk.mdx.skill.md (900 tokens)
├── engine-sdk.mdx (2.2k tokens)
├── engine-sdk.mdx.skill.md (2.2k tokens)
├── node-sdk.mdx (1100 tokens)
├── node-sdk.mdx.skill.md (1100 tokens)
├── python-sdk.mdx (1200 tokens)
├── python-sdk.mdx.skill.md (1200 tokens)
├── rust-sdk.mdx (1100 tokens)
├── rust-sdk.mdx.skill.md (1100 tokens)
├── tutorials/
├── build-an-agent/
├── define-tools.mdx
├── define-tools.mdx.skill.md
├── memory-and-state.mdx
├── memory-and-state.mdx.skill.md
├── orchestration-loop.mdx
├── orchestration-loop.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (100 tokens)
├── incremental-adoption/
├── migrate-persistence.mdx
├── migrate-persistence.mdx.skill.md
├── offload-to-queue.mdx
├── offload-to-queue.mdx.skill.md
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── wrap-existing-api.mdx
├── wrap-existing-api.mdx.skill.md
├── linkly/
├── channels.mdx (800 tokens)
├── channels.mdx.skill.md (800 tokens)
├── console-traces.png
├── durable-execution.mdx (2.2k tokens)
├── durable-execution.mdx.skill.md (2.2k tokens)
├── foundations.mdx (2.5k tokens)
├── foundations.mdx.skill.md (2.5k tokens)
├── frontend.mdx (2.2k tokens)
├── frontend.mdx.skill.md (2.1k tokens)
├── observability.mdx (1400 tokens)
├── observability.mdx.skill.md (1400 tokens)
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── persistence.mdx (1500 tokens)
├── persistence.mdx.skill.md (1500 tokens)
├── streaming.mdx (800 tokens)
├── streaming.mdx.skill.md (800 tokens)
├── reactive-crud/
├── crud-endpoints.mdx
├── crud-endpoints.mdx.skill.md
├── model-and-store.mdx
├── model-and-store.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (200 tokens)
├── realtime-subscriptions.mdx
├── realtime-subscriptions.mdx.skill.md
├── understanding-iii/
├── channels.mdx (500 tokens)
├── channels.mdx.skill.md (500 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── index.mdx (2.9k tokens)
├── index.mdx.skill.md (2.9k tokens)
├── using-iii/
├── cli.mdx (600 tokens)
├── cli.mdx.skill.md (600 tokens)
├── console.mdx (1500 tokens)
├── console.mdx.skill.md (1500 tokens)
├── deployment.mdx (700 tokens)
├── deployment.mdx.skill.md (700 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── functions.mdx (1400 tokens)
├── functions.mdx.skill.md (1400 tokens)
├── triggers.mdx (2.3k tokens)
├── triggers.mdx.skill.md (2.3k tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (300 tokens)
├── workers.mdx (1500 tokens)
├── workers.mdx.skill.md (1400 tokens)
├── 0-19-0/
├── assets/
├── favicon.svg (3.6k tokens)
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── cli-reference/
├── index.mdx (5.1k tokens)
├── index.mdx.skill.md (5.1k tokens)
├── creating-workers/
├── channels.mdx (2000 tokens)
├── channels.mdx.skill.md (2000 tokens)
├── functions.mdx (2.5k tokens)
├── functions.mdx.skill.md (2.5k tokens)
├── index.mdx (300 tokens)
├── index.mdx.skill.md (300 tokens)
├── triggers.mdx (3.7k tokens)
├── triggers.mdx.skill.md (3.7k tokens)
├── worker-manifest.mdx (900 tokens)
├── worker-manifest.mdx.skill.md (900 tokens)
├── workers-registry.mdx (1200 tokens)
├── workers-registry.mdx.skill.md (1200 tokens)
├── workers.mdx (3.6k tokens)
├── workers.mdx.skill.md (3.6k tokens)
├── how-to/
├── build-a-realtime-todo-app.mdx (300 tokens)
├── build-a-realtime-todo-app.mdx.skill.md (300 tokens)
├── schedule-cron-task.mdx (100 tokens)
├── schedule-cron-task.mdx.skill.md (100 tokens)
├── use-trigger-conditions.mdx (100 tokens)
├── use-trigger-conditions.mdx.skill.md (100 tokens)
├── index.mdx (1000 tokens)
├── index.mdx.skill.md (1000 tokens)
├── install.mdx (500 tokens)
├── install.mdx.skill.md (500 tokens)
├── patterns/
├── adapter-pattern.mdx (200 tokens)
├── adapter-pattern.mdx.skill.md (200 tokens)
├── reactive-state-pattern.mdx (300 tokens)
├── reactive-state-pattern.mdx.skill.md (300 tokens)
├── quickstart.mdx (1500 tokens)
├── quickstart.mdx.skill.md (1500 tokens)
├── sdk-reference/
├── browser-sdk.mdx (900 tokens)
├── browser-sdk.mdx.skill.md (900 tokens)
├── engine-sdk.mdx (2.2k tokens)
├── engine-sdk.mdx.skill.md (2.2k tokens)
├── node-sdk.mdx (1200 tokens)
├── node-sdk.mdx.skill.md (1200 tokens)
├── python-sdk.mdx (1200 tokens)
├── python-sdk.mdx.skill.md (1200 tokens)
├── rust-sdk.mdx (1200 tokens)
├── rust-sdk.mdx.skill.md (1200 tokens)
├── troubleshooting.mdx (200 tokens)
├── troubleshooting.mdx.skill.md (200 tokens)
├── tutorials/
├── build-an-agent/
├── define-tools.mdx
├── define-tools.mdx.skill.md
├── memory-and-state.mdx
├── memory-and-state.mdx.skill.md
├── orchestration-loop.mdx
├── orchestration-loop.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (100 tokens)
├── incremental-adoption/
├── migrate-persistence.mdx
├── migrate-persistence.mdx.skill.md
├── offload-to-queue.mdx
├── offload-to-queue.mdx.skill.md
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── wrap-existing-api.mdx
├── wrap-existing-api.mdx.skill.md
├── linkly/
├── channels.mdx (800 tokens)
├── channels.mdx.skill.md (800 tokens)
├── console-traces.png
├── durable-execution.mdx (2.2k tokens)
├── durable-execution.mdx.skill.md (2.2k tokens)
├── foundations.mdx (2.5k tokens)
├── foundations.mdx.skill.md (2.5k tokens)
├── frontend.mdx (2.2k tokens)
├── frontend.mdx.skill.md (2.2k tokens)
├── observability.mdx (1200 tokens)
├── observability.mdx.skill.md (1200 tokens)
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── persistence.mdx (1600 tokens)
├── persistence.mdx.skill.md (1600 tokens)
├── streaming.mdx (900 tokens)
├── streaming.mdx.skill.md (900 tokens)
├── reactive-crud/
├── crud-endpoints.mdx
├── crud-endpoints.mdx.skill.md
├── model-and-store.mdx
├── model-and-store.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (200 tokens)
├── realtime-subscriptions.mdx
├── realtime-subscriptions.mdx.skill.md
├── understanding-iii/
├── channels.mdx (500 tokens)
├── channels.mdx.skill.md (500 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── index.mdx (2.9k tokens)
├── index.mdx.skill.md (2.9k tokens)
├── using-iii/
├── cli.mdx (600 tokens)
├── cli.mdx.skill.md (600 tokens)
├── console.mdx (1500 tokens)
├── console.mdx.skill.md (1500 tokens)
├── deployment.mdx (700 tokens)
├── deployment.mdx.skill.md (700 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── functions.mdx (1900 tokens)
├── functions.mdx.skill.md (1900 tokens)
├── triggers.mdx (1700 tokens)
├── triggers.mdx.skill.md (1700 tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (300 tokens)
├── workers.mdx (1800 tokens)
├── workers.mdx.skill.md (1700 tokens)
├── 0-20-0/
├── api-reference/
├── helpers-node.mdx (7.4k tokens)
├── helpers-node.mdx.skill.md (7.4k tokens)
├── helpers-python.mdx (5.4k tokens)
├── helpers-python.mdx.skill.md (5.4k tokens)
├── helpers-rust.mdx (4.9k tokens)
├── helpers-rust.mdx.skill.md (4.9k tokens)
├── sdk-browser.mdx (7.6k tokens)
├── sdk-browser.mdx.skill.md (7.6k tokens)
├── sdk-node.mdx (5.4k tokens)
├── sdk-node.mdx.skill.md (5.5k tokens)
├── sdk-python.mdx (4.4k tokens)
├── sdk-python.mdx.skill.md (4.4k tokens)
├── sdk-rust.mdx (5.6k tokens)
├── sdk-rust.mdx.skill.md (5.6k tokens)
├── assets/
├── favicon.svg (3.6k tokens)
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── cli-reference/
├── index.mdx (5.1k tokens)
├── index.mdx.skill.md (5.1k tokens)
├── creating-workers/
├── channels.mdx (2000 tokens)
├── channels.mdx.skill.md (2000 tokens)
├── functions.mdx (2.8k tokens)
├── functions.mdx.skill.md (2.8k tokens)
├── http.mdx (800 tokens)
├── http.mdx.skill.md (800 tokens)
├── index.mdx (500 tokens)
├── index.mdx.skill.md (500 tokens)
├── observability.mdx (1100 tokens)
├── observability.mdx.skill.md (1100 tokens)
├── queues.mdx (1500 tokens)
├── queues.mdx.skill.md (1500 tokens)
├── sandboxes.mdx (900 tokens)
├── sandboxes.mdx.skill.md (900 tokens)
├── triggers.mdx (3.7k tokens)
├── triggers.mdx.skill.md (3.7k tokens)
├── worker-manager.mdx (2.4k tokens)
├── worker-manager.mdx.skill.md (2.4k tokens)
├── worker-manifest.mdx (1000 tokens)
├── worker-manifest.mdx.skill.md (1000 tokens)
├── workers-registry.mdx (1200 tokens)
├── workers-registry.mdx.skill.md (1200 tokens)
├── workers.mdx (3.8k tokens)
├── workers.mdx.skill.md (3.8k tokens)
├── how-to/
├── build-a-realtime-todo-app.mdx (300 tokens)
├── build-a-realtime-todo-app.mdx.skill.md (300 tokens)
├── schedule-cron-task.mdx (100 tokens)
├── schedule-cron-task.mdx.skill.md (100 tokens)
├── use-trigger-conditions.mdx (100 tokens)
├── use-trigger-conditions.mdx.skill.md (100 tokens)
├── index.mdx (1000 tokens)
├── install.mdx (500 tokens)
├── patterns/
├── adapter-pattern.mdx (200 tokens)
├── adapter-pattern.mdx.skill.md (200 tokens)
├── reactive-state-pattern.mdx (300 tokens)
├── reactive-state-pattern.mdx.skill.md (300 tokens)
├── quickstart.mdx (1500 tokens)
├── scripts/
├── compare-exports.mts (3.4k tokens)
├── generate-api-docs.mts (800 tokens)
├── parsers/
├── parse-griffe.mts (3k tokens)
├── parse-rustdoc.mts (4.1k tokens)
├── parse-typedoc.mts (3.1k tokens)
├── renderers/
├── components.mts (700 tokens)
├── render-mdx.mts (1600 tokens)
├── types.mts (500 tokens)
├── sdk-reference/
├── engine-sdk.mdx (2.2k tokens)
├── engine-sdk.mdx.skill.md (2.2k tokens)
├── troubleshooting.mdx (200 tokens)
├── tutorials/
├── build-an-agent/
├── define-tools.mdx
├── define-tools.mdx.skill.md
├── memory-and-state.mdx
├── memory-and-state.mdx.skill.md
├── orchestration-loop.mdx
├── orchestration-loop.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (100 tokens)
├── incremental-adoption/
├── migrate-persistence.mdx
├── migrate-persistence.mdx.skill.md
├── offload-to-queue.mdx
├── offload-to-queue.mdx.skill.md
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── wrap-existing-api.mdx
├── wrap-existing-api.mdx.skill.md
├── linkly/
├── channels.mdx (900 tokens)
├── channels.mdx.skill.md (800 tokens)
├── console-traces.png
├── durable-execution.mdx (2.3k tokens)
├── durable-execution.mdx.skill.md (2.3k tokens)
├── foundations.mdx (2.6k tokens)
├── foundations.mdx.skill.md (2.6k tokens)
├── frontend.mdx (2.3k tokens)
├── frontend.mdx.skill.md (2.3k tokens)
├── observability.mdx (1300 tokens)
├── observability.mdx.skill.md (1300 tokens)
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── persistence.mdx (1700 tokens)
├── persistence.mdx.skill.md (1600 tokens)
├── streaming.mdx (900 tokens)
├── streaming.mdx.skill.md (900 tokens)
├── reactive-crud/
├── crud-endpoints.mdx
├── crud-endpoints.mdx.skill.md
├── model-and-store.mdx
├── model-and-store.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (200 tokens)
├── realtime-subscriptions.mdx
├── realtime-subscriptions.mdx.skill.md
├── understanding-iii/
├── channels.mdx (500 tokens)
├── channels.mdx.skill.md (500 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── index.mdx (2.9k tokens)
├── index.mdx.skill.md (2.9k tokens)
├── upgrading/
├── from-0-19-x.mdx (2.2k tokens)
├── from-0-19-x.mdx.skill.md (2.2k tokens)
├── using-iii/
├── cli.mdx (500 tokens)
├── cli.mdx.skill.md (500 tokens)
├── console.mdx (1500 tokens)
├── console.mdx.skill.md (1500 tokens)
├── deployment.mdx (700 tokens)
├── deployment.mdx.skill.md (700 tokens)
├── engine.mdx (600 tokens)
├── engine.mdx.skill.md (600 tokens)
├── functions.mdx (1900 tokens)
├── functions.mdx.skill.md (1900 tokens)
├── triggers.mdx (1700 tokens)
├── triggers.mdx.skill.md (1700 tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (300 tokens)
├── workers.mdx (2k tokens)
├── workers.mdx.skill.md (1900 tokens)
├── LICENSE (2.2k tokens)
├── README.md (200 tokens)
├── README.md.skill.md (200 tokens)
├── RELEASING.md (700 tokens)
├── api-reference/
├── helpers-node.mdx (7.4k tokens)
├── helpers-node.mdx.skill.md (7.4k tokens)
├── helpers-python.mdx (5.3k tokens)
├── helpers-python.mdx.skill.md (5.3k tokens)
├── helpers-rust.mdx (4.9k tokens)
├── helpers-rust.mdx.skill.md (4.9k tokens)
├── sdk-browser.mdx (7.6k tokens)
├── sdk-browser.mdx.skill.md (7.6k tokens)
├── sdk-node.mdx (5.6k tokens)
├── sdk-node.mdx.skill.md (5.6k tokens)
├── sdk-python.mdx (4.4k tokens)
├── sdk-python.mdx.skill.md (4.4k tokens)
├── sdk-rust.mdx (5.9k tokens)
├── sdk-rust.mdx.skill.md (5.9k tokens)
├── assets/
├── favicon.svg (3.6k tokens)
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── changelog/
├── 0-11-0/
├── everything-is-a-worker.mdx (2.1k tokens)
├── migrated-examples.mdx (900 tokens)
├── migrating-from-motia-js.mdx (3.6k tokens)
├── migrating-from-motia-py.mdx (4k tokens)
├── 0-16-0/
├── align-rust-register-function-with-signature.mdx (1500 tokens)
├── 0-20-0/
├── cross-language-parity.mdx (600 tokens)
├── extract-helpers-lib.mdx (700 tokens)
├── observability-into-helpers.mdx (500 tokens)
├── organize-sdk-imports.mdx (600 tokens)
├── submodule-clean-break.mdx (500 tokens)
├── index.mdx (16k tokens)
├── cli-reference/
├── index.mdx (5.1k tokens)
├── index.mdx.skill.md (5.1k tokens)
├── creating-workers/
├── channels.mdx (2000 tokens)
├── channels.mdx.skill.md (2000 tokens)
├── functions.mdx (3.1k tokens)
├── functions.mdx.skill.md (3.1k tokens)
├── http.mdx (900 tokens)
├── http.mdx.skill.md (900 tokens)
├── index.mdx (500 tokens)
├── index.mdx.skill.md (500 tokens)
├── observability.mdx (1100 tokens)
├── observability.mdx.skill.md (1100 tokens)
├── queues.mdx (3k tokens)
├── queues.mdx.skill.md (3k tokens)
├── sandboxes.mdx (900 tokens)
├── sandboxes.mdx.skill.md (900 tokens)
├── triggers.mdx (3.7k tokens)
├── triggers.mdx.skill.md (3.7k tokens)
├── worker-manager.mdx (2.5k tokens)
├── worker-manager.mdx.skill.md (2.5k tokens)
├── worker-manifest.mdx (1100 tokens)
├── worker-manifest.mdx.skill.md (1100 tokens)
├── workers-registry.mdx (1200 tokens)
├── workers-registry.mdx.skill.md (1200 tokens)
├── workers.mdx (3.9k tokens)
├── workers.mdx.skill.md (3.9k tokens)
├── custom.css (1900 tokens)
├── docs.json (7.8k tokens)
├── how-to/
├── build-a-realtime-todo-app.mdx (300 tokens)
├── build-a-realtime-todo-app.mdx.skill.md (300 tokens)
├── schedule-cron-task.mdx (100 tokens)
├── schedule-cron-task.mdx.skill.md (100 tokens)
├── use-trigger-conditions.mdx (100 tokens)
├── use-trigger-conditions.mdx.skill.md (100 tokens)
├── index.mdx (1000 tokens)
├── index.mdx.skill.md (1000 tokens)
├── install.mdx (500 tokens)
├── install.mdx.skill.md (500 tokens)
├── navbar-counters.js (500 tokens)
├── next/
├── api-reference/
├── helpers-node.mdx (7.4k tokens)
├── helpers-node.mdx.skill.md (7.4k tokens)
├── helpers-python.mdx (5.3k tokens)
├── helpers-python.mdx.skill.md (5.3k tokens)
├── helpers-rust.mdx (4.9k tokens)
├── helpers-rust.mdx.skill.md (4.9k tokens)
├── sdk-browser.mdx (7.6k tokens)
├── sdk-browser.mdx.skill.md (7.6k tokens)
├── sdk-node.mdx (5.6k tokens)
├── sdk-node.mdx.skill.md (5.6k tokens)
├── sdk-python.mdx (4.4k tokens)
├── sdk-python.mdx.skill.md (4.4k tokens)
├── sdk-rust.mdx (5.9k tokens)
├── sdk-rust.mdx.skill.md (5.9k tokens)
├── assets/
├── favicon.svg (3.6k tokens)
├── iii-black.svg (100 tokens)
├── iii-white.svg (200 tokens)
├── cli-reference/
├── index.mdx (5.1k tokens)
├── index.mdx.skill.md (5.1k tokens)
├── creating-workers/
├── channels.mdx (2000 tokens)
├── channels.mdx.skill.md (2000 tokens)
├── functions.mdx (3.1k tokens)
├── functions.mdx.skill.md (3.1k tokens)
├── http.mdx (900 tokens)
├── http.mdx.skill.md (900 tokens)
├── index.mdx (500 tokens)
├── index.mdx.skill.md (500 tokens)
├── observability.mdx (1100 tokens)
├── observability.mdx.skill.md (1100 tokens)
├── queues.mdx (3k tokens)
├── queues.mdx.skill.md (3k tokens)
├── sandboxes.mdx (900 tokens)
├── sandboxes.mdx.skill.md (900 tokens)
├── triggers.mdx (3.7k tokens)
├── triggers.mdx.skill.md (3.7k tokens)
├── worker-manager.mdx (2.5k tokens)
├── worker-manager.mdx.skill.md (2.5k tokens)
├── worker-manifest.mdx (1100 tokens)
├── worker-manifest.mdx.skill.md (1100 tokens)
├── workers-registry.mdx (1200 tokens)
├── workers-registry.mdx.skill.md (1200 tokens)
├── workers.mdx (3.9k tokens)
├── workers.mdx.skill.md (3.9k tokens)
├── how-to/
├── build-a-realtime-todo-app.mdx (300 tokens)
├── build-a-realtime-todo-app.mdx.skill.md (300 tokens)
├── schedule-cron-task.mdx (100 tokens)
├── schedule-cron-task.mdx.skill.md (100 tokens)
├── use-trigger-conditions.mdx (100 tokens)
├── use-trigger-conditions.mdx.skill.md (100 tokens)
├── index.mdx (1000 tokens)
├── index.mdx.skill.md (1000 tokens)
├── install.mdx (500 tokens)
├── install.mdx.skill.md (500 tokens)
├── patterns/
├── adapter-pattern.mdx (200 tokens)
├── adapter-pattern.mdx.skill.md (200 tokens)
├── reactive-state-pattern.mdx (300 tokens)
├── reactive-state-pattern.mdx.skill.md (300 tokens)
├── quickstart.mdx (1500 tokens)
├── quickstart.mdx.skill.md (1500 tokens)
├── scripts/
├── compare-exports.mts (3.4k tokens)
├── generate-api-docs.mts (800 tokens)
├── parsers/
├── parse-griffe.mts (3k tokens)
├── parse-rustdoc.mts (4.3k tokens)
├── parse-typedoc.mts (3.1k tokens)
├── renderers/
├── components.mts (700 tokens)
├── render-mdx.mts (1600 tokens)
├── types.mts (500 tokens)
├── sdk-reference/
├── engine-sdk.mdx (2.3k tokens)
├── engine-sdk.mdx.skill.md (2.3k tokens)
├── troubleshooting.mdx (200 tokens)
├── troubleshooting.mdx.skill.md (200 tokens)
├── tutorials/
├── build-an-agent/
├── define-tools.mdx
├── define-tools.mdx.skill.md
├── memory-and-state.mdx
├── memory-and-state.mdx.skill.md
├── orchestration-loop.mdx
├── orchestration-loop.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (100 tokens)
├── incremental-adoption/
├── migrate-persistence.mdx
├── migrate-persistence.mdx.skill.md
├── offload-to-queue.mdx
├── offload-to-queue.mdx.skill.md
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── wrap-existing-api.mdx
├── wrap-existing-api.mdx.skill.md
├── linkly/
├── channels.mdx (900 tokens)
├── channels.mdx.skill.md (800 tokens)
├── console-traces.png
├── durable-execution.mdx (2.4k tokens)
├── durable-execution.mdx.skill.md (2.4k tokens)
├── foundations.mdx (2.6k tokens)
├── foundations.mdx.skill.md (2.6k tokens)
├── frontend.mdx (2.3k tokens)
├── frontend.mdx.skill.md (2.3k tokens)
├── observability.mdx (1300 tokens)
├── observability.mdx.skill.md (1300 tokens)
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── persistence.mdx (1700 tokens)
├── persistence.mdx.skill.md (1700 tokens)
├── streaming.mdx (900 tokens)
├── streaming.mdx.skill.md (900 tokens)
├── reactive-crud/
├── crud-endpoints.mdx
├── crud-endpoints.mdx.skill.md
├── model-and-store.mdx
├── model-and-store.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (200 tokens)
├── realtime-subscriptions.mdx
├── realtime-subscriptions.mdx.skill.md
├── understanding-iii/
├── channels.mdx (500 tokens)
├── channels.mdx.skill.md (500 tokens)
├── engine.mdx (700 tokens)
├── engine.mdx.skill.md (700 tokens)
├── index.mdx (2.9k tokens)
├── index.mdx.skill.md (2.9k tokens)
├── upgrading/
├── from-0-19-x.mdx (2.2k tokens)
├── from-0-19-x.mdx.skill.md (2.2k tokens)
├── using-iii/
├── cli.mdx (500 tokens)
├── cli.mdx.skill.md (500 tokens)
├── configuration.mdx (1400 tokens)
├── configuration.mdx.skill.md (1400 tokens)
├── console.mdx (1500 tokens)
├── console.mdx.skill.md (1500 tokens)
├── deployment.mdx (700 tokens)
├── deployment.mdx.skill.md (700 tokens)
├── engine.mdx (700 tokens)
├── engine.mdx.skill.md (700 tokens)
├── functions.mdx (2000 tokens)
├── functions.mdx.skill.md (2000 tokens)
├── triggers.mdx (1800 tokens)
├── triggers.mdx.skill.md (1900 tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (300 tokens)
├── workers.mdx (2.1k tokens)
├── workers.mdx.skill.md (2000 tokens)
├── package.json
├── patterns/
├── adapter-pattern.mdx (200 tokens)
├── adapter-pattern.mdx.skill.md (200 tokens)
├── reactive-state-pattern.mdx (300 tokens)
├── reactive-state-pattern.mdx.skill.md (300 tokens)
├── quickstart.mdx (1500 tokens)
├── quickstart.mdx.skill.md (1500 tokens)
├── scripts/
├── compare-exports.mts (3.4k tokens)
├── generate-api-docs.mts (800 tokens)
├── parsers/
├── parse-griffe.mts (3k tokens)
├── parse-rustdoc.mts (4.3k tokens)
├── parse-typedoc.mts (3.1k tokens)
├── renderers/
├── components.mts (700 tokens)
├── render-mdx.mts (1600 tokens)
├── types.mts (500 tokens)
├── sdk-reference/
├── engine-sdk.mdx (2.3k tokens)
├── engine-sdk.mdx.skill.md (2.3k tokens)
├── troubleshooting.mdx (200 tokens)
├── troubleshooting.mdx.skill.md (200 tokens)
├── tutorials/
├── build-an-agent/
├── define-tools.mdx
├── define-tools.mdx.skill.md
├── memory-and-state.mdx
├── memory-and-state.mdx.skill.md
├── orchestration-loop.mdx
├── orchestration-loop.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (100 tokens)
├── incremental-adoption/
├── migrate-persistence.mdx
├── migrate-persistence.mdx.skill.md
├── offload-to-queue.mdx
├── offload-to-queue.mdx.skill.md
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── wrap-existing-api.mdx
├── wrap-existing-api.mdx.skill.md
├── linkly/
├── channels.mdx (900 tokens)
├── channels.mdx.skill.md (800 tokens)
├── console-traces.png
├── durable-execution.mdx (2.4k tokens)
├── durable-execution.mdx.skill.md (2.4k tokens)
├── foundations.mdx (2.6k tokens)
├── foundations.mdx.skill.md (2.6k tokens)
├── frontend.mdx (2.3k tokens)
├── frontend.mdx.skill.md (2.3k tokens)
├── observability.mdx (1300 tokens)
├── observability.mdx.skill.md (1300 tokens)
├── overview.mdx (400 tokens)
├── overview.mdx.skill.md (400 tokens)
├── persistence.mdx (1700 tokens)
├── persistence.mdx.skill.md (1700 tokens)
├── streaming.mdx (900 tokens)
├── streaming.mdx.skill.md (900 tokens)
├── reactive-crud/
├── crud-endpoints.mdx
├── crud-endpoints.mdx.skill.md
├── model-and-store.mdx
├── model-and-store.mdx.skill.md
├── overview.mdx (100 tokens)
├── overview.mdx.skill.md (200 tokens)
├── realtime-subscriptions.mdx
├── realtime-subscriptions.mdx.skill.md
├── understanding-iii/
├── channels.mdx (500 tokens)
├── channels.mdx.skill.md (500 tokens)
├── engine.mdx (700 tokens)
├── engine.mdx.skill.md (700 tokens)
├── index.mdx (2.9k tokens)
├── index.mdx.skill.md (2.9k tokens)
├── upgrading/
├── from-0-19-x.mdx (2.2k tokens)
├── from-0-19-x.mdx.skill.md (2.2k tokens)
├── using-iii/
├── cli.mdx (500 tokens)
├── cli.mdx.skill.md (500 tokens)
├── configuration.mdx (1400 tokens)
├── configuration.mdx.skill.md (1400 tokens)
├── console.mdx (1500 tokens)
├── console.mdx.skill.md (1500 tokens)
├── deployment.mdx (700 tokens)
├── deployment.mdx.skill.md (700 tokens)
├── engine.mdx (700 tokens)
├── engine.mdx.skill.md (700 tokens)
├── functions.mdx (2000 tokens)
├── functions.mdx.skill.md (2000 tokens)
├── triggers.mdx (1800 tokens)
├── triggers.mdx.skill.md (1900 tokens)
├── workers-registry.mdx (300 tokens)
├── workers-registry.mdx.skill.md (300 tokens)
├── workers.mdx (2.1k tokens)
├── workers.mdx.skill.md (2000 tokens)
├── engine/
├── .dockerignore
├── .gitignore
├── .pre-commit-config.yaml (100 tokens)
├── CONTRIBUTING.md (1500 tokens)
├── Caddyfile
├── Cargo.toml (1000 tokens)
├── Dockerfile (300 tokens)
├── Dockerfile.debug (200 tokens)
├── LICENSE (800 tokens)
├── Makefile
├── NOTICE
├── PATENTS (100 tokens)
├── README.md (1000 tokens)
├── benches/
├── common/
├── mod.rs (500 tokens)
├── concurrent_invocation_bench.rs (500 tokens)
├── control_plane_churn_bench.rs (1100 tokens)
├── core_runtime_bench.rs (300 tokens)
├── http_blackbox/
├── mod.rs (1900 tokens)
├── http_concurrency_loopback_bench.rs (400 tokens)
├── http_many_routes_loopback_bench.rs (200 tokens)
├── http_single_route_loopback_bench.rs (200 tokens)
├── invoke_function_payload_sizes_bench.rs (300 tokens)
├── kv_store_bench.rs (1500 tokens)
├── protocol_serde_bench.rs (900 tokens)
├── pubsub_fanout_bench.rs (600 tokens)
├── queue_enqueue_bench.rs (700 tokens)
├── startup_bench.rs (200 tokens)
├── state_adapter_bench.rs (800 tokens)
├── trigger_fanout_bench.rs (900 tokens)
├── worker_cleanup_bench.rs (2.4k tokens)
├── worker_invocation_tracking_bench.rs (800 tokens)
├── ws_roundtrip_bench.rs (2.4k tokens)
├── build.rs
├── build.sh (100 tokens)
├── config-remote-kv.yaml (100 tokens)
├── config.prod.yaml
├── config.yaml (1700 tokens)
├── docker-compose.prod.yml (100 tokens)
├── docker-compose.yml (300 tokens)
├── examples/
├── custom_queue_adapter.rs (2.4k tokens)
├── python/
├── echo_invoker.py (400 tokens)
├── firmware/
├── libkrunfw-darwin-aarch64.dylib
├── libkrunfw-linux-aarch64.so
├── libkrunfw-linux-x86_64.so
├── function-macros/
├── Cargo.lock (omitted)
├── Cargo.toml
├── src/
├── lib.rs (3.7k tokens)
├── header.txt (100 tokens)
├── iii.lock (omitted)
├── install.sh (5.9k tokens)
├── licenserc.toml
├── local-worker-dev.md (1300 tokens)
├── packaging/
├── aur/
├── .SRCINFO.template (100 tokens)
├── PKGBUILD.template (200 tokens)
├── chocolatey/
├── iii.nuspec.template (300 tokens)
├── tools/
├── chocolateyinstall.ps1.template (100 tokens)
├── rpm/
├── iii-fedora.spec.template (200 tokens)
├── iii-rhel.spec.template (200 tokens)
├── src/
├── builtins/
├── kv.rs (10.6k tokens)
├── mod.rs (100 tokens)
├── pubsub_lite.rs (1300 tokens)
├── queue.rs (21k tokens)
├── queue_kv.rs (6.3k tokens)
├── cli/
├── advisory.rs (1500 tokens)
├── download.rs (1600 tokens)
├── error.rs (700 tokens)
├── exec.rs (700 tokens)
├── gen_docs.rs (700 tokens)
├── github.rs (1700 tokens)
├── mod.rs (1700 tokens)
├── platform.rs (1900 tokens)
├── project/
├── mod.rs (3.6k tokens)
├── registry.rs (1600 tokens)
├── state.rs (1700 tokens)
├── telemetry.rs (2.4k tokens)
├── update.rs (4.8k tokens)
├── cli_trigger/
├── exec.rs (500 tokens)
├── help.rs (2.6k tokens)
├── mod.rs (1000 tokens)
├── payload.rs (1000 tokens)
├── condition.rs (900 tokens)
├── config/
├── mod.rs (1000 tokens)
├── engine/
├── mod.rs (36.7k tokens)
├── function.rs (3k tokens)
├── invocation/
├── auth.rs (2.4k tokens)
├── http_function.rs (700 tokens)
├── http_invoker.rs (6.1k tokens)
├── method.rs (500 tokens)
├── mod.rs (4.2k tokens)
├── signature.rs (300 tokens)
├── url_validator.rs (3.1k tokens)
├── lib.rs (300 tokens)
├── logging.rs (12.9k tokens)
├── main.rs (6.5k tokens)
├── protocol.rs (3.1k tokens)
├── services.rs (2.7k tokens)
├── telemetry.rs (1000 tokens)
├── trigger.rs (7.1k tokens)
├── trigger_formats.rs (2.5k tokens)
├── update_ops.rs (10.5k tokens)
├── worker_connections/
├── mod.rs (3.8k tokens)
├── traits.rs (2.5k tokens)
├── workers/
├── bridge_client/
├── README.md (600 tokens)
├── iii.worker.yaml
├── mod.rs (4.1k tokens)
├── skills/
├── SKILL.md (700 tokens)
├── config.rs (17.2k tokens)
├── config_rewrite.rs (2.2k tokens)
├── configuration/
├── README.md (2.1k tokens)
├── adapters/
├── bridge.rs (2.5k tokens)
├── fs.rs (6.4k tokens)
├── mod.rs (600 tokens)
├── config.rs (400 tokens)
├── configuration.rs (8.3k tokens)
├── iii.worker.yaml
├── mod.rs (100 tokens)
├── registry.rs (100 tokens)
├── skills/
├── SKILL.md (1100 tokens)
├── store.rs (4.9k tokens)
├── structs.rs (1300 tokens)
├── trigger.rs (3.8k tokens)
├── cron/
├── README.md (1200 tokens)
├── adapters/
├── kv_adapter.rs (1100 tokens)
├── mod.rs (100 tokens)
├── redis_adapter.rs (900 tokens)
├── config.rs (2.7k tokens)
├── configuration.rs (3.1k tokens)
├── cron.rs (7.9k tokens)
├── iii.worker.yaml
├── mod.rs (100 tokens)
├── registry.rs (100 tokens)
├── skills/
├── SKILL.md (800 tokens)
├── structs.rs (6.4k tokens)
├── engine_fn/
├── README.md (1500 tokens)
├── iii.worker.yaml
├── mod.rs (28.1k tokens)
├── skills/
├── SKILL.md (3.2k tokens)
├── external.rs (7.9k tokens)
├── http_functions/
├── config.rs (500 tokens)
├── mod.rs (3.4k tokens)
├── observability/
├── README.md (4.5k tokens)
├── config.rs (5.5k tokens)
├── configuration.rs (3.1k tokens)
├── iii.worker.yaml
├── logs_layer.rs (3.8k tokens)
├── metrics.rs (29k tokens)
├── mod.rs (60.4k tokens)
├── otel.rs (57.5k tokens)
├── otlp_exporter.rs (2.9k tokens)
├── sampler.rs (3.5k tokens)
├── skills/
├── SKILL.md (1200 tokens)
├── pubsub/
├── README.md (900 tokens)
├── adapters/
├── local_adapter.rs (1400 tokens)
├── memory_adapter.rs (300 tokens)
├── mod.rs (100 tokens)
├── redis_adapter.rs (1600 tokens)
├── config.rs (2.1k tokens)
├── configuration.rs (2.9k tokens)
├── iii.worker.yaml
├── mod.rs (200 tokens)
├── pubsub.rs (7.3k tokens)
├── registry.rs (100 tokens)
├── skills/
├── SKILL.md (700 tokens)
├── queue/
├── README.md (1200 tokens)
├── adapters/
├── bridge.rs (3.7k tokens)
├── builtin/
├── adapter.rs (12.2k tokens)
├── mod.rs (100 tokens)
├── memory_adapter.rs (300 tokens)
├── mod.rs (100 tokens)
├── rabbitmq/
├── adapter.rs (9k tokens)
├── consumer.rs (400 tokens)
├── mod.rs (100 tokens)
├── naming.rs (800 tokens)
├── publisher.rs (1100 tokens)
├── retry.rs (500 tokens)
├── topology.rs (1600 tokens)
├── types.rs (1200 tokens)
├── worker.rs (2000 tokens)
├── redis_adapter.rs (3.2k tokens)
├── config.rs (6.2k tokens)
├── configuration.rs (3.1k tokens)
├── message.rs (500 tokens)
├── mod.rs (1000 tokens)
├── queue.rs (25k tokens)
├── registry.rs (100 tokens)
├── skills/
├── SKILL.md (900 tokens)
├── subscriber_config.rs (1200 tokens)
├── redis.rs (4.3k tokens)
├── registry.rs (1100 tokens)
├── registry_worker.rs (9.5k tokens)
├── reload.rs (2.9k tokens)
├── rest_api/
├── README.md (1600 tokens)
├── api_core.rs (13.6k tokens)
├── config.rs (3k tokens)
├── configuration.rs (3k tokens)
├── hot_router.rs (1000 tokens)
├── iii.worker.yaml (100 tokens)
├── mod.rs (100 tokens)
├── skills/
├── SKILL.md (1200 tokens)
├── types.rs (3.2k tokens)
├── views.rs (20.2k tokens)
├── secure_temp.rs (900 tokens)
├── shell/
├── README.md (400 tokens)
├── config.rs (300 tokens)
├── exec.rs (4.4k tokens)
├── glob_exec.rs (1000 tokens)
├── iii.worker.yaml
├── mod.rs (100 tokens)
├── skills/
├── SKILL.md (600 tokens)
├── worker.rs (600 tokens)
├── state/
├── README.md (1400 tokens)
├── adapters/
├── bridge.rs (1300 tokens)
├── kv_store.rs (1500 tokens)
├── mod.rs (300 tokens)
├── redis_adapter.rs (3.1k tokens)
├── config.rs (3.1k tokens)
├── configuration.rs (4.9k tokens)
├── iii.worker.yaml (100 tokens)
├── mod.rs (100 tokens)
├── registry.rs (100 tokens)
├── skills/
├── SKILL.md (800 tokens)
├── state.rs (13.7k tokens)
├── structs.rs (1100 tokens)
├── trigger.rs (1400 tokens)
├── stream/
├── README.md (1600 tokens)
├── adapters/
├── bridge.rs (2.1k tokens)
├── kv_store.rs (2.1k tokens)
├── mod.rs (500 tokens)
├── redis_adapter.rs (3.5k tokens)
├── config.rs (3.4k tokens)
├── configuration.rs (3k tokens)
├── connection.rs (7.1k tokens)
├── iii.worker.yaml (100 tokens)
├── mod.rs (200 tokens)
├── registry.rs (100 tokens)
├── skills/
├── SKILL.md (1000 tokens)
├── socket.rs (4.2k tokens)
├── stream.rs (21.1k tokens)
├── structs.rs (2.1k tokens)
├── trigger.rs (3.4k tokens)
├── utils.rs (600 tokens)
├── telemetry/
├── README.md (100 tokens)
├── amplitude.rs (5.1k tokens)
├── collector.rs (4.6k tokens)
├── environment.rs (5.7k tokens)
├── mod.rs (19.6k tokens)
├── traits.rs (5.5k tokens)
├── worker/
├── README.md (2.7k tokens)
├── channels.rs (3.3k tokens)
├── iii.worker.yaml
├── mod.rs (1600 tokens)
├── rbac_config.rs (4.2k tokens)
├── rbac_session.rs (900 tokens)
├── skills/
├── SKILL.md (2000 tokens)
├── ws_handler.rs (3k tokens)
├── tests/
├── builtin_functions_e2e.rs (2.4k tokens)
├── cli_args.rs (400 tokens)
├── cli_integration.rs (2.7k tokens)
├── common/
├── http_helpers.rs (600 tokens)
├── mod.rs
├── queue_helpers.rs (2.7k tokens)
├── rabbitmq_helpers.rs (1000 tokens)
├── config_reload_e2e.rs (3.8k tokens)
├── configuration_e2e.rs (2.2k tokens)
├── cron_configuration_e2e.rs (3.6k tokens)
├── dlq_redrive_e2e.rs (4.5k tokens)
├── external_known_sandbox.rs (500 tokens)
├── fixtures/
├── argv_probe.sh (100 tokens)
├── templates/
├── bare/
├── template.yaml (200 tokens)
├── default-gitignore (1500 tokens)
├── default-iii-config.yaml (100 tokens)
├── docker/
├── Dockerfile (100 tokens)
├── docker-compose.yml (200 tokens)
├── template.yaml (100 tokens)
├── template.yaml (400 tokens)
├── http_configuration_e2e.rs (4.7k tokens)
├── http_e2e_error_handling.rs (1200 tokens)
├── http_e2e_security.rs (900 tokens)
├── http_middleware_e2e.rs (3.4k tokens)
├── install_sh_integration.sh (1000 tokens)
├── install_sh_unit.bats (1300 tokens)
├── invocation_extensibility.rs (1800 tokens)
├── invocation_integration.rs (400 tokens)
├── observability_configuration_e2e.rs (6k tokens)
├── otel_ws_no_worker_registration_test.rs (1100 tokens)
├── project_init_e2e.rs (2.9k tokens)
├── pubsub_configuration_e2e.rs (3.5k tokens)
├── queue_configuration_e2e.rs (4.2k tokens)
├── queue_e2e_concurrency_resilience.rs (2.4k tokens)
├── queue_e2e_failure_retry.rs (2.4k tokens)
├── queue_e2e_fanout.rs (3.1k tokens)
├── queue_e2e_happy_path.rs (1200 tokens)
├── queue_integration.rs (3.3k tokens)
├── rabbitmq_queue_integration.rs (11.3k tokens)
├── rbac_infrastructure_e2e.rs (2.3k tokens)
├── reload_diff_unit.rs (1300 tokens)
├── reload_manager_unit.rs (1900 tokens)
├── reload_scope_unit.rs (1900 tokens)
├── serverless_http.rs (200 tokens)
├── state_configuration_e2e.rs (2.2k tokens)
├── state_stream_update_e2e.rs (4.2k tokens)
├── state_stream_update_redis_e2e.rs (1200 tokens)
├── stream_configuration_e2e.rs (4.8k tokens)
├── vm_platform_e2e.rs (1300 tokens)
├── worker_init_dispatch_e2e.rs (300 tokens)
├── worker_trigger_e2e.rs (4.3k tokens)
├── infra/
├── terraform/
├── .gitignore (100 tokens)
├── _bootstrap/
├── .terraform.lock.hcl (300 tokens)
├── README.md (700 tokens)
├── main.tf (400 tokens)
├── versions.tf
├── website/
├── .terraform.lock.hcl (300 tokens)
├── acm.tf (200 tokens)
├── cloudfront.tf (1600 tokens)
├── cloudfront_functions/
├── redirects.js (1600 tokens)
├── redirects.test.js (4.4k tokens)
├── iam_github_oidc.tf (1100 tokens)
├── main.tf (100 tokens)
├── observability.tf (300 tokens)
├── outputs.tf (400 tokens)
├── route53.tf (500 tokens)
├── s3.tf (300 tokens)
├── variables.tf (600 tokens)
├── versions.tf
├── package.json (300 tokens)
├── pnpm-lock.yaml (omitted)
├── pnpm-workspace.yaml
├── scripts/
├── generate-api-docs.sh (300 tokens)
├── generate-cli-docs.sh (400 tokens)
├── start-iii.sh (200 tokens)
├── stop-iii.sh
├── sdk/
├── .gitignore
├── .pre-commit-config.yaml (300 tokens)
├── LICENSE (2.2k tokens)
├── README.md (1500 tokens)
├── biome.json (200 tokens)
├── fixtures/
├── config-bridge-backend.yaml (100 tokens)
├── config-bridge.yaml (100 tokens)
├── config-test.yaml (500 tokens)
├── packages/
├── go/
├── iii-example/
├── README.md (200 tokens)
├── go.mod (200 tokens)
├── go.sum (400 tokens)
├── main.go (900 tokens)
├── iii/
├── LICENSE (2.2k tokens)
├── README.md (2.1k tokens)
├── channels.go (2.3k tokens)
├── channels_test.go (1600 tokens)
├── client.go (6.2k tokens)
├── client_reply_test.go (800 tokens)
├── client_test.go (6.2k tokens)
├── constants.go (800 tokens)
├── doc.go (500 tokens)
├── errors.go (500 tokens)
├── errors_test.go (600 tokens)
├── example_test.go (700 tokens)
├── go.mod (100 tokens)
├── go.sum (400 tokens)
├── metadata.go (300 tokens)
├── metadata_test.go (300 tokens)
├── mockengine_test.go (2.6k tokens)
├── otel.go (300 tokens)
├── protocol.go (3.5k tokens)
├── protocol_test.go (1900 tokens)
├── schema.go (800 tokens)
├── schema_test.go (1400 tokens)
├── tests/
├── api_triggers_test.go (1100 tokens)
├── bridge_test.go (600 tokens)
├── common_test.go (700 tokens)
├── data_channels_test.go (600 tokens)
├── errors_test.go (400 tokens)
├── queue_test.go (400 tokens)
├── registration_dedup_test.go (300 tokens)
├── trigger_registration_error_test.go (500 tokens)
├── worker_metadata_test.go (500 tokens)
├── triggers.go (500 tokens)
├── node/
├── .prettierrc
├── helpers/
├── README.md
├── package.json (500 tokens)
├── src/
├── http/
├── index.ts (1000 tokens)
├── observability/
├── http-instrumentation.test.ts (100 tokens)
├── http-instrumentation.ts (700 tokens)
├── index.ts (300 tokens)
├── internal.ts (100 tokens)
├── logger.test.ts (100 tokens)
├── logger.ts (1100 tokens)
├── otel-worker-gauges.ts (900 tokens)
├── telemetry-system/
├── baggage-span-processor.ts (300 tokens)
├── connection.ts (1700 tokens)
├── context.test.ts (100 tokens)
├── context.ts (700 tokens)
├── exporters.ts
├── fetch-instrumentation.ts (1200 tokens)
├── index.ts (2.4k tokens)
├── log-exporter.ts (700 tokens)
├── metrics-exporter.ts (800 tokens)
├── payload.ts (600 tokens)
├── span-exporter.test.ts (500 tokens)
├── span-exporter.ts (800 tokens)
├── span-ops.ts (200 tokens)
├── types.ts (900 tokens)
├── utils.test.ts (400 tokens)
├── utils.ts (300 tokens)
├── types.ts (200 tokens)
├── utils.ts (100 tokens)
├── worker-metrics.ts (1000 tokens)
├── queue/
├── index.ts
├── stream/
├── index.ts (2.1k tokens)
├── worker-connection-manager/
├── index.ts (1000 tokens)
├── tsconfig.json (100 tokens)
├── tsdown.config.ts (100 tokens)
├── typedoc.json (100 tokens)
├── vitest.config.ts
├── iii-browser/
├── .npmignore
├── README.md (900 tokens)
├── package.json (300 tokens)
├── src/
├── channels.ts (1500 tokens)
├── helpers.ts (300 tokens)
├── iii-constants.ts (400 tokens)
├── iii-types.ts (2.1k tokens)
├── iii.ts (5.3k tokens)
├── index.ts (200 tokens)
├── state.ts (700 tokens)
├── stream.ts (2000 tokens)
├── triggers.ts (200 tokens)
├── types.ts (2.1k tokens)
├── utils.ts (500 tokens)
├── tests/
├── bridge.test.ts (1500 tokens)
├── channels.test.ts (1000 tokens)
├── connection.test.ts (500 tokens)
├── exports.test.ts (600 tokens)
├── helpers.test.ts (300 tokens)
├── integration/
├── bridge.test.ts (900 tokens)
├── channels.test.ts (1200 tokens)
├── functions-available-trigger.test.ts (500 tokens)
├── setup.ts
├── trigger-type-lifecycle.test.ts (900 tokens)
├── trigger-types.test.ts (800 tokens)
├── triggers.test.ts (600 tokens)
├── utils.ts (200 tokens)
├── mock-websocket.ts (1500 tokens)
├── setup.ts
├── trigger-types.test.ts (1000 tokens)
├── triggers.test.ts (800 tokens)
├── tsconfig.json (100 tokens)
├── tsdown.config.ts (100 tokens)
├── typedoc.json
├── vitest.config.ts (100 tokens)
├── vitest.integration.config.ts (100 tokens)
├── iii-example/
├── config.yaml (400 tokens)
├── docker-compose.yaml (300 tokens)
├── otel-collector-config.yaml (300 tokens)
├── package.json (100 tokens)
├── src/
├── hooks.ts (200 tokens)
├── http-example.ts (300 tokens)
├── iii-zod-example.ts (100 tokens)
├── iii.ts (100 tokens)
├── index.ts (800 tokens)
├── middleware-example.ts (600 tokens)
├── queue-basic.ts (600 tokens)
├── queue-bulk-email.ts (300 tokens)
├── queue-dlq.ts (400 tokens)
├── queue-ecommerce.ts (500 tokens)
├── queue-financial.ts (600 tokens)
├── state.ts (200 tokens)
├── stream.ts (500 tokens)
├── trigger-types.ts (1000 tokens)
├── types.ts
├── stress-dlq.sh (400 tokens)
├── test-queues.sh (600 tokens)
├── iii/
├── .npmignore
├── README.md (600 tokens)
├── local.yml
├── package.json (500 tokens)
├── src/
├── channel.ts
├── channels.ts (1800 tokens)
├── engine.ts
├── errors.ts (400 tokens)
├── helpers.ts (300 tokens)
├── iii-constants.ts (400 tokens)
├── iii-types.ts (1300 tokens)
├── iii.ts (7.6k tokens)
├── index.ts (100 tokens)
├── internal.ts
├── protocol.ts (100 tokens)
├── runtime.ts
├── state.ts (700 tokens)
├── stream.ts (200 tokens)
├── trigger.ts
├── triggers.ts (200 tokens)
├── types.ts (2.3k tokens)
├── utils.ts (600 tokens)
├── tests/
├── api-triggers.test.ts (3.5k tokens)
├── baggage-span-processor.test.ts (700 tokens)
├── bridge-state.test.ts (800 tokens)
├── bridge-stream.test.ts (1000 tokens)
├── bridge-utils.ts (200 tokens)
├── bridge.test.ts (600 tokens)
├── channel-close-delay.test.ts (1500 tokens)
├── channel-writer-readiness.test.ts (1700 tokens)
├── data-channels.test.ts (1500 tokens)
├── errors.test.ts (600 tokens)
├── exports.test.ts (400 tokens)
├── fetch-instrumentation.test.ts (3.9k tokens)
├── healthcheck.test.ts (300 tokens)
├── helpers.test.ts (200 tokens)
├── http-external-functions.test.ts (2.4k tokens)
├── invoke-metadata.test.ts (1500 tokens)
├── logger-runtime.test.ts (500 tokens)
├── logger.test.ts (500 tokens)
├── middleware.test.ts (1100 tokens)
├── otel-defaults.test.ts (1700 tokens)
├── otel-exception.test.ts (600 tokens)
├── otel-worker-gauges.test.ts (700 tokens)
├── payload.test.ts (1100 tokens)
├── pubsub.test.ts (600 tokens)
├── rbac-workers.test.ts (3.4k tokens)
├── register-worker-metadata.test.ts (900 tokens)
├── service-triggers.test.ts (1900 tokens)
├── setup.ts (100 tokens)
├── span-ops.test.ts (800 tokens)
├── state.test.ts (1700 tokens)
├── stream.test.ts (2.9k tokens)
├── trigger-action.test.ts (100 tokens)
├── trigger-registration-error.test.ts (500 tokens)
├── trigger-type-lifecycle.test.ts (900 tokens)
├── utils.ts (400 tokens)
├── worker-metrics.test.ts (600 tokens)
├── tsconfig.json (100 tokens)
├── tsdown.config.ts (100 tokens)
├── typedoc.json (100 tokens)
├── vitest.config.ts (200 tokens)
├── observability/
├── README.md (100 tokens)
├── package.json (200 tokens)
├── src/
├── index.ts
├── internal.ts
├── tsconfig.json (100 tokens)
├── tsdown.config.ts (100 tokens)
├── package.json
├── python/
├── .gitignore (200 tokens)
├── helpers/
├── README.md
├── pyproject.toml (300 tokens)
├── src/
├── iii_helpers/
├── __init__.py
├── http/
├── __init__.py (1000 tokens)
├── observability/
├── __init__.py (300 tokens)
├── baggage_span_processor.py (300 tokens)
├── http_instrumentation.py (800 tokens)
├── logger.py (1300 tokens)
├── payload.py (500 tokens)
├── reconnection.py (200 tokens)
├── span_ops.py (200 tokens)
├── telemetry.py (4.2k tokens)
├── telemetry_exporters.py (3.6k tokens)
├── telemetry_types.py (400 tokens)
├── py.typed
├── queue/
├── __init__.py (100 tokens)
├── stream/
├── __init__.py (1900 tokens)
├── worker_connection_manager/
├── __init__.py (1700 tokens)
├── tests/
├── __init__.py
├── test_observability_http_instrumentation.py (200 tokens)
├── test_observability_telemetry.py (100 tokens)
├── uv.lock (omitted)
├── iii-example/
├── config.yaml (300 tokens)
├── docker-compose.yaml (200 tokens)
├── otel-collector-config.yaml (200 tokens)
├── pyproject.toml (100 tokens)
├── src/
├── __init__.py
├── hooks.py (400 tokens)
├── iii_function_example.py (100 tokens)
├── main.py (1400 tokens)
├── models.py
├── state.py (200 tokens)
├── stream.py (700 tokens)
├── trigger_types.py (1300 tokens)
├── uv.lock (omitted)
├── iii/
├── README.md (700 tokens)
├── pyproject.toml (300 tokens)
├── src/
├── iii/
├── __init__.py (200 tokens)
├── channel.py
├── channels.py (1600 tokens)
├── engine.py
├── errors.py (500 tokens)
├── format_utils.py (900 tokens)
├── helpers.py (600 tokens)
├── iii.py (11.1k tokens)
├── iii_constants.py (700 tokens)
├── iii_types.py (2.4k tokens)
├── internal.py
├── otel_worker_gauges.py (1000 tokens)
├── protocol.py (100 tokens)
├── runtime.py
├── state.py (500 tokens)
├── stream.py (300 tokens)
├── trigger.py
├── triggers.py (700 tokens)
├── types.py (1300 tokens)
├── utils.py (100 tokens)
├── worker_metrics.py (1100 tokens)
├── tests/
├── conftest.py (100 tokens)
├── test_api_triggers.py (4.2k tokens)
├── test_async_api.py (300 tokens)
├── test_baggage_span_processor.py (800 tokens)
├── test_bridge.py (700 tokens)
├── test_channel_close_delay.py (600 tokens)
├── test_channel_submodule.py (100 tokens)
├── test_context_propagation.py (700 tokens)
├── test_data_channels.py (1300 tokens)
├── test_engine_constants.py (200 tokens)
├── test_errors.py (1300 tokens)
├── test_format_utils.py (1900 tokens)
├── test_healthcheck.py (500 tokens)
├── test_helpers.py (1200 tokens)
├── test_hold_process.py (500 tokens)
├── test_http_external_functions_integration.py (3.8k tokens)
├── test_iii_registration_dedup.py (1100 tokens)
├── test_init_api.py (400 tokens)
├── test_invocation_exception.py (700 tokens)
├── test_invocation_metadata.py (1800 tokens)
├── test_logger_function_ids.py (200 tokens)
├── test_logger_otel.py (800 tokens)
├── test_middleware.py (1400 tokens)
├── test_payload.py (1100 tokens)
├── test_pubsub.py (600 tokens)
├── test_rbac_workers.py (3.6k tokens)
├── test_register_function_args.py (2.8k tokens)
├── test_runtime_submodule.py (100 tokens)
├── test_span_ops.py (700 tokens)
├── test_state.py (2000 tokens)
├── test_stream_models.py (1000 tokens)
├── test_stream_types.py (100 tokens)
├── test_streams.py (2.4k tokens)
├── test_streams_runtime_annotations.py
├── test_sync_api.py (1200 tokens)
├── test_telemetry.py (1600 tokens)
├── test_telemetry_exporters.py (1600 tokens)
├── test_telemetry_types.py (300 tokens)
├── test_trace_helpers.py (100 tokens)
├── test_trigger_action.py (100 tokens)
├── test_trigger_metadata.py (200 tokens)
├── test_trigger_registration_error.py (400 tokens)
├── test_trigger_submodule.py (200 tokens)
├── test_trigger_type_lifecycle.py (1000 tokens)
├── test_utils.py (300 tokens)
├── test_worker_metadata.py (800 tokens)
├── test_worker_metrics.py (1000 tokens)
├── uv.lock (omitted)
├── observability/
├── README.md
├── pyproject.toml (200 tokens)
├── src/
├── iii_observability/
├── __init__.py (100 tokens)
├── py.typed
├── uv.lock (omitted)
├── rust/
├── helpers/
├── Cargo.toml (300 tokens)
├── README.md
├── src/
├── http.rs (500 tokens)
├── lib.rs
├── observability/
├── logger.rs (1500 tokens)
├── mod.rs (200 tokens)
├── telemetry/
├── baggage_span_processor.rs (1300 tokens)
├── connection.rs (2.2k tokens)
├── context.rs (2k tokens)
├── http_instrumentation.rs (1600 tokens)
├── json_serializer.rs (900 tokens)
├── live_span_processor.rs (1900 tokens)
├── log_exporter.rs (1100 tokens)
├── metrics_exporter.rs (2.2k tokens)
├── mod.rs (5.3k tokens)
├── otel_worker_gauges.rs (600 tokens)
├── payload.rs (1500 tokens)
├── span_exporter.rs (1400 tokens)
├── span_ops.rs (400 tokens)
├── types.rs (1200 tokens)
├── worker_metrics.rs (700 tokens)
├── queue.rs (100 tokens)
├── stream.rs (4.8k tokens)
├── worker_connection_manager.rs (1500 tokens)
├── tests/
├── observability_context_test.rs
├── observability_init_test.rs (100 tokens)
├── observability_logger_test.rs
├── observability_payload_test.rs (100 tokens)
├── observability_types_test.rs (100 tokens)
├── iii-example/
├── Cargo.lock (omitted)
├── Cargo.toml (100 tokens)
├── src/
├── cron_trigger_example.rs (1000 tokens)
├── custom_trigger_example.rs (1100 tokens)
├── http_example.rs (800 tokens)
├── logger_example.rs (200 tokens)
├── main.rs (2.1k tokens)
├── trigger_type_example.rs (900 tokens)
├── iii/
├── .gitignore
├── Cargo.lock (omitted)
├── Cargo.toml (300 tokens)
├── README.md (1200 tokens)
├── src/
├── builtin_triggers.rs (2.3k tokens)
├── channels.rs (2.5k tokens)
├── engine.rs (500 tokens)
├── error.rs (700 tokens)
├── helpers.rs (800 tokens)
├── iii.rs (20.2k tokens)
├── lib.rs (2.5k tokens)
├── protocol.rs (2.4k tokens)
├── stream_provider.rs (200 tokens)
├── structs.rs (200 tokens)
├── triggers.rs (400 tokens)
├── types.rs (500 tokens)
├── tests/
├── api_triggers.rs (7.7k tokens)
├── bridge.rs (1000 tokens)
├── common/
├── mock_engine.rs (1600 tokens)
├── mod.rs (200 tokens)
├── data_channels.rs (2.8k tokens)
├── error_wire.rs (800 tokens)
├── healthcheck.rs (600 tokens)
├── helpers_module.rs (100 tokens)
├── hold_process.rs (200 tokens)
├── http_external_functions.rs (2.8k tokens)
├── init_api.rs (200 tokens)
├── middleware.rs (1400 tokens)
├── payload_capture.rs (1500 tokens)
├── pubsub.rs (1000 tokens)
├── rbac_workers.rs (6.2k tokens)
├── registration_dedup.rs (1700 tokens)
├── span_ops_api.rs (1200 tokens)
├── state.rs (2.5k tokens)
├── stream.rs (2.1k tokens)
├── stream_provider_trait.rs (200 tokens)
├── trigger_action.rs (200 tokens)
├── trigger_type_lifecycle.rs (1600 tokens)
├── observability/
├── Cargo.toml (100 tokens)
├── README.md
├── src/
├── lib.rs (100 tokens)
├── test-assets/
├── handbook.pdf
├── skills/
├── .gitignore
├── LICENSE (2.2k tokens)
├── README.md (500 tokens)
├── SKILLS.md (300 tokens)
├── iii-architecture-patterns/
├── SKILL.md (1300 tokens)
├── iii-core-primitives/
├── SKILL.md (1900 tokens)
├── iii-engine-config/
├── SKILL.md (1900 tokens)
├── iii-error-handling/
├── SKILL.md (1000 tokens)
├── iii-getting-started/
├── SKILL.md (1400 tokens)
├── iii-sdk-reference/
├── SKILL.md (1100 tokens)
├── presentation/
├── README.md (600 tokens)
├── SKILL.md (3.1k tokens)
├── base/
├── .gitignore
├── COMPONENTS.md (3.1k tokens)
├── README.md (1100 tokens)
├── _viewer/
├── index.html (200 tokens)
├── src/
├── ViewerApp.tsx (700 tokens)
├── main.tsx (100 tokens)
├── build.mjs (1600 tokens)
├── index.html (200 tokens)
├── package.json (200 tokens)
├── scripts/
├── manifest.mjs (2000 tokens)
├── src/
├── components/
├── Footer.tsx (300 tokens)
├── PageShell.tsx (400 tokens)
├── PlayerControls.tsx (300 tokens)
├── Section.tsx (400 tokens)
├── SpecSheet.tsx (500 tokens)
├── TopNav.tsx (700 tokens)
├── diagrams/
├── CliPlayground.tsx (1100 tokens)
├── DurabilityTimeline.tsx (1300 tokens)
├── EventFanOut.tsx (1300 tokens)
├── FanOut.tsx (1800 tokens)
├── Funnel.tsx (1900 tokens)
├── SequencePlayer.tsx (1500 tokens)
├── SpawnTree.tsx (2k tokens)
├── StepReveal.tsx (1300 tokens)
├── SystemMap.tsx (2.8k tokens)
├── schematic/
├── Button.tsx (300 tokens)
├── Caret.tsx (100 tokens)
├── Cell.tsx (100 tokens)
├── CodeBlock.tsx (300 tokens)
├── FnChip.tsx (200 tokens)
├── ModeToggle.tsx (200 tokens)
├── Prompt.tsx (100 tokens)
├── Sheet.tsx (100 tokens)
├── StatusDot.tsx (100 tokens)
├── StatusPanel.tsx (300 tokens)
├── Terminal.tsx (300 tokens)
├── Wordmark.tsx (100 tokens)
├── WorkerCard.tsx (300 tokens)
├── content/
├── highlight.tsx (1800 tokens)
├── markdown.tsx (1900 tokens)
├── mermaid.tsx (700 tokens)
├── gallery/
├── App.tsx (500 tokens)
├── Gallery.tsx (600 tokens)
├── PresentationCard.tsx (500 tokens)
├── SiteFooter.tsx (100 tokens)
├── SiteHeader.tsx (200 tokens)
├── main.tsx
├── site.ts (200 tokens)
├── hooks/
├── useHashRoute.ts (300 tokens)
├── useStepper.ts (400 tokens)
├── useTheme.ts (100 tokens)
├── index.css (1000 tokens)
├── lib/
├── deck-types.ts (200 tokens)
├── utils.ts
├── pages/
├── SpecPage.tsx (700 tokens)
├── vite-env.d.ts (omitted)
├── tsconfig.app.json (100 tokens)
├── tsconfig.json
├── tsconfig.node.json (100 tokens)
├── vite.config.ts (200 tokens)
├── reference/
├── archetypes.md (2.2k tokens)
├── component-standards.md (900 tokens)
├── design-system.md (1000 tokens)
├── hosting.md (1200 tokens)
├── narrative-framework.md (800 tokens)
├── quality-bar.md (700 tokens)
├── template/
├── README.md (100 tokens)
├── index.html (200 tokens)
├── src/
├── App.tsx (400 tokens)
├── content/
├── deck.ts (200 tokens)
├── example.ts (2.2k tokens)
├── diagrams/
├── .gitkeep
├── main.tsx
├── pages/
├── ExamplePage.tsx (300 tokens)
├── sections/
├── Hero.tsx (900 tokens)
├── PayoffSection.tsx (500 tokens)
├── SystemMapSection.tsx (1000 tokens)
├── spec-docs.ts (100 tokens)
├── tech-specs/
├── 2026-06-08-agentic/
├── README.md (5.5k tokens)
├── approval-gate.md (8.2k tokens)
├── context-manager.md (3k tokens)
├── harness.md (12k tokens)
├── llm-router.md (8.2k tokens)
├── session-manager.md (4.2k tokens)
├── 2026-06-22-rbac-proxy-worker/
├── README.md (2.3k tokens)
├── engine-overrides.md (2.9k tokens)
├── protocol-interception.md (3.1k tokens)
├── rbac-proxy.md (3.2k tokens)
├── rbac.md (5k tokens)
├── 2026-06-29-codegen/
├── README.md (2.1k tokens)
├── configuration.md (2000 tokens)
├── discovery-and-types.md (2k tokens)
├── emitters.md (2k tokens)
├── worker-and-cli.md (1800 tokens)
├── README.md (100 tokens)
├── engine-register-trigger-metadata.md (4.8k tokens)
├── turbo.json (100 tokens)
├── website/
├── .gitignore
├── README.md (400 tokens)
├── favicon.svg (100 tokens)
├── fonts/
├── ChivoMono-Italic-VariableFont_wght.ttf
├── ChivoMono-VariableFont_wght.ttf
├── index.html (85.1k tokens)
├── manifesto.html (6.4k tokens)
├── og-image.png
├── og-image.xcf
├── package.json (100 tokens)
├── posthog-consent.js (700 tokens)
├── presentations/
├── .gitignore
├── 2026-06-08-agentic/
├── index.html (200 tokens)
├── src/
├── App.tsx (500 tokens)
├── content/
├── deck.ts (200 tokens)
├── durability.ts (900 tokens)
├── fanout.ts (100 tokens)
├── spawn.ts (700 tokens)
├── workers.ts (2.1k tokens)
├── diagrams/
├── SystemMap.tsx (4k tokens)
├── main.tsx
├── pages/
├── ConsolePage.tsx (1300 tokens)
├── LoopsPage.tsx (1400 tokens)
├── TelegramPage.tsx (1700 tokens)
├── UseCaseShell.tsx (400 tokens)
├── sections/
├── DurableSection.tsx (700 tokens)
├── GovernanceSection.tsx (2.1k tokens)
├── Hero.tsx (1000 tokens)
├── OnboardingSection.tsx (1400 tokens)
├── ReactiveSection.tsx (900 tokens)
├── SolvesSection.tsx (900 tokens)
├── SpawnSection.tsx (800 tokens)
├── SubstrateSection.tsx (1700 tokens)
├── SystemMapSection.tsx (1300 tokens)
├── TurnSection.tsx (1300 tokens)
├── UseCasesSection.tsx (600 tokens)
├── spec-docs.ts (100 tokens)
├── 2026-06-22-rbac-proxy-worker/
├── index.html (200 tokens)
├── src/
├── App.tsx (500 tokens)
├── content/
├── access.ts (1000 tokens)
├── architecture.ts (1700 tokens)
├── coexistence.ts (200 tokens)
├── deck.ts (300 tokens)
├── flow.ts (1300 tokens)
├── hero.ts (200 tokens)
├── payoff.ts (300 tokens)
├── rbac.ts (800 tokens)
├── why.ts (500 tokens)
├── main.tsx
├── pages/
├── EngineOverridesPage.tsx (1100 tokens)
├── RbacContractPage.tsx (1500 tokens)
├── sections/
├── AccessSection.tsx (1200 tokens)
├── CoexistenceSection.tsx (800 tokens)
├── FailClosedSection.tsx (1000 tokens)
├── Hero.tsx (900 tokens)
├── LifecycleSection.tsx (500 tokens)
├── MapSection.tsx (1000 tokens)
├── OverridesSection.tsx (800 tokens)
├── PayoffSection.tsx (500 tokens)
├── WhySection.tsx (600 tokens)
├── spec-docs.ts (100 tokens)
├── 2026-06-29-codegen/
├── index.html (200 tokens)
├── src/
├── App.tsx (500 tokens)
├── content/
├── cli.ts (200 tokens)
├── deck.ts (200 tokens)
├── harness.ts (400 tokens)
├── hero.ts (200 tokens)
├── map.ts (1000 tokens)
├── payoff.ts (300 tokens)
├── select.ts (300 tokens)
├── sequence.ts (400 tokens)
├── why.ts (200 tokens)
├── main.tsx
├── pages/
├── HarnessConsumerPage.tsx (1000 tokens)
├── sections/
├── HarnessSection.tsx (400 tokens)
├── Hero.tsx (1000 tokens)
├── LanguagesSection.tsx (1100 tokens)
├── PayoffSection.tsx (500 tokens)
├── RunItSection.tsx (200 tokens)
├── SelectSection.tsx (600 tokens)
├── SourceOfTruthSection.tsx (300 tokens)
├── SystemMapSection.tsx (1000 tokens)
├── WhySection.tsx (300 tokens)
├── spec-docs.ts (100 tokens)
├── COMPONENTS.md (3.1k tokens)
├── README.md (1100 tokens)
├── _viewer/
├── index.html (200 tokens)
├── src/
├── ViewerApp.tsx (700 tokens)
├── main.tsx (100 tokens)
├── build.mjs (1600 tokens)
├── index.html (200 tokens)
├── package.json (100 tokens)
├── scripts/
├── manifest.mjs (2000 tokens)
├── src/
├── components/
├── Footer.tsx (300 tokens)
├── PageShell.tsx (400 tokens)
├── PlayerControls.tsx (300 tokens)
├── Section.tsx (400 tokens)
├── SpecSheet.tsx (500 tokens)
├── TopNav.tsx (700 tokens)
├── diagrams/
├── CliPlayground.tsx (1100 tokens)
├── DurabilityTimeline.tsx (1300 tokens)
├── EventFanOut.tsx (1300 tokens)
├── FanOut.tsx (1800 tokens)
├── Funnel.tsx (1900 tokens)
├── SequencePlayer.tsx (1500 tokens)
├── SpawnTree.tsx (2k tokens)
├── StepReveal.tsx (1300 tokens)
├── SystemMap.tsx (2.8k tokens)
├── schematic/
├── Button.tsx (300 tokens)
├── Caret.tsx (100 tokens)
├── Cell.tsx (100 tokens)
├── CodeBlock.tsx (300 tokens)
├── FnChip.tsx (200 tokens)
├── ModeToggle.tsx (200 tokens)
├── Prompt.tsx (100 tokens)
├── Sheet.tsx (100 tokens)
├── StatusDot.tsx (100 tokens)
├── StatusPanel.tsx (300 tokens)
├── Terminal.tsx (300 tokens)
├── Wordmark.tsx (100 tokens)
├── WorkerCard.tsx (300 tokens)
├── content/
├── highlight.tsx (1800 tokens)
├── markdown.tsx (1900 tokens)
├── mermaid.tsx (700 tokens)
├── gallery/
├── App.tsx (500 tokens)
├── Gallery.tsx (600 tokens)
├── PresentationCard.tsx (500 tokens)
├── SiteFooter.tsx (100 tokens)
├── SiteHeader.tsx (200 tokens)
├── main.tsx
├── site.ts (200 tokens)
├── hooks/
├── useHashRoute.ts (300 tokens)
├── useStepper.ts (400 tokens)
├── useTheme.ts (100 tokens)
├── index.css (1000 tokens)
├── lib/
├── deck-types.ts (200 tokens)
├── utils.ts
├── pages/
├── SpecPage.tsx (700 tokens)
├── vite-env.d.ts (omitted)
├── tsconfig.app.json (100 tokens)
├── tsconfig.json
├── tsconfig.node.json (100 tokens)
├── vite.config.ts (200 tokens)
├── privacy-policy.html (5.9k tokens)
├── robots.txt (200 tokens)
├── scripts/
├── agents-appendix.md (1500 tokens)
├── ai-overview.ts (2000 tokens)
├── blog-posts.test.ts (500 tokens)
├── blog-posts.ts (400 tokens)
├── generate-llms-agents.test.ts (500 tokens)
├── generate-llms-agents.ts (1500 tokens)
├── generate-sitemap.ts (700 tokens)
├── routes-kvs.test.ts (400 tokens)
├── routes-kvs.ts (600 tokens)
├── routes.ts (300 tokens)
├── tech-specs.test.ts (500 tokens)
├── tech-specs.ts (500 tokens)
├── sitemap.xml (600 tokens)
```
## /.cargo/config.toml
```toml path="/.cargo/config.toml"
# Linker configuration for init binary musl cross-compilation.
# rust-lld is bundled with the Rust toolchain -- no system packages needed.
# D-01 specified musl-tools/aarch64-linux-gnu-gcc, but rust-lld achieves
# the same static musl linking with zero external deps (see RESEARCH.md).
[target.x86_64-unknown-linux-musl]
linker = "rust-lld"
[target.aarch64-unknown-linux-musl]
linker = "rust-lld"
```
## /.config/nextest.toml
```toml path="/.config/nextest.toml"
[profile.ci]
fail-fast = false
[profile.ci.junit]
path = "junit.xml"
```
## /.cursor/rules/docs.mdc
```mdc path="/.cursor/rules/docs.mdc"
---
description: Documentation agent using Divio system for docs/ directory
globs: docs/**/*.mdx, docs/**/*.md
alwaysApply: false
---
# Documentation Agent Capabilities
You are an expert Documentation Architect using the Divio documentation system.
Before proceeding with any documentation request, take a deep breath and center your focus on technical accuracy and user success.
## Core Directives
1. **Analyze First**: When a user requests documentation, first categorize the request into one of the four Divio quadrants:
- **Tutorials**: Learning-oriented (File: `.cursor/skills/doc_tutorial.md`)
- **How-to Guides**: Problem-oriented (File: `.cursor/skills/doc_howto.md`)
- **Reference**: Information-oriented (File: `.cursor/skills/doc_reference.md`)
- **Explanation**: Understanding-oriented (File: `.cursor/skills/doc_explanation.md`)
Use the two axes to resolve ambiguous categorization:
- **Axis 1 — Practical vs. Theoretical**: Tutorials and How-to Guides are practical (the user is doing something). Reference and Explanation are theoretical (the user is reading to understand).
- **Axis 2 — Studying vs. Working**: Tutorials and Explanation are for when the user is learning or studying. How-to Guides and Reference are for when the user is actively working and needs an answer fast.
When a request is ambiguous, use these axes to reason toward a quadrant before asking the user to confirm. For example: if the user wants to "understand why X works this way," that is theoretical + studying = **Explanation**. If they want to "do X in their specific environment," that is practical + working = **How-to Guide**.
2. **Predict & Suggest**:
- Inspect the current file structure.
- Suggest the optimal file path for the new doc.
- If the request is ambiguous, ask the user to confirm the category.
3. **Load Skill**: Once the category and location are confirmed (or if the intent is obvious), strictly adhere to the guidelines in the corresponding `.cursor/skills/` file AND the global workflow rules in `.cursor/skills/doc_workflow.md`.
4. **Reference**:
- Always read `.cursor/skills/doc_workflow.md` for rules regarding Tone, Component Usage (Callouts), and Chunked Execution.
```
## /.cursor/skills/doc_explanation.md
---
name: doc_explanation
description: Writing guide for Explanation documentation — understanding-oriented content that provides context, background, and the reasoning behind design decisions. Use when the reader wants to understand why something works the way it does, not how to use it.
---
# Skill: Writing Explanation (Understanding-Oriented)
**Goal**: Clarify and illuminate specific topics.
## content_rules
- **Context Provider**: Discuss the background, the "why," and the bigger picture.
- **Discursive**: Allow for connections between differing concepts.
- **No Code**: Avoid code blocks unless used for high-level architectural illustration.
- **Permanence**: Write content that changes slowly. Focus on design decisions.
- **Justification**: Explain why the software works the way it does.
## structure
Explanation is the most loosely structured quadrant. It is discursive and essay-like — do not impose a rigid sequence. The following is a suggested flow, not a template to fill in:
- **Topic**: Name the concept being discussed and why it warrants explanation.
- **Background**: Provide contextual or historical information that helps the reader place this concept.
- **Details**: Explore the mechanics, philosophy, or trade-offs in depth. Allow for digressions and connections between ideas.
- **Related**: Link outward to specific Reference or How-to docs. Do not inline their content — cross-reference instead.
Adjust as needed: some explanations need no background; others need no "related" section. The measure of a good explanation is whether the reader comes away understanding _why_, not whether it followed a specific structure.
## /.cursor/skills/doc_howto.md
---
name: doc_howto
description: Writing guide for How-to Guide documentation — task-oriented content for users who already know the basics and need to accomplish a specific, well-defined goal. Use when the reader knows what they want to do but not how to do it.
---
# Skill: Writing How-to Guides (Problem-Oriented)
**Goal**: Show the user how to solve a specific problem they already have.
## content_rules
- **Problem Focus**: Start with the specific problem or real-world use case.
- **Assumed Competence**: Assume the user knows the basics. Do not explain "what" things are, only "how" to use them.
- **Flexibility**: Unlike tutorials, acknowledge that the user might be adapting this to their own setup.
- **External Links**: If a concept requires explanation, link to an _Explanation_ document.
## structure
1. **Goal**: "How to [Task Name]"
2. **Context**: A single sentence on when this is useful.
3. **Steps**: A series of steps to achieve the goal. Steps can be less granular than tutorials.
4. **Result**: What the final state looks like.
## /.cursor/skills/doc_reference.md
---
name: doc_reference
description: Writing guide for Reference documentation — information-oriented content that accurately and completely describes the technical machinery of the software. Use when the reader needs to look something up, not learn or accomplish a task.
---
# Skill: Writing Technical Reference (Information-Oriented)
**Goal**: Describe the machinery accurately.
## content_rules
- **Map of Territory**: Function strictly as a map. Describe classes, functions, API endpoints, commands, or configurations.
- **Austerity**: Be dry, accurate, and concise. Avoid examples unless necessary for syntax clarity.
- **Completeness**: Strive for 100% technical accuracy. Ask the user for context if it is missing.
- **One-to-One**: Ensure the structure of the documentation mirrors the structure of the code/software.
- **No Instruction**: Do not tell the user "how" to use the feature. Link to a _How-to_ guide for that.
## structure
- **Lists and Tables**: Prioritize structured data formats over paragraphs.
- **Signature/Type**: Clearly state inputs, outputs, types, and defaults.
- **Description**: Technical description of functionality.
## /.cursor/skills/doc_tutorial.md
---
name: doc_tutorial
description: Writing guide for Tutorial documentation — learning-oriented content that teaches users a skill through hands-on, concrete, step-by-step practice. Use when the goal is for the reader to acquire a new skill by doing, not just reading.
---
# Skill: Writing Tutorials (Learning-Oriented)
**Goal**: Allow the user to acquire a skill by doing.
## content_rules
- **Learning Experience**: Focus entirely on the learning experience of the user.
- **Concrete Steps**: Provide exact, ordered steps.
- **Immediate Results**: Ensure the user sees results immediately after actions.
- **Repetition**: Repeat actions only if necessary for muscle memory; otherwise, reference previous steps.
- **No Alternatives**: Provide exactly one way to do the task. Avoid "or you could do this."
- **Concrete Examples**: Use specific filenames, variable names, and code blocks. Avoid abstract concepts.
- **Resist Abstraction**: The temptation to generalize is strong — resist it. Tutorials proceed strictly from the concrete to the particular. Never introduce edge cases, advanced options, or generalizations mid-tutorial. Those belong in Explanation or Reference docs. Naming the failure mode: if you catch yourself writing "or, more generally..." — stop.
- **Robustness**: Every step must be tested and verifiable before publishing. A tutorial that breaks partway through is worse than no tutorial — it abandons the beginner at the worst moment. Tutorials must be actively maintained as the underlying software changes.
## structure
1. **Introduction**: "In this tutorial, you will learn how to..."
2. **Prerequisites**: Briefly list what is strictly needed.
3. **Steps**: Numbered sequence of actions.
4. **Conclusion**: "You have successfully..."
## /.cursor/skills/doc_workflow.md
---
name: doc_workflow
description: Global workflow and style rules that apply to all documentation types. Always load this file alongside the quadrant-specific skill file. Contains tone guidance, component usage, chunked execution protocol, and structural integrity rules.
---
# Global Documentation Workflow & Style
Apply these rules to ALL documentation tasks.
## 1. Tone and Focus
- **Your Knowledge**: Always make sure you have the relevant context, if you're writing technical documentation but do not have it (ex. code base access, libraries, sdks, diagrams) then always ask the user for it first.
- **Educational Focus**: _(Tutorials, How-to Guides, Reference only)_ Maintain a tone that is strictly factual, goal-oriented, and solution-focused. Explanation docs are exempt — they use a discursive, conceptual tone instead (see `doc_explanation.md`).
- **Technical Purity**: _(Tutorials, How-to Guides, Reference only)_ Describe features and implementation details solely. Focus on "how it works" and "how to use it." Explanation docs focus on "why it works this way" and are not bound by this rule.
- **Positive Framing**: Structure instructions to guide the user toward the correct path.
- **Contextual Awareness**: Assume the user is intelligent but lacks the specific context of this feature, design pattern, or other knowledge.
## 2. Component Usage
- **Cross-Referencing**: When referencing concepts explained elsewhere, utilize `<Callout>` components to link to existing documentation rather than restating the information.
- **Fumadocs Standard**: Use the default Fumadocs syntax for callouts. Adapt to other frameworks only if the project structure explicitly demands it.
- Example: `<Callout type="info">See [Concept Name](./path) for details.</Callout>`
- Types: `info`, `warn`, `error`, `tip`.
## 3. Execution Protocol (Chunked)
- **Phase 1: Plan**: Outline the file structure and section headers. Pause for confirmation.
- **Phase 2: Execution**: Write content in cohesive chunks (e.g., one complete section or logical step).
- **Phase 3: Review**: Stop after each cohesive chunk.
- Ask the user: "Review this section. Shall I proceed to [Next Section]?"
- If the user makes changes relevant to upcoming changes in the plan then take those changes into account.
- If the user makes changes to the current change then verify their change and prompt them if clarification is needed.
- If the user makes changes relevant to past changes then outline and suggest making necessary additional revisions to the user.
- **Phase 4: Commit**: Upon completing a logical set of changes (e.g., a full file or a major edit), suggest a Git commit message.
- Format: `scope: concise description of change`
## 4. Quadrant Integrity (Anti-Contamination)
Each quadrant must stay pure. Documentation naturally drifts — resist it.
- **If you find yourself explaining *why* something works inside a How-to Guide** → stop. Extract it to an Explanation doc and link to it instead.
- **If you find yourself listing all available options inside a Tutorial** → stop. That content belongs in Reference. Link to it.
- **If you find yourself writing step-by-step instructions inside an Explanation doc** → stop. Link to the relevant How-to Guide.
- **If your Reference entry starts coaching the user on what to do** → remove it. Link to a How-to Guide.
Cross-referencing is correct. Embedding foreign content is contamination. When in doubt, link — don't inline.
## 5. Structural Integrity
- **Cohesion**: Ensure every sentence serves the specific goal of the document type (Tutorial vs. Reference).
- **Location**: Always prefer existing file structure over prompting the user to restructure. Avoid any restructuring that would impact SEO. Place files in directories that match their intent (e.g., `/tutorials`, `/how-to`, `/reference`, `/explanation`). Suggest moving files if they are misplaced.
## /.githooks/pre-commit
```githooks/pre-commit path="/.githooks/pre-commit"
#!/usr/bin/env bash
# Pre-commit hook: block commits that would fail CI's format check.
# Install with: make install-hooks
set -euo pipefail
staged_rs=$(git diff --cached --name-only --diff-filter=ACM -- '*.rs' || true)
if [ -n "$staged_rs" ]; then
if ! command -v cargo >/dev/null 2>&1; then
echo "[pre-commit] cargo not found; skipping rustfmt check" >&2
else
if ! cargo fmt --all -- --check >/dev/null 2>&1; then
echo "[pre-commit] rustfmt would reformat files — CI will fail." >&2
echo " Run 'make fix-fmt' (or 'cargo fmt --all'), re-stage, and commit again." >&2
exit 1
fi
fi
fi
```
## /.github/assets/iii-intro-preview.gif
Binary file available at https://raw.githubusercontent.com/MotiaDev/motia/refs/heads/main/.github/assets/iii-intro-preview.gif
## /.github/assets/workers-add.gif
Binary file available at https://raw.githubusercontent.com/MotiaDev/motia/refs/heads/main/.github/assets/workers-add.gif
## /.github/assets/zero-integration.png
Binary file available at https://raw.githubusercontent.com/MotiaDev/motia/refs/heads/main/.github/assets/zero-integration.png
## /.github/pull_request_template.md
## What
<!-- Brief description of the change -->
## Why
<!-- Motivation or context -->
## Notes
<!-- Anything reviewers should know (breaking changes, migration steps, etc.) -->
## /.github/scripts/build_engine_publish_payload.py
```py path="/.github/scripts/build_engine_publish_payload.py"
#!/usr/bin/env python3
import argparse
import json
import pathlib
import sys
import tomllib
from typing import Any
def normalize_dependencies(raw_deps: Any) -> list[dict[str, Any]]:
if raw_deps in (None, ""):
return []
if isinstance(raw_deps, dict):
return [{"name": name, "version": version} for name, version in raw_deps.items()]
if isinstance(raw_deps, list):
return raw_deps
raise ValueError(f"`dependencies` must be a map or list, got {type(raw_deps).__name__}")
def derive_registry_function_name(function_id: str, metadata: dict[str, Any] | None) -> str:
metadata = metadata or {}
for key in ("registry_name", "name"):
value = metadata.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
return function_id
def _extract_array(payload: dict[str, Any], key: str) -> list[dict[str, Any]]:
value = payload.get(key, [])
if value is None:
return []
if not isinstance(value, list):
raise ValueError(f"`{key}` must be an array")
return value
def _read_yaml(path: pathlib.Path) -> Any:
import yaml
return yaml.safe_load(path.read_text(encoding="utf-8"))
def _schema_or_empty(value: Any) -> dict[str, Any]:
if value is None:
return {}
if isinstance(value, dict):
return value
raise ValueError("function schema fields must be objects or null")
def _metadata_or_empty(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _string_or_empty(value: Any) -> str:
return value if isinstance(value, str) else ""
def _normalize_registry_trigger_type(trigger_type: dict[str, Any]) -> dict[str, Any]:
# Field renames vs the pre-rework engine: `trigger_request_format` is now
# `configuration_schema`; `call_request_format` is now `request_schema`.
return {
"name": _string_or_empty(trigger_type.get("id")),
"description": _string_or_empty(trigger_type.get("description")),
"invocation_schema": _schema_or_empty(trigger_type.get("configuration_schema")),
"return_schema": _schema_or_empty(trigger_type.get("request_schema")),
"metadata": {},
}
def normalize_worker_interface(
*,
worker_name: str,
worker_info: dict[str, Any],
function_details: list[dict[str, Any]],
trigger_type_details: list[dict[str, Any]],
baseline_trigger_type_ids: list[str] | None = None,
) -> dict[str, list[dict[str, Any]]]:
"""Normalize the engine's enriched worker surface into the registry shape.
The shape change vs the pre-rework engine:
- `engine::workers::list` no longer carries per-worker `functions[]`; the
detail envelope from `engine::workers::info` is the source of truth.
- `engine::functions::list` is lean (no schemas / metadata); the caller
must fetch `engine::functions::info` per function for `request_schema`,
`response_schema`, `metadata`.
- `engine::trigger-types::list` was retired in favor of
`engine::triggers::list` (types) + `engine::triggers::info` (schemas).
Args:
worker_name: Sanity check that the info envelope is the right worker.
worker_info: Response from `engine::workers::info { name }`.
function_details: List of `engine::functions::info` responses, one
per function id reported by `worker_info`.
trigger_type_details: List of `engine::triggers::info` responses,
one per trigger type reported by `worker_info`.
baseline_trigger_type_ids: Trigger type ids present in the engine
BEFORE the target worker was reloaded — used to subtract
mandatory-worker types (e.g. iii-observability's `log`).
"""
worker = worker_info.get("worker") or {}
if not isinstance(worker, dict):
raise ValueError("worker_info `worker` must be an object")
actual_name = worker.get("name") or worker.get("id")
if actual_name != worker_name:
raise ValueError(
f"worker_info envelope is for {actual_name!r}, expected {worker_name!r}"
)
functions = []
for details in function_details:
if not isinstance(details, dict):
continue
function_id = details.get("function_id")
if not isinstance(function_id, str):
continue
metadata = details.get("metadata") or {}
functions.append(
{
"name": derive_registry_function_name(function_id, metadata),
"description": _string_or_empty(details.get("description")),
"request_schema": _schema_or_empty(details.get("request_schema")),
"response_schema": _schema_or_empty(details.get("response_schema")),
"metadata": _metadata_or_empty(metadata),
}
)
baseline_ids = set(baseline_trigger_type_ids or [])
triggers = []
for trigger_type in trigger_type_details:
if not isinstance(trigger_type, dict):
continue
tt_id = trigger_type.get("id")
if not isinstance(tt_id, str) or tt_id.startswith("engine::"):
continue
if tt_id in baseline_ids:
continue
triggers.append(_normalize_registry_trigger_type(trigger_type))
return {"functions": functions, "triggers": triggers}
def build_payload(
*,
repo_root: pathlib.Path,
worker: str,
worker_dir: pathlib.Path,
expected_version: str,
registry_tag: str,
repo_url: str,
interface: dict[str, Any],
) -> dict[str, Any]:
engine_manifest = repo_root / "engine" / "Cargo.toml"
if not engine_manifest.exists():
raise ValueError(f"{engine_manifest} not found")
try:
engine_version = tomllib.loads(engine_manifest.read_text(encoding="utf-8"))["package"]["version"]
except KeyError as exc:
raise ValueError(f"{engine_manifest}: missing [package].version ({exc})") from exc
if expected_version and expected_version != engine_version:
raise ValueError(
f"engine worker version mismatch: input is {expected_version}, engine/Cargo.toml is {engine_version}"
)
manifest_path = repo_root / worker_dir / "iii.worker.yaml"
if not manifest_path.exists():
raise ValueError(f"{manifest_path} not found")
meta = _read_yaml(manifest_path) or {}
if meta.get("type") != "engine":
raise ValueError(
f"{worker}: iii.worker.yaml type must be 'engine' (got {meta.get('type')!r})"
)
readme_path = repo_root / worker_dir / "README.md"
readme = readme_path.read_text(encoding="utf-8") if readme_path.exists() else ""
manifest_name = meta.get("name")
worker_name = (
manifest_name.strip()
if isinstance(manifest_name, str) and manifest_name.strip()
else worker
)
return {
"worker_name": worker_name,
"version": engine_version,
"tag": registry_tag or "latest",
"type": "engine",
"readme": readme,
"repo": repo_url,
"description": meta.get("description", ""),
"dependencies": normalize_dependencies(meta.get("dependencies")),
"config": meta.get("config") or {},
"functions": interface.get("functions") or [],
"triggers": interface.get("triggers") or [],
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--worker", required=True)
parser.add_argument("--worker-dir", required=True)
parser.add_argument("--expected-version", default="")
parser.add_argument("--registry-tag", default="latest")
parser.add_argument("--repo-url", required=True)
parser.add_argument("--interface-json", required=True)
parser.add_argument("--repo-root", default=".")
parser.add_argument("--out", default="payload.json")
args = parser.parse_args()
interface = json.loads(pathlib.Path(args.interface_json).read_text(encoding="utf-8"))
payload = build_payload(
repo_root=pathlib.Path(args.repo_root),
worker=args.worker,
worker_dir=pathlib.Path(args.worker_dir),
expected_version=args.expected_version,
registry_tag=args.registry_tag,
repo_url=args.repo_url,
interface=interface,
)
pathlib.Path(args.out).write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
print(json.dumps({k: v for k, v in payload.items() if k != "readme"}, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())
```
## /.github/scripts/build_skills_payload.py
```py path="/.github/scripts/build_skills_payload.py"
#!/usr/bin/env python3
"""Build the POST /w/<slug>/skills payload from a worker directory.
Walks the worker's top-of-tree skill doc plus ``<worker>/skills/**/*.md`` and
produces the JSON body expected by the workers-registry endpoint. Skill paths
map to payload keys as:
<worker>/skills/SKILL.md -> "index.md" (preferred top-of-tree)
<worker>/skills/index.md -> "index.md" (legacy)
<worker>/skill.md -> "index.md" (legacy)
<worker>/skills/<rel>.md -> "skills/<rel>.md"
When more than one top-of-tree candidate is present the highest-precedence one
wins (``skills/SKILL.md`` > ``skills/index.md`` > ``skill.md``) and a GitHub
Actions warning is emitted.
If no non-empty markdown is found the script writes ``skip=true`` to
``$GITHUB_OUTPUT`` (so the calling workflow can gate the POST step off) and
exits cleanly; the API rejects payloads that omit both ``skills`` and
``prompts``, and ``skills: {}`` would be a destructive "clear all" call which is
wrong on a fresh publish.
"""
import argparse
import json
import os
import pathlib
import re
import sys
KEY_RE = re.compile(r"^[a-z0-9][a-z0-9._/\-]*\.md{{contextString}}quot;, re.IGNORECASE)
def collect_skills(worker_root: pathlib.Path) -> dict[str, str]:
"""Return a ``{payload-key: markdown-body}`` map for one worker directory.
The top-of-tree resolution order is ``skills/SKILL.md`` then
``skills/index.md`` then ``skill.md``; whichever wins is published under the
``index.md`` payload key. If more than one is present a GitHub Actions
warning is emitted and the highest-precedence file wins. Empty bodies are
skipped silently so blank placeholder files don't end up in the registry.
"""
skills: dict[str, str] = {}
leaves_dir = worker_root / "skills"
skill_md = leaves_dir / "SKILL.md"
skills_index = leaves_dir / "index.md"
intro = worker_root / "skill.md"
# Preferred new convention is skills/SKILL.md; the skills/index.md and
# skill.md forms are legacy. Whichever wins maps to the "index.md" key.
top_candidates = [c for c in (skill_md, skills_index, intro) if c.is_file()]
if top_candidates:
top = top_candidates[0]
body = top.read_text(encoding="utf-8")
if body.strip():
skills["index.md"] = body
if len(top_candidates) > 1:
present = ", ".join(
c.relative_to(worker_root).as_posix() for c in top_candidates
)
print(
f"::warning::{worker_root.name}: multiple top-of-tree skill "
f"files present ({present}); using "
f"{top.relative_to(worker_root).as_posix()} as the top-of-tree."
)
if leaves_dir.is_dir():
for path in sorted(leaves_dir.rglob("*.md")):
if path in (skill_md, skills_index):
continue
rel = path.relative_to(worker_root).as_posix()
if not KEY_RE.match(rel):
raise ValueError(
f"skill path rejected by server regex: {rel} "
"(must match /^[a-z0-9][a-z0-9._/\\-]*\\.md$/i)"
)
body = path.read_text(encoding="utf-8")
if not body.strip():
continue
skills[rel] = body
return skills
def _signal_skip(worker: str) -> None:
gha_out = os.environ.get("GITHUB_OUTPUT")
if gha_out:
with open(gha_out, "a", encoding="utf-8") as f:
f.write("skip=true\n")
print(f"::notice::no skills found for {worker}; skipping POST /w/.../skills")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--worker-dir",
help="Path to worker source directory relative to repo root "
"(e.g. engine/src/workers/state).",
)
parser.add_argument(
"--worker",
help="(deprecated) Same as --worker-dir.",
)
parser.add_argument(
"--version",
required=True,
help="Worker version tag or semver to attach this snapshot to (e.g. latest, next, 1.2.3).",
)
parser.add_argument("--out", default="skills-payload.json")
parser.add_argument(
"--repo-root",
default=".",
help="Repo root containing the worker folder (default: cwd).",
)
args = parser.parse_args()
worker_dir = args.worker_dir or args.worker
if not worker_dir:
parser.error("one of --worker-dir or --worker is required")
worker_root = pathlib.Path(args.repo_root) / worker_dir
if not worker_root.is_dir():
print(f"::error::worker directory not found: {worker_root}", file=sys.stderr)
return 1
try:
skills = collect_skills(worker_root)
except ValueError as exc:
print(f"::error::{exc}", file=sys.stderr)
return 1
out_path = pathlib.Path(args.out)
if not skills:
out_path.write_text("{}\n", encoding="utf-8")
_signal_skip(worker_dir)
return 0
payload = {"version": args.version, "skills": skills}
out_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
print(f"::notice::collected {len(skills)} skill file(s) for {worker_dir}")
return 0
if __name__ == "__main__":
sys.exit(main())
```
## /.github/scripts/bump_manifests.py
```py path="/.github/scripts/bump_manifests.py"
#!/usr/bin/env python3
"""Rewrite every release-bumped manifest in lockstep.
Pure functions live at module top so they can be unit-tested. The CLI at
the bottom drives the rewrites the create-tag workflow used to perform
inline with sed/jq.
"""
from __future__ import annotations
import re
_CARGO_PACKAGE_VERSION_RE = re.compile(r'^version = "[^"]*"', re.MULTILINE)
def bump_cargo_package_version(text: str, new_version: str) -> str:
"""Replace the first top-level ``version = "..."`` line in a Cargo manifest."""
m = _CARGO_PACKAGE_VERSION_RE.search(text)
if m is None:
raise ValueError("no top-level version line in Cargo manifest")
start, end = m.span()
return f'{text[:start]}version = "{new_version}"{text[end:]}'
def bump_cargo_workspace_dep_version(text: str, dep_name: str, new_version: str) -> str:
"""Replace the ``version = "..."`` value inside a ``dep_name = { ... }`` line.
Matches a single workspace-dependency entry whose key is exactly
``dep_name`` and rewrites the embedded ``version`` field. Other fields
on the line (``path``, ``features``, ...) are preserved.
"""
line_re = re.compile(
rf'^(?P<lead>{re.escape(dep_name)}\s*=\s*\{{[^}}\n]*?version\s*=\s*")[^"]*(?P<tail>"[^}}\n]*\}})',
re.MULTILINE,
)
m = line_re.search(text)
if m is None:
raise ValueError(f"no workspace dependency entry for {dep_name!r}")
return f'{text[:m.start()]}{m.group("lead")}{new_version}{m.group("tail")}{text[m.end():]}'
_JSON_TOP_VERSION_RE = re.compile(r'^(\s*)"version"(\s*):(\s*)"[^"]*"', re.MULTILINE)
def bump_json_top_level_version(text: str, new_version: str) -> str:
"""Replace the first top-level ``"version": "..."`` line in a JSON file.
Operates textually to preserve formatting. The npm/iii workflow uses
``jq`` today; we replace it with this so the workflow has a single
invocation point and the behaviour is unit-tested.
"""
m = _JSON_TOP_VERSION_RE.search(text)
if m is None:
raise ValueError("no top-level version key in JSON manifest")
indent, sp1, sp2 = m.group(1), m.group(2), m.group(3)
replacement = f'{indent}"version"{sp1}:{sp2}"{new_version}"'
return f'{text[:m.start()]}{replacement}{text[m.end():]}'
def bump_pep440_dep_pin(text: str, dep_name: str, new_pep440: str) -> str:
"""Replace ``"<dep_name>==<old>"`` with ``"<dep_name>==<new_pep440>"``.
Replaces a pinned PEP 440 dependency version in a pyproject.toml
``dependencies = [...]`` array.
"""
line_re = re.compile(rf'"{re.escape(dep_name)}==[^"]*"')
m = line_re.search(text)
if m is None:
raise ValueError(f"no pinned dependency entry for {dep_name!r}")
return f'{text[:m.start()]}"{dep_name}=={new_pep440}"{text[m.end():]}'
_GO_SDK_VERSION_RE = re.compile(r'const sdkVersion = "[^"]*"')
def bump_go_const_version(text: str, new_version: str) -> str:
"""Replace the ``const sdkVersion = "..."`` line in the Go SDK client.
Go modules are versioned by their git tag, but the const is reported in
worker metadata, so it must stay in lockstep with the other SDKs. Uses
the raw semver string (not the PEP 440 form).
"""
m = _GO_SDK_VERSION_RE.search(text)
if m is None:
raise ValueError("no sdkVersion const in Go client")
return f'{text[:m.start()]}const sdkVersion = "{new_version}"{text[m.end():]}'
from pathlib import Path
_CARGO_PACKAGE_FILES = (
"engine/Cargo.toml",
"sdk/packages/rust/iii/Cargo.toml",
"sdk/packages/rust/observability/Cargo.toml",
"sdk/packages/rust/helpers/Cargo.toml",
"console/packages/console-rust/Cargo.toml",
)
_JSON_PACKAGE_FILES = (
"sdk/packages/node/iii/package.json",
"sdk/packages/node/iii-browser/package.json",
"sdk/packages/node/observability/package.json",
"sdk/packages/node/helpers/package.json",
)
def rewrite_all(root: Path, new_version: str, new_py_version: str) -> None:
"""Rewrite every release manifest under ``root`` in lockstep."""
# Cargo package versions
for rel in _CARGO_PACKAGE_FILES:
path = root / rel
path.write_text(bump_cargo_package_version(path.read_text(), new_version))
# Workspace root: bump the workspace.package version and the internal
# workspace-dep version pins. The pins are plain "X.Y.Z" requirements, so
# they must move to the prerelease too — otherwise `cargo` can't match the
# prerelease candidate (e.g. ^0.19.4 won't accept 0.19.4-alpha.1).
workspace_path = root / "Cargo.toml"
body = bump_cargo_package_version(workspace_path.read_text(), new_version)
body = bump_cargo_workspace_dep_version(body, "iii-observability", new_version)
body = bump_cargo_workspace_dep_version(body, "iii-helpers", new_version)
workspace_path.write_text(body)
# JSON package versions
for rel in _JSON_PACKAGE_FILES:
path = root / rel
path.write_text(bump_json_top_level_version(path.read_text(), new_version))
# Python iii: top-level version + internal pins (== requirements must
# move to the prerelease alongside the packages they point at).
# Note: iii-observability is no longer a direct dep of Python iii (replaced
# by iii-helpers after the observability-into-helpers refactor).
py_iii = root / "sdk/packages/python/iii/pyproject.toml"
body = bump_cargo_package_version(py_iii.read_text(), new_py_version)
body = bump_pep440_dep_pin(body, "iii-helpers", new_py_version)
py_iii.write_text(body)
# Python observability: top-level version + iii-helpers dep pin (shim must
# depend on the same version of iii-helpers being released alongside it).
py_obs = root / "sdk/packages/python/observability/pyproject.toml"
body = bump_cargo_package_version(py_obs.read_text(), new_py_version)
body = bump_pep440_dep_pin(body, "iii-helpers", new_py_version)
py_obs.write_text(body)
# Go SDK: keep the reported sdkVersion const in lockstep. The module
# itself is versioned by its git tag; this only updates the metadata
# const, using the raw semver (not the PEP 440) version.
go_client = root / "sdk/packages/go/iii/client.go"
go_client.write_text(bump_go_const_version(go_client.read_text(), new_version))
# Python helpers: top-level version only
py_helpers = root / "sdk/packages/python/helpers/pyproject.toml"
py_helpers.write_text(bump_cargo_package_version(py_helpers.read_text(), new_py_version))
import argparse
import sys
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", required=True, type=Path)
parser.add_argument("--version", required=True)
parser.add_argument("--python-version", required=True)
args = parser.parse_args(argv)
rewrite_all(root=args.root, new_version=args.version, new_py_version=args.python_version)
print(f"Bumped manifests to {args.version} (python: {args.python_version})")
return 0
if __name__ == "__main__":
sys.exit(main())
```
## /.github/scripts/calculate_release_version.py
```py path="/.github/scripts/calculate_release_version.py"
#!/usr/bin/env python3
"""Compute the next release version for the create-tag workflow.
Pure functions live at module top so they can be unit-tested. The CLI at
the bottom reads inputs from environment variables / git and emits the
GitHub Actions outputs the workflow needs.
"""
from __future__ import annotations
import argparse
import os
import re
import subprocess
import sys
from dataclasses import dataclass
PRERELEASE_LABELS = ("alpha", "beta", "rc", "next")
BUMP_RANK = {"patch": 1, "minor": 2, "major": 3}
PRERELEASE_RE = re.compile(r"^(\d+\.\d+\.\d+)-([a-z]+)\.(\d+){{contextString}}quot;)
BASE_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+){{contextString}}quot;)
@dataclass(frozen=True)
class Version:
major: int
minor: int
patch: int
prerelease_label: str | None = None
prerelease_num: int | None = None
@property
def base(self) -> str:
return f"{self.major}.{self.minor}.{self.patch}"
def __str__(self) -> str:
if self.prerelease_label is not None:
return f"{self.base}-{self.prerelease_label}.{self.prerelease_num}"
return self.base
def parse_version(s: str) -> Version:
m = PRERELEASE_RE.match(s)
if m:
base, label, num = m.group(1), m.group(2), int(m.group(3))
bm = BASE_RE.match(base)
assert bm is not None
return Version(int(bm.group(1)), int(bm.group(2)), int(bm.group(3)), label, num)
bm = BASE_RE.match(s)
if bm:
return Version(int(bm.group(1)), int(bm.group(2)), int(bm.group(3)))
raise ValueError(f"unrecognized version string: {s!r}")
def bump_base(base: str, bump_type: str) -> str:
m = BASE_RE.match(base)
if not m:
raise ValueError(f"not a base version: {base!r}")
major, minor, patch = int(m.group(1)), int(m.group(2)), int(m.group(3))
if bump_type == "major":
return f"{major + 1}.0.0"
if bump_type == "minor":
return f"{major}.{minor + 1}.0"
if bump_type == "patch":
return f"{major}.{minor}.{patch + 1}"
raise ValueError(f"unknown bump_type: {bump_type!r}")
def detect_current_level(current_base: str, latest_stable: str | None) -> str | None:
"""Classify how `current_base` relates to the last stable release.
Returns "major" / "minor" / "patch" depending on which component grew,
or None when we can't tell (no stable yet, or current is older).
"""
if latest_stable is None:
return None
cur = parse_version(current_base)
stable = parse_version(latest_stable)
if cur.major > stable.major:
return "major"
if cur.major == stable.major and cur.minor > stable.minor:
return "minor"
if cur.major == stable.major and cur.minor == stable.minor and cur.patch > stable.patch:
return "patch"
return None
def next_prerelease_counter(
new_base: str,
prerelease: str,
existing_tags: list[str],
tag_prefix: str,
) -> int:
"""Find the next prerelease counter by scanning existing tags."""
pattern = re.compile(
rf"^{re.escape(tag_prefix)}/v{re.escape(new_base)}-{re.escape(prerelease)}\.(\d+){{contextString}}quot;
)
nums = [int(m.group(1)) for t in existing_tags if (m := pattern.match(t))]
return max(nums) + 1 if nums else 1
def next_dry_run_counter(base: str, existing_tags: list[str], tag_prefix: str) -> int:
pattern = re.compile(rf"^{re.escape(tag_prefix)}/v{re.escape(base)}-dry-run\.(\d+){{contextString}}quot;)
nums = [int(m.group(1)) for t in existing_tags if (m := pattern.match(t))]
return max(nums) + 1 if nums else 1
def highest_inflight_prerelease_base(
existing_tags: list[str],
tag_prefix: str,
latest_stable: str | None,
) -> str | None:
"""Highest prerelease base, under ``tag_prefix``, strictly above the latest stable.
Drives the ``bump="none"`` path: it lets a release iterate an in-flight
prerelease train (e.g. ``0.19.5-next.1`` -> ``0.19.5-next.2``) without
advancing the base. Any prerelease channel counts toward the base
detection, so ``next`` and ``rc`` of the same upcoming release share it.
Returns ``None`` when no prerelease tag sits above ``latest_stable`` — the
alpha-from-stable case, where every prerelease anchors on the current
stable — so callers fall back to anchoring on the stable release itself.
"""
pattern = re.compile(rf"^{re.escape(tag_prefix)}/v(\d+\.\d+\.\d+)-[a-z]+\.\d+{{contextString}}quot;)
floor = parse_version(latest_stable) if latest_stable else None
best: Version | None = None
for t in existing_tags:
if not (m := pattern.match(t)):
continue
base = parse_version(m.group(1))
key = (base.major, base.minor, base.patch)
if floor is not None and key <= (floor.major, floor.minor, floor.patch):
continue
if best is None or key > (best.major, best.minor, best.patch):
best = base
return best.base if best else None
def calculate_version(
current: str,
bump_type: str,
prerelease: str,
latest_stable: str | None,
existing_tags: list[str],
tag_prefix: str,
) -> str:
"""Decide the next version, accounting for the current prerelease train.
Rules:
- `none` never advances the base: it iterates the prerelease
counter on the in-flight train (the highest prerelease base above
the latest stable), or on the latest stable when no such train
exists (alpha-from-stable). Requires a prerelease label.
- Promoting a prerelease to stable (prerelease == "none" and current
has a prerelease label) keeps the base as-is.
- `patch` always increments the patch component of the current
base, starting a fresh prerelease counter. Patch never iterates
an existing train.
- `minor` / `major` in the same prerelease channel are
level-aware: if the current base already represents a bump at
the same or higher level (relative to the latest stable), keep
the base and iterate the counter; if the request escalates
beyond the current level, restart from the latest stable.
- Otherwise apply the requested bump to the current base.
"""
cur = parse_version(current)
if bump_type == "none":
# Iterate-only mode: never advances the base, only the prerelease
# counter. Two scenarios resolve to the same operation:
# - Continue an in-flight train (create-tag): if a prerelease tag
# sits above the latest stable, anchor on its base so repeated
# `none` releases iterate 0.19.5-next.1 -> .2 -> .3 instead of
# re-bumping the base. To START a train, use patch/minor/major.
# - Alpha-from-stable: when no prerelease train exists above the
# latest stable, anchor on the stable release (fall back to the
# current base when there is no stable yet).
if prerelease not in PRERELEASE_LABELS:
raise ValueError(
f"bump_type 'none' requires a prerelease label, got {prerelease!r}"
)
inflight = highest_inflight_prerelease_base(existing_tags, tag_prefix, latest_stable)
new_base = inflight or latest_stable or cur.base
counter = next_prerelease_counter(new_base, prerelease, existing_tags, tag_prefix)
return f"{new_base}-{prerelease}.{counter}"
if bump_type not in BUMP_RANK:
raise ValueError(f"unknown bump_type: {bump_type!r}")
if prerelease != "none" and prerelease not in PRERELEASE_LABELS:
raise ValueError(f"unknown prerelease: {prerelease!r}")
if prerelease == "none" and cur.prerelease_label is not None:
# Promote prerelease to stable.
return cur.base
same_channel = (
prerelease != "none"
and cur.prerelease_label == prerelease
)
if same_channel and bump_type != "patch":
current_level = detect_current_level(cur.base, latest_stable)
if current_level is not None and BUMP_RANK[bump_type] <= BUMP_RANK[current_level]:
new_base = cur.base
else:
# No stable to anchor against, or the requested bump
# escalates past the current train: restart from the
# latest stable when available, else bump the current base.
anchor = latest_stable if (latest_stable and current_level is not None) else cur.base
new_base = bump_base(anchor, bump_type)
else:
new_base = bump_base(cur.base, bump_type)
if prerelease == "none":
return new_base
counter = next_prerelease_counter(new_base, prerelease, existing_tags, tag_prefix)
return f"{new_base}-{prerelease}.{counter}"
def to_pep440(version: str) -> str:
m = PRERELEASE_RE.match(version)
if not m:
return version
base, label, num = m.group(1), m.group(2), m.group(3)
mapping = {"rc": f"rc{num}", "alpha": f"a{num}", "beta": f"b{num}", "next": f".dev{num}"}
suffix = mapping.get(label)
if suffix is None:
return version
return f"{base}{suffix}"
def latest_stable_from_tags(tags: list[str], tag_prefix: str) -> str | None:
"""Pick the highest stable (non-prerelease, non-dry-run) tag."""
pattern = re.compile(rf"^{re.escape(tag_prefix)}/v(\d+\.\d+\.\d+){{contextString}}quot;)
stables = []
for t in tags:
if (m := pattern.match(t)):
stables.append(parse_version(m.group(1)))
if not stables:
return None
stables.sort(key=lambda v: (v.major, v.minor, v.patch))
return stables[-1].base
# ---------- CLI ----------
def _read_cargo_version(path: str) -> str:
with open(path, encoding="utf-8") as f:
for line in f:
if line.startswith("version = "):
return line.split('"')[1]
raise RuntimeError(f"no version in {path}")
def _git_tags() -> list[str]:
out = subprocess.run(
["git", "tag", "-l"], capture_output=True, text=True, check=True
)
return [t for t in out.stdout.splitlines() if t]
def _emit(name: str, value: str) -> None:
gho = os.environ.get("GITHUB_OUTPUT")
if gho:
with open(gho, "a", encoding="utf-8") as f:
f.write(f"{name}={value}\n")
print(f"{name}={value}")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--target", required=True)
parser.add_argument("--bump", required=True, choices=list(BUMP_RANK) + ["none"])
parser.add_argument("--prerelease", required=True)
parser.add_argument(
"--counter-tag-prefix",
default=None,
help=(
"Tag prefix used when scanning for the prerelease counter "
"(default: --target). Lets alpha releases accumulate under a "
"separate namespace, e.g. iii-alpha, without colliding with the "
"official iii/v* tags."
),
)
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--current-version-file", required=True)
args = parser.parse_args(argv)
current = _read_cargo_version(args.current_version_file)
tags = _git_tags()
stable_prefix = args.target
counter_prefix = args.counter_tag_prefix or args.target
latest_stable = latest_stable_from_tags(tags, stable_prefix)
new_ver = calculate_version(
current=current,
bump_type=args.bump,
prerelease=args.prerelease,
latest_stable=latest_stable,
existing_tags=tags,
tag_prefix=counter_prefix,
)
if args.dry_run:
base_ver = new_ver.split("-", 1)[0]
counter = next_dry_run_counter(base_ver, tags, counter_prefix)
new_ver = f"{base_ver}-dry-run.{counter}"
py_ver = to_pep440(new_ver)
is_prerelease = args.prerelease != "none"
npm_tag = args.prerelease if is_prerelease else "latest"
_emit("version", new_ver)
_emit("python_version", py_ver)
_emit("tag", f"{counter_prefix}/v{new_ver}")
_emit("current", current)
_emit("is_prerelease", "true" if is_prerelease else "false")
_emit("npm_tag", npm_tag)
print(f"::notice::{counter_prefix}: {current} -> {new_ver} (python: {py_ver})", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
```
## /.github/scripts/collect_engine_worker_interface.py
```py path="/.github/scripts/collect_engine_worker_interface.py"
#!/usr/bin/env python3
import argparse
import json
import pathlib
import subprocess
import sys
import time
from build_engine_publish_payload import normalize_worker_interface
def count_worker_matches(workers_json: dict[str, object], worker_name: str) -> int:
workers = workers_json.get("workers", [])
if not isinstance(workers, list):
return 0
return sum(
1
for worker in workers
if isinstance(worker, dict)
and (worker.get("name") == worker_name or worker.get("id") == worker_name)
)
def run_iii(function_path: str, payload: dict[str, object]) -> dict[str, object]:
completed = subprocess.run(
[
"iii",
"trigger",
function_path,
"--json",
json.dumps(payload),
],
check=True,
text=True,
capture_output=True,
timeout=60,
)
return json.loads(completed.stdout)
def wait_for_worker(worker_name: str, wait_seconds: int) -> dict[str, object]:
# `engine::workers::list` is lean post-rework (no per-worker `functions[]`),
# but we only need it to confirm the worker is reachable. The function
# surface is fetched via `engine::workers::info` below.
deadline = time.monotonic() + wait_seconds
workers_json = run_iii("engine::workers::list", {})
while count_worker_matches(workers_json, worker_name) != 1 and time.monotonic() < deadline:
time.sleep(2)
workers_json = run_iii("engine::workers::list", {})
return workers_json
def collect_worker_info(worker_name: str) -> dict[str, object]:
# The detailed info envelope is the source of truth for a worker's surface
# in the new engine_fn world. The `worker` field mirrors a workers::list
# row; `functions`, `trigger_types`, and `registered_triggers` arrays are
# appended.
return run_iii("engine::workers::info", {"name": worker_name})
# `engine::functions::info` accepts at most 32 ids per `function_ids` batch.
FUNCTIONS_INFO_BATCH_MAX = 32
def collect_function_details(function_ids: list[str]) -> list[dict[str, object]]:
# `engine::functions::list` is lean post-rework — schemas live behind
# `engine::functions::info`. Its `function_ids` batch mode fetches up
# to 32 details per round-trip; a per-item `error` marker ("forbidden"
# / "not_found") fails the collection, matching the old per-id loop.
details: list[dict[str, object]] = []
for start in range(0, len(function_ids), FUNCTIONS_INFO_BATCH_MAX):
chunk = function_ids[start : start + FUNCTIONS_INFO_BATCH_MAX]
try:
response = run_iii("engine::functions::info", {"function_ids": chunk})
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, json.JSONDecodeError) as exc:
raise RuntimeError(
f"could not collect function info for batch {chunk!r}: {exc}"
) from exc
entries = response.get("functions")
if not isinstance(entries, list):
raise RuntimeError(
f"functions::info batch response missing `functions` array: {response!r}"
)
for entry in entries:
if not isinstance(entry, dict):
raise RuntimeError(
f"functions::info batch entry is not an object: {entry!r}"
)
if entry.get("error"):
raise RuntimeError(
f"could not collect function info for "
f"{entry.get('function_id')!r}: {entry['error']}"
)
details.append(entry)
return details
def collect_trigger_type_details(trigger_type_ids: list[str]) -> list[dict[str, object]]:
# Same shape concern as functions: `engine::triggers::list` is lean, so
# we fetch per-type schemas via `engine::triggers::info`. Trigger types
# are the primary content for infrastructure workers (iii-http, iii-cron,
# iii-bridge, ...) — failing closed if any lookup errors out is
# intentional to avoid masking the real surface of the worker.
details: list[dict[str, object]] = []
for tt_id in trigger_type_ids:
try:
details.append(run_iii("engine::triggers::info", {"id": tt_id}))
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, json.JSONDecodeError) as exc:
raise RuntimeError(
f"could not collect trigger type info for {tt_id!r}: {exc}"
) from exc
return details
def collect_baseline_trigger_type_ids(baseline_path: pathlib.Path) -> list[str]:
# The baseline snapshot is captured BEFORE the target worker is reloaded
# into the engine, so it contains the always-on (`mandatory`) trigger
# types. We use the lean `triggers::list` shape here — only ids are
# needed for diffing.
if not baseline_path.exists():
return []
raw = json.loads(baseline_path.read_text(encoding="utf-8"))
triggers = raw.get("triggers") or raw.get("trigger_types") or []
return [t.get("id") for t in triggers if isinstance(t, dict) and isinstance(t.get("id"), str)]
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--worker", required=True)
parser.add_argument("--out", default="worker-interface.json")
parser.add_argument("--wait-seconds", type=int, default=0)
parser.add_argument("--trigger-types-baseline", default="")
args = parser.parse_args()
baseline_ids: list[str] = []
if args.trigger_types_baseline:
baseline_ids = collect_baseline_trigger_type_ids(
pathlib.Path(args.trigger_types_baseline)
)
wait_for_worker(args.worker, args.wait_seconds)
worker_info = collect_worker_info(args.worker)
functions = worker_info.get("functions") or []
if not isinstance(functions, list):
raise ValueError("workers::info `functions` must be an array")
function_ids = [
f.get("function_id")
for f in functions
if isinstance(f, dict) and isinstance(f.get("function_id"), str)
]
trigger_types = worker_info.get("trigger_types") or []
if not isinstance(trigger_types, list):
raise ValueError("workers::info `trigger_types` must be an array")
trigger_type_ids = [
t.get("id")
for t in trigger_types
if isinstance(t, dict) and isinstance(t.get("id"), str)
]
function_details = collect_function_details(function_ids)
trigger_type_details = collect_trigger_type_details(trigger_type_ids)
interface = normalize_worker_interface(
worker_name=args.worker,
worker_info=worker_info,
function_details=function_details,
trigger_type_details=trigger_type_details,
baseline_trigger_type_ids=baseline_ids,
)
pathlib.Path(args.out).write_text(json.dumps(interface, indent=2) + "\n", encoding="utf-8")
print(json.dumps(interface, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())
```
## /.github/scripts/discover_engine_worker_skills.py
```py path="/.github/scripts/discover_engine_worker_skills.py"
#!/usr/bin/env python3
"""Discover workers with publishable skill bundles.
Scans the repo for ``iii.worker.yaml`` manifests that sit next to a non-empty
``skills/`` tree (``skills/SKILL.md`` or the legacy ``skills/index.md`` /
``skill.md``) and emits a GitHub Actions matrix JSON with ``worker_dir`` (path
relative to repo root) and ``slug`` (registry name from the manifest). Workers
whose skills tree has no non-empty markdown are omitted.
"""
from __future__ import annotations
import argparse
import json
import os
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
from build_skills_payload import collect_skills # noqa: E402
_SKIP_DIR_NAMES = frozenset(
{
".git",
"node_modules",
"target",
".turbo",
"dist",
"build",
}
)
def _read_slug(manifest_path: pathlib.Path) -> str:
try:
import yaml
except ImportError as exc:
raise RuntimeError("pyyaml is required: pip install pyyaml") from exc
data = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) or {}
name = data.get("name")
if not isinstance(name, str) or not name.strip():
raise ValueError(f"{manifest_path}: missing or empty `name` field")
return name.strip()
def _iter_worker_manifests(repo_root: pathlib.Path):
for path in sorted(repo_root.rglob("iii.worker.yaml")):
if any(part in _SKIP_DIR_NAMES for part in path.relative_to(repo_root).parts):
continue
yield path
def discover(repo_root: pathlib.Path) -> list[dict[str, str]]:
entries: list[dict[str, str]] = []
seen_dirs: set[str] = set()
for manifest in _iter_worker_manifests(repo_root):
worker_dir = manifest.parent
worker_key = worker_dir.relative_to(repo_root).as_posix()
if worker_key in seen_dirs:
continue
skills_dir = worker_dir / "skills"
intro = worker_dir / "skill.md"
if not skills_dir.is_dir() and not intro.is_file():
continue
try:
slug = _read_slug(manifest)
skills = collect_skills(worker_dir)
except ValueError as exc:
print(f"::error::{exc}", file=sys.stderr)
raise
if not skills:
print(
f"::notice::{worker_key}: "
"no non-empty skill markdown; skipping"
)
continue
seen_dirs.add(worker_key)
entries.append(
{
"worker_dir": worker_key,
"slug": slug,
}
)
return sorted(entries, key=lambda entry: entry["worker_dir"])
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--repo-root",
default=".",
help="Repository root (default: cwd).",
)
parser.add_argument(
"--out",
default="-",
help="Write matrix JSON here, or '-' for stdout (default).",
)
args = parser.parse_args()
repo_root = pathlib.Path(args.repo_root).resolve()
matrix = {"include": discover(repo_root)}
text = json.dumps(matrix)
if args.out == "-":
print(text)
else:
pathlib.Path(args.out).write_text(text + "\n", encoding="utf-8")
gha_out = os.environ.get("GITHUB_OUTPUT")
if gha_out:
with open(gha_out, "a", encoding="utf-8") as f:
f.write(f"matrix={text}\n")
f.write(f"has_workers={'true' if matrix['include'] else 'false'}\n")
print(
f"::notice::discovered {len(matrix['include'])} worker(s) with publishable skills"
)
return 0
if __name__ == "__main__":
sys.exit(main())
```
## /.github/scripts/license-agreement-check.mjs
```mjs path="/.github/scripts/license-agreement-check.mjs"
import { readFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
export const CHECKBOX_TEXT =
"I license my contributions to this repository under Apache 2.0, and I have all necessary rights over the code I am contributing.";
export const STATUS_CONTEXT = "license-agreement";
// The workflow posts as whatever identity mints its token: github-actions[bot]
// under GITHUB_TOKEN, or <app-slug>[bot] under a GitHub App installation token.
// Match any bot login so the sticky comment is recognized and updated in place
// regardless of which identity is in use.
function isBotLogin(login = "") {
return login.endsWith("[bot]");
}
export function hasCheckedCheckbox(prBody = "") {
const escaped = CHECKBOX_TEXT.replace(/[.*+?^${}()|[\]\\]/g, "\\{{contextString}}amp;");
return new RegExp(`^- \\[x\\] ${escaped}\\s*$`, "im").test(prBody);
}
export function isInContributorsFile(username, content = "") {
return content
.split("\n")
.some((line) => line.trim() === `- @${username}` || line.trim().startsWith(`- @${username} `));
}
export function evaluateAgreement({ prBody = "", inContributorsFile = false } = {}) {
const checkboxChecked = hasCheckedCheckbox(prBody);
const acknowledged = inContributorsFile || checkboxChecked;
return { acknowledged, checkboxChecked, inContributorsFile };
}
export function buildCommitMessage(prAuthor) {
return `docs: add @${prAuthor} to contributors.md\n\n@${prAuthor} agrees to license contributions to iii under Apache 2.0.`;
}
export function buildPendingComment(prAuthor) {
return [
"## License agreement required",
"",
`@${prAuthor}, to contribute to this repository please confirm that you license your changes under Apache 2.0 and that you have all necessary rights over the code you are contributing.`,
"",
"Your agreement also signifies that you have read and agree to CONTRIBUTING.md located in the root of this repository",
"",
"Copy the following into your PR description and check the box:",
"",
"\`\`\`markdown",
`- [ ] ${CHECKBOX_TEXT}`,
"\`\`\`",
"",
"When you check the box, this workflow will automatically add you to contributors.md with the following commit:",
"",
"\`\`\`",
buildCommitMessage(prAuthor),
"\`\`\`",
"",
"Once added, all future PRs from your account will pass this check automatically.",
"",
"Alternatively you can add your github username as a new line to the end of `contributors.md` in the repository root.",
].join("\n");
}
export function buildSatisfiedComment(prAuthor) {
return [
"## License agreement recorded",
"",
`@${prAuthor}, your agreement has been recorded and you have been added to contributors.md. All future PRs from your account will pass this check automatically.`,
].join("\n");
}
export function findStickyComment(comments = []) {
return comments.find(
(comment) =>
isBotLogin(comment.user?.login ?? "") &&
(comment.body?.includes("## License agreement required") ||
comment.body?.includes("## License agreement recorded")),
);
}
function getRequiredEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required`);
}
return value;
}
function getRepoParts() {
const repository = getRequiredEnv("GITHUB_REPOSITORY");
const [owner, repo] = repository.split("/");
if (!owner || !repo) {
throw new Error(`Invalid GITHUB_REPOSITORY: ${repository}`);
}
return { owner, repo, repository };
}
async function githubRequest(path, options = {}) {
const token = getRequiredEnv("GITHUB_TOKEN");
const response = await fetch(`https://api.github.com${path}`, {
...options,
headers: {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
"content-type": "application/json",
"x-github-api-version": "2022-11-28",
...options.headers,
},
});
if (!response.ok) {
const message = await response.text();
const error = new Error(`GitHub API request failed: ${response.status} ${path} ${message}`);
error.status = response.status;
throw error;
}
if (response.status === 204) {
return null;
}
return response.json();
}
async function listIssueComments({ owner, repo, issueNumber }) {
const comments = [];
for (let page = 1; ; page += 1) {
const batch = await githubRequest(
`/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100&page=${page}`,
);
comments.push(...batch);
if (batch.length < 100) {
return comments;
}
}
}
async function upsertStickyComment({ owner, repo, issueNumber, comments, body }) {
const stickyComment = findStickyComment(comments);
if (stickyComment) {
await githubRequest(`/repos/${owner}/${repo}/issues/comments/${stickyComment.id}`, {
method: "PATCH",
body: JSON.stringify({ body }),
});
return;
}
await githubRequest(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
method: "POST",
body: JSON.stringify({ body }),
});
}
async function createCommitStatus({ owner, repo, sha, state, description }) {
const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
const repository = getRequiredEnv("GITHUB_REPOSITORY");
const runId = getRequiredEnv("GITHUB_RUN_ID");
await githubRequest(`/repos/${owner}/${repo}/statuses/${sha}`, {
method: "POST",
body: JSON.stringify({
context: STATUS_CONTEXT,
description,
state,
target_url: `${serverUrl}/${repository}/actions/runs/${runId}`,
}),
});
}
async function fetchContributorsFile({ owner, repo }) {
try {
const result = await githubRequest(`/repos/${owner}/${repo}/contents/contributors.md`);
const content = Buffer.from(result.content, "base64").toString("utf8");
return { content, sha: result.sha };
} catch (error) {
if (error.status === 404) {
return { content: "", sha: null };
}
throw error;
}
}
async function addToContributorsFile({ owner, repo, username, content, sha }) {
if (isInContributorsFile(username, content)) {
return;
}
const newContent = content.trimEnd() + `\n- @${username}\n`;
const encodedContent = Buffer.from(newContent).toString("base64");
const body = {
message: buildCommitMessage(username),
content: encodedContent,
committer: {
name: "github-actions[bot]",
email: "41898282+github-actions[bot]@users.noreply.github.com",
},
};
if (sha) {
body.sha = sha;
}
try {
await githubRequest(`/repos/${owner}/${repo}/contents/contributors.md`, {
method: "PUT",
body: JSON.stringify(body),
});
} catch (error) {
if (error.status === 409) {
const { content: freshContent, sha: freshSha } = await fetchContributorsFile({ owner, repo });
if (!isInContributorsFile(username, freshContent)) {
const retryContent = freshContent.trimEnd() + `\n- @${username}\n`;
body.content = Buffer.from(retryContent).toString("base64");
body.sha = freshSha;
await githubRequest(`/repos/${owner}/${repo}/contents/contributors.md`, {
method: "PUT",
body: JSON.stringify(body),
});
}
} else {
throw error;
}
}
}
// Whether `username` belongs to the `org`. Uses an authenticated call so it sees
// private (concealed) members too, which the event payload's `author_association`
// does not. Requires the token's App to have organization `Members: read`.
// Returns false when the repo owner is a user account rather than an org.
async function isOrgMember({ org, username }) {
try {
await githubRequest(`/orgs/${org}/members/${encodeURIComponent(username)}`);
return true;
} catch (error) {
if (error.status === 404 || error.status === 302) {
return false;
}
throw error;
}
}
async function getPullRequestForEvent({ event, owner, repo }) {
if (event.pull_request) {
return {
issueNumber: event.pull_request.number,
pullRequest: event.pull_request,
};
}
if (!event.issue?.pull_request) {
return null;
}
return {
issueNumber: event.issue.number,
pullRequest: await githubRequest(`/repos/${owner}/${repo}/pulls/${event.issue.number}`),
};
}
export async function run() {
const event = JSON.parse(await readFile(getRequiredEnv("GITHUB_EVENT_PATH"), "utf8"));
const { owner, repo } = getRepoParts();
const prContext = await getPullRequestForEvent({ event, owner, repo });
if (!prContext) {
console.log("No pull request found for this event; skipping license agreement check.");
return;
}
const { issueNumber, pullRequest } = prContext;
const prAuthor = pullRequest.user.login;
const headSha = pullRequest.head.sha;
const prBody = pullRequest.body || "";
if (await isOrgMember({ org: owner, username: prAuthor })) {
await createCommitStatus({
owner,
repo,
sha: headSha,
state: "success",
description: "Org member; license agreement not required.",
});
console.log(`${prAuthor} is a member of ${owner}; license agreement skipped.`);
return;
}
const { content: contributorsContent, sha: contributorsSha } = await fetchContributorsFile({
owner,
repo,
});
const inContributorsFile = isInContributorsFile(prAuthor, contributorsContent);
if (inContributorsFile) {
await createCommitStatus({
owner,
repo,
sha: headSha,
state: "success",
description: "Contributor agreement on file.",
});
console.log(`${prAuthor} is already in contributors.md; license agreement satisfied.`);
return;
}
if (hasCheckedCheckbox(prBody)) {
await addToContributorsFile({
owner,
repo,
username: prAuthor,
content: contributorsContent,
sha: contributorsSha,
});
const comments = await listIssueComments({ owner, repo, issueNumber });
await upsertStickyComment({
owner,
repo,
issueNumber,
comments,
body: buildSatisfiedComment(prAuthor),
});
await createCommitStatus({
owner,
repo,
sha: headSha,
state: "success",
description: "License agreement recorded.",
});
console.log(`License agreement recorded for ${prAuthor}; added to contributors.md.`);
return;
}
const comments = await listIssueComments({ owner, repo, issueNumber });
await upsertStickyComment({
owner,
repo,
issueNumber,
comments,
body: buildPendingComment(prAuthor),
});
await createCommitStatus({
owner,
repo,
sha: headSha,
state: "failure",
description: "License agreement acknowledgement required.",
});
console.error(
`::error::License agreement required. ${prAuthor} must check the agreement box in the PR description.`,
);
process.exitCode = 1;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});
}
```
## /.github/scripts/license-agreement-check.test.mjs
```mjs path="/.github/scripts/license-agreement-check.test.mjs"
import assert from 'node:assert/strict';
import test from 'node:test';
import {
CHECKBOX_TEXT,
buildCommitMessage,
buildPendingComment,
buildSatisfiedComment,
evaluateAgreement,
findStickyComment,
hasCheckedCheckbox,
isInContributorsFile,
} from './license-agreement-check.mjs';
// ── hasCheckedCheckbox ────────────────────────────────────────────────────────
test('detects a checked checkbox in the PR body', () => {
assert.equal(hasCheckedCheckbox(`- [x] ${CHECKBOX_TEXT}`), true);
});
test('detects a checked checkbox regardless of case', () => {
assert.equal(hasCheckedCheckbox(`- [X] ${CHECKBOX_TEXT}`), true);
});
test('does not treat an unchecked checkbox as checked', () => {
assert.equal(hasCheckedCheckbox(`- [ ] ${CHECKBOX_TEXT}`), false);
});
test('returns false when the PR body is empty', () => {
assert.equal(hasCheckedCheckbox(''), false);
});
// ── isInContributorsFile ──────────────────────────────────────────────────────
test('finds a username that appears as a list entry', () => {
const content = '# Contributors\n\n- @alice\n- @bob\n';
assert.equal(isInContributorsFile('alice', content), true);
assert.equal(isInContributorsFile('bob', content), true);
});
test('does not match a partial username', () => {
const content = '# Contributors\n\n- @alice-bot\n';
assert.equal(isInContributorsFile('alice', content), false);
});
test('returns false when the file is empty', () => {
assert.equal(isInContributorsFile('alice', ''), false);
});
test('returns false when the username is not present', () => {
assert.equal(isInContributorsFile('carol', '# Contributors\n\n- @alice\n'), false);
});
// ── evaluateAgreement ─────────────────────────────────────────────────────────
test('passes contributors already in contributors.md', () => {
const result = evaluateAgreement({ prBody: '', inContributorsFile: true });
assert.deepEqual(result, {
acknowledged: true,
checkboxChecked: false,
inContributorsFile: true,
});
});
test('passes contributors who have checked the checkbox', () => {
const result = evaluateAgreement({ prBody: `- [x] ${CHECKBOX_TEXT}`, inContributorsFile: false });
assert.deepEqual(result, {
acknowledged: true,
checkboxChecked: true,
inContributorsFile: false,
});
});
test('fails contributors without acknowledgement', () => {
const result = evaluateAgreement({ prBody: '', inContributorsFile: false });
assert.deepEqual(result, {
acknowledged: false,
checkboxChecked: false,
inContributorsFile: false,
});
});
// ── findStickyComment ─────────────────────────────────────────────────────────
test('finds the pending comment when authored by the workflow bot', () => {
const comments = [
{ body: '## License agreement required', user: { login: 'external-user' } },
{ body: '## License agreement required', user: { login: 'github-actions[bot]' }, id: 123 },
];
assert.deepEqual(findStickyComment(comments), comments[1]);
});
test('recognizes the sticky comment under a GitHub App bot login', () => {
const comments = [
{ body: '## License agreement required', user: { login: 'iii-cla[bot]' }, id: 789 },
];
assert.deepEqual(findStickyComment(comments), comments[0]);
});
test('finds the satisfied comment when authored by the workflow bot', () => {
const comments = [
{ body: '## License agreement recorded', user: { login: 'github-actions[bot]' }, id: 456 },
];
assert.deepEqual(findStickyComment(comments), comments[0]);
});
test('returns undefined when no bot comment exists', () => {
const comments = [
{ body: '## License agreement required', user: { login: 'external-user' } },
];
assert.equal(findStickyComment(comments), undefined);
});
// ── buildCommitMessage ────────────────────────────────────────────────────────
test('buildCommitMessage includes the username', () => {
const msg = buildCommitMessage('alice');
assert.ok(msg.includes('@alice'));
assert.ok(msg.startsWith('docs:'));
});
// ── buildPendingComment ───────────────────────────────────────────────────────
test('buildPendingComment includes the checkbox text as a copyable code sample', () => {
const comment = buildPendingComment('alice');
assert.ok(comment.includes('@alice'));
assert.ok(comment.includes('docs: add @alice to contributors.md'));
assert.ok(comment.includes('\`\`\`markdown'));
assert.ok(comment.includes(`- [ ] ${CHECKBOX_TEXT}`));
});
// ── buildSatisfiedComment ─────────────────────────────────────────────────────
test('buildSatisfiedComment includes the username and contributors.md reference', () => {
const comment = buildSatisfiedComment('alice');
assert.ok(comment.includes('@alice'));
assert.ok(comment.includes('contributors.md'));
});
```
## /.github/scripts/pin_docs.py
```py path="/.github/scripts/pin_docs.py"
#!/usr/bin/env python3
"""Rotate Mintlify docs versions during the create-tag release flow.
On a stable minor/major release: archive the old Latest (root) into a
MAJOR-MINOR-0/ folder, promote the fixed docs/next/ folder into the root as
the new Latest (labeled with the tag version), and relabel the Next block to
the following minor.
Two subcommands:
* ``validate`` — gate a stable release on the Next docs being ready (a Next
block exists and docs/next/ is non-empty). Runs even on dry runs.
* ``rotate`` — update docs from docs/next/. Dispatches on the version: a
minor/major does a full rotation (archive + promote + bump Next); a patch
refreshes the current Latest in place (no archive, no Next bump).
Pure functions live at module top so they can be unit-tested. The CLI at the
bottom is invoked from .github/workflows/create-tag.yml.
"""
from __future__ import annotations
import argparse
import copy
import json
import re
import shutil
import sys
from pathlib import Path
NEXT_TAG = "Next"
LATEST_TAG = "Latest"
NEXT_DIR = "next" # fixed folder name holding the Next docs
_VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)")
def parse_version(version: str) -> tuple[int, int, int]:
"""Return (major, minor, patch); any prerelease/dry-run suffix is ignored."""
m = _VERSION_RE.match(version)
if not m:
raise ValueError(f"unrecognized version string: {version!r}")
return int(m.group(1)), int(m.group(2)), int(m.group(3))
def minor_label(major: int, minor: int) -> str:
return f"{major}.{minor}.x"
def dir_prefix(major: int, minor: int) -> str:
return f"{major}-{minor}-0"
def next_minor(version: str) -> tuple[int, int]:
"""Return (major, minor+1) for the version — the new Next after a release."""
major, minor, _ = parse_version(version)
return major, minor + 1
def _map_page_strings(node, fn, in_pages=False):
if isinstance(node, str):
return fn(node) if in_pages else node
if isinstance(node, list):
return [_map_page_strings(item, fn, in_pages) for item in node]
if isinstance(node, dict):
return {k: _map_page_strings(v, fn, k == "pages") for k, v in node.items()}
return node
def _is_shared_page(path: str) -> bool:
return path.split("/", 1)[0] in SHARED_ROOTS
def add_prefix(tabs: list, prefix: str) -> list:
"""Deep-copy ``tabs`` and prefix every page string with ``prefix/``, except
shared-root pages (e.g. changelog), which always stay at the root."""
return _map_page_strings(
copy.deepcopy(tabs),
lambda s: s if _is_shared_page(s) else f"{prefix}/{s}",
)
def strip_prefix(tabs: list, prefix: str) -> list:
"""Deep-copy ``tabs`` and remove a leading ``prefix/`` from page strings."""
head = f"{prefix}/"
return _map_page_strings(
copy.deepcopy(tabs),
lambda s: s[len(head):] if s.startswith(head) else s,
)
def find_index_by_tag(versions: list[dict], tag: str) -> int:
for i, block in enumerate(versions):
if block.get("tag") == tag:
return i
raise ValueError(f"no version block tagged {tag!r} in docs.json")
_MINOR_RE = re.compile(r"^(\d+)\.(\d+)")
def _version_sort_key(block: dict) -> tuple[int, int]:
m = _MINOR_RE.match(block.get("version", ""))
return (int(m.group(1)), int(m.group(2))) if m else (-1, -1)
def sort_versions(versions: list[dict]) -> list[dict]:
"""Order: Next first, then Latest, then archived newest-first (Mintlify
renders the dropdown in array order)."""
nxt = [b for b in versions if b.get("tag") == NEXT_TAG]
latest = [b for b in versions if b.get("tag") == LATEST_TAG]
rest = [b for b in versions if b.get("tag") not in (NEXT_TAG, LATEST_TAG)]
rest.sort(key=_version_sort_key, reverse=True)
return nxt + latest + rest
_VERSION_DIR_RE = re.compile(r"^\d+-\d+-\d+{{contextString}}quot;)
# Content shared across all versions: kept at the docs root, never copied into a
# version folder and never version-prefixed in docs.json (every version's tab
# points at the single root copy).
SHARED_ROOTS = {"changelog"}
_EXCLUDED_NAMES = {
"docs.json", "package.json", "package-lock.json", "node_modules",
"custom.css", "navbar-counters.js", ".gitignore", ".mintignore",
".prettierrc", "README.md", "RELEASING.md", "LICENSE", NEXT_DIR,
}
def is_excluded(name: str) -> bool:
"""True if a top-level docs entry is not movable content (infra, sidecars,
version folders, the fixed next/ folder, or shared root content)."""
return (
name in _EXCLUDED_NAMES
or name in SHARED_ROOTS
or name.endswith(".skill.md")
or bool(_VERSION_DIR_RE.match(name))
)
def _content_entries(docs_dir: Path):
return [e for e in sorted(docs_dir.iterdir()) if not is_excluded(e.name)]
def _copy_entry(src: Path, dst: Path) -> None:
if src.is_dir():
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
def copy_root_to_dir(docs_dir: Path, prefix: str) -> None:
"""Copy root content (excluding infra/version/next) into ``docs_dir/prefix``,
overwriting it."""
target = docs_dir / prefix
if target.exists():
shutil.rmtree(target)
target.mkdir(parents=True)
for entry in _content_entries(docs_dir):
_copy_entry(entry, target / entry.name)
def replace_root_with_dir(docs_dir: Path, prefix: str) -> None:
"""Replace root content with the content of ``docs_dir/prefix`` (infra,
version dirs, and the next/ folder are left in place). The source folder is
not removed."""
source = docs_dir / prefix
for entry in _content_entries(docs_dir):
if entry.is_dir():
shutil.rmtree(entry)
else:
entry.unlink()
for entry in sorted(source.iterdir()):
_copy_entry(entry, docs_dir / entry.name)
def _load(docs_dir: Path) -> dict:
return json.loads((docs_dir / "docs.json").read_text(encoding="utf-8"))
def _save(docs_dir: Path, doc: dict) -> None:
(docs_dir / "docs.json").write_text(
json.dumps(doc, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
)
def validate(docs_dir: Path) -> int:
"""Exit 0 if the Next docs are ready; 1 (with error) otherwise.
Ready = a Next block exists in docs.json AND docs/next/ is non-empty. The
release version is irrelevant — rotation always pulls whatever is in next/.
"""
try:
versions = _load(docs_dir)["navigation"]["versions"]
find_index_by_tag(versions, NEXT_TAG)
except (FileNotFoundError, ValueError) as exc:
print(f"::error::Cannot validate Next docs: {exc}")
return 1
folder = docs_dir / NEXT_DIR
if not folder.is_dir() or not any(folder.iterdir()):
print(
f"::error::Missing Next docs at {folder}. "
f"Prepare docs/{NEXT_DIR}/ before tagging."
)
return 1
print("pin_docs: Next docs ready")
return 0
def rotate(docs_dir: Path, version: str) -> None:
"""Archive the old Latest, promote docs/next/ into root as the new Latest
(labeled with the tag version), and relabel the Next block to minor+1.
Assumes ``validate`` already passed.
"""
major, minor, patch = parse_version(version)
# A patch release does not rotate — it only refreshes the current Latest
# from docs/next/ in place (no archive, no Next bump, no version changes).
if patch > 0:
sync_patch(docs_dir)
return
rel_label = minor_label(major, minor) # tag version label, e.g. 0.17.x
nmaj, nmin = next_minor(version)
nxt_label = minor_label(nmaj, nmin) # e.g. 0.18.x
doc = _load(docs_dir)
versions = doc["navigation"]["versions"]
next_block = versions[find_index_by_tag(versions, NEXT_TAG)]
latest_block = versions[find_index_by_tag(versions, LATEST_TAG)]
# Idempotency guard: if the current Latest is already this version, the
# rotation has already happened for it — re-running would duplicate the
# block and re-copy folders. No-op.
if latest_block["version"] == rel_label:
print(f"pin_docs: already rotated to {rel_label}, skipping")
return
old_major, old_minor, _ = parse_version(latest_block["version"].replace("x", "0"))
old_prefix = dir_prefix(old_major, old_minor) # e.g. 0-16-0
# --- filesystem (order matters) ---
# In-content links are version-relative, so they survive the move untouched.
copy_root_to_dir(docs_dir, old_prefix) # archive old Latest -> folder
replace_root_with_dir(docs_dir, NEXT_DIR) # promote next/ -> root (next/ kept)
# --- docs.json ---
# old Latest -> archived (root paths -> old_prefix), drop tag/default
latest_block["tabs"] = add_prefix(latest_block["tabs"], old_prefix)
latest_block.pop("tag", None)
latest_block.pop("default", None)
# new Latest block from the Next tabs (next/ -> root), labeled with the tag
new_latest = {
"version": rel_label,
"tag": LATEST_TAG,
"default": True,
"tabs": strip_prefix(next_block["tabs"], NEXT_DIR),
}
# Next block keeps its next/ paths; only its label advances to minor+1
next_block["version"] = nxt_label
versions.append(new_latest)
doc["navigation"]["versions"] = sort_versions(versions)
_save(docs_dir, doc)
print(f"pin_docs: rotated — Latest={rel_label}, Next={nxt_label}, archived {latest_block['version']}")
def sync_patch(docs_dir: Path) -> None:
"""Patch release: refresh the Latest (root) docs from docs/next/ in place.
Unlike ``rotate`` there is no version ceremony — the old Latest is not
archived, the Next block is not bumped, and no version blocks in docs.json
change. Only the root content is replaced (in-content links are relative, so
they survive the move untouched).
"""
replace_root_with_dir(docs_dir, NEXT_DIR)
print("pin_docs: patched Latest from docs/next/ (no rotation)")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
p_validate = sub.add_parser("validate")
p_validate.add_argument("--docs-dir", required=True, type=Path)
p_rotate = sub.add_parser("rotate")
p_rotate.add_argument("--docs-dir", required=True, type=Path)
p_rotate.add_argument("--version", required=True)
args = parser.parse_args(argv)
if args.command == "validate":
return validate(args.docs_dir)
rotate(args.docs_dir, args.version)
return 0
if __name__ == "__main__":
sys.exit(main())
```
## /.github/scripts/test_bump_manifests.py
```py path="/.github/scripts/test_bump_manifests.py"
"""Unit tests for bump_manifests.py.
Run with: python -m pytest .github/scripts/test_bump_manifests.py -v
"""
from __future__ import annotations
import subprocess
import sys
import textwrap
from pathlib import Path
import pytest
from bump_manifests import bump_cargo_package_version
from bump_manifests import bump_cargo_workspace_dep_version
from bump_manifests import bump_go_const_version
from bump_manifests import bump_json_top_level_version
from bump_manifests import bump_pep440_dep_pin
from bump_manifests import rewrite_all
class TestBumpCargoPackageVersion:
def test_replaces_first_version_only(self):
src = textwrap.dedent(
"""\
[package]
name = "iii-observability"
version = "0.13.0-next.1"
edition = "2024"
[dependencies]
other = { version = "1.2.3" }
"""
)
out = bump_cargo_package_version(src, "0.16.0-next.2")
assert 'version = "0.16.0-next.2"' in out
assert 'other = { version = "1.2.3" }' in out
# First match anchored on line start only — no other top-level
# version line should change.
assert out.count('version = "0.16.0-next.2"') == 1
def test_raises_when_no_version(self):
src = '[package]\nname = "foo"\n'
with pytest.raises(ValueError, match="no top-level version"):
bump_cargo_package_version(src, "0.16.0-next.2")
class TestBumpCargoWorkspaceDepVersion:
def test_replaces_version_pin(self):
src = (
"[workspace.dependencies]\n"
'iii-observability = { path = "sdk/packages/rust/observability", version = "0.13.0-next.1" }\n'
'tokio = { version = "1", features = ["macros"] }\n'
)
out = bump_cargo_workspace_dep_version(src, "iii-observability", "0.16.0-next.2")
assert (
'iii-observability = { path = "sdk/packages/rust/observability", version = "0.16.0-next.2" }'
in out
)
assert 'tokio = { version = "1", features = ["macros"] }' in out
def test_raises_when_dep_missing(self):
src = '[workspace.dependencies]\ntokio = { version = "1" }\n'
with pytest.raises(ValueError, match="iii-observability"):
bump_cargo_workspace_dep_version(src, "iii-observability", "0.16.0-next.2")
class TestBumpJsonTopLevelVersion:
def test_replaces_first_version(self):
src = (
'{\n'
' "name": "iii-sdk",\n'
' "version": "0.13.0-next.1",\n'
' "dependencies": {\n'
' "@iii-dev/observability": "workspace:*"\n'
' }\n'
'}\n'
)
out = bump_json_top_level_version(src, "0.16.0-next.2")
assert '"version": "0.16.0-next.2"' in out
assert '"@iii-dev/observability": "workspace:*"' in out
def test_raises_when_no_top_level_version(self):
src = '{ "name": "x" }\n'
with pytest.raises(ValueError, match="no top-level version"):
bump_json_top_level_version(src, "1.0.0")
class TestBumpPep440DepPin:
def test_replaces_pin(self):
src = (
'dependencies = [\n'
' "websockets>=12.0",\n'
' "iii-observability==0.13.0.dev1",\n'
']\n'
)
out = bump_pep440_dep_pin(src, "iii-observability", "0.16.0.dev2")
assert '"iii-observability==0.16.0.dev2"' in out
assert '"websockets>=12.0"' in out
def test_raises_when_dep_missing(self):
src = 'dependencies = [\n "websockets>=12.0",\n]\n'
with pytest.raises(ValueError, match="iii-observability"):
bump_pep440_dep_pin(src, "iii-observability", "0.16.0.dev2")
class TestBumpGoConstVersion:
def test_replaces_const(self):
src = (
"// sdkVersion is reported in the worker metadata.\n"
'const sdkVersion = "0.1.0"\n'
)
out = bump_go_const_version(src, "0.19.2-alpha.1")
assert 'const sdkVersion = "0.19.2-alpha.1"' in out
assert '"0.1.0"' not in out
def test_raises_when_missing(self):
with pytest.raises(ValueError, match="sdkVersion"):
bump_go_const_version("package iii\n", "0.19.2-alpha.1")
def _write(p: Path, body: str) -> None:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(body)
def test_rewrite_all_updates_every_target_file(tmp_path: Path):
root = tmp_path
_write(root / "Cargo.toml", (
'[workspace.package]\n'
'version = "0.15.0-next.1"\n\n'
'[workspace.dependencies]\n'
'iii-observability = { path = "sdk/packages/rust/observability", version = "0.13.0-next.1" }\n'
'iii-helpers = { path = "sdk/packages/rust/helpers", version = "0.13.0-next.1" }\n'
))
_write(root / "engine" / "Cargo.toml", '[package]\nname = "iii"\nversion = "0.15.0-next.1"\n')
_write(root / "sdk/packages/rust/iii/Cargo.toml", '[package]\nname = "iii-sdk"\nversion = "0.15.0-next.1"\n')
_write(root / "sdk/packages/rust/observability/Cargo.toml", '[package]\nname = "iii-observability"\nversion = "0.13.0-next.1"\n')
_write(root / "sdk/packages/rust/helpers/Cargo.toml", '[package]\nname = "iii-helpers"\nversion = "0.13.0-next.1"\n')
_write(root / "sdk/packages/node/iii/package.json", '{\n "name": "iii-sdk",\n "version": "0.15.0-next.1"\n}\n')
_write(root / "sdk/packages/node/iii-browser/package.json", '{\n "name": "iii-browser",\n "version": "0.15.0-next.1"\n}\n')
_write(root / "sdk/packages/node/observability/package.json", '{\n "name": "@iii-dev/observability",\n "version": "0.13.0-next.1"\n}\n')
_write(root / "sdk/packages/node/helpers/package.json", '{\n "name": "@iii-dev/helpers",\n "version": "0.13.0-next.1"\n}\n')
# Python iii: iii-observability is no longer a direct dep (removed in
# observability-into-helpers refactor); only iii-helpers is pinned.
_write(root / "sdk/packages/python/iii/pyproject.toml", (
'[project]\nname = "iii-sdk"\nversion = "0.15.0.dev1"\n'
'dependencies = [\n "iii-helpers==0.13.0.dev1",\n]\n'
))
_write(root / "sdk/packages/python/observability/pyproject.toml", (
'[project]\nname = "iii-observability"\nversion = "0.13.0.dev1"\n'
'dependencies = [\n "iii-helpers==0.13.0.dev1",\n]\n\n'
'[tool.uv.sources]\niii-helpers = { path = "../helpers", editable = true }\n'
))
_write(root / "sdk/packages/python/helpers/pyproject.toml", '[project]\nname = "iii-helpers"\nversion = "0.13.0.dev1"\n')
_write(root / "console/packages/console-rust/Cargo.toml", '[package]\nname = "console-rust"\nversion = "0.15.0-next.1"\n')
_write(root / "sdk/packages/go/iii/client.go", 'package iii\n\nconst sdkVersion = "0.1.0"\n')
rewrite_all(root=root, new_version="0.16.0-next.2", new_py_version="0.16.0.dev2")
assert 'version = "0.16.0-next.2"' in (root / "Cargo.toml").read_text()
assert 'iii-observability = { path = "sdk/packages/rust/observability", version = "0.16.0-next.2" }' in (root / "Cargo.toml").read_text()
assert 'iii-helpers = { path = "sdk/packages/rust/helpers", version = "0.16.0-next.2" }' in (root / "Cargo.toml").read_text()
assert 'version = "0.16.0-next.2"' in (root / "engine" / "Cargo.toml").read_text()
assert 'version = "0.16.0-next.2"' in (root / "sdk/packages/rust/iii/Cargo.toml").read_text()
assert 'version = "0.16.0-next.2"' in (root / "sdk/packages/rust/observability/Cargo.toml").read_text()
assert 'version = "0.16.0-next.2"' in (root / "sdk/packages/rust/helpers/Cargo.toml").read_text()
assert '"version": "0.16.0-next.2"' in (root / "sdk/packages/node/iii/package.json").read_text()
assert '"version": "0.16.0-next.2"' in (root / "sdk/packages/node/iii-browser/package.json").read_text()
assert '"version": "0.16.0-next.2"' in (root / "sdk/packages/node/observability/package.json").read_text()
assert '"version": "0.16.0-next.2"' in (root / "sdk/packages/node/helpers/package.json").read_text()
py_iii = (root / "sdk/packages/python/iii/pyproject.toml").read_text()
assert 'version = "0.16.0.dev2"' in py_iii
assert '"iii-helpers==0.16.0.dev2"' in py_iii
# iii-observability is published as a shim but is no longer a dep of iii-sdk
assert '"iii-observability==' not in py_iii
py_obs = (root / "sdk/packages/python/observability/pyproject.toml").read_text()
assert 'version = "0.16.0.dev2"' in py_obs
# The shim's iii-helpers dep pin must be bumped to the new release version.
assert '"iii-helpers==0.16.0.dev2"' in py_obs
assert '"iii-helpers==0.13.0.dev1"' not in py_obs
assert 'version = "0.16.0.dev2"' in (root / "sdk/packages/python/helpers/pyproject.toml").read_text()
assert 'version = "0.16.0-next.2"' in (root / "console/packages/console-rust/Cargo.toml").read_text()
assert 'const sdkVersion = "0.16.0-next.2"' in (root / "sdk/packages/go/iii/client.go").read_text()
def test_cli_invokes_rewrite_all(tmp_path: Path):
root = tmp_path
_write(root / "Cargo.toml", (
'[workspace.package]\n'
'version = "0.15.0-next.1"\n\n'
'[workspace.dependencies]\n'
'iii-observability = { path = "sdk/packages/rust/observability", version = "0.13.0-next.1" }\n'
'iii-helpers = { path = "sdk/packages/rust/helpers", version = "0.13.0-next.1" }\n'
))
_write(root / "engine" / "Cargo.toml", 'version = "0.15.0-next.1"\n')
_write(root / "sdk/packages/rust/iii/Cargo.toml", 'version = "0.15.0-next.1"\n')
_write(root / "sdk/packages/rust/observability/Cargo.toml", 'version = "0.13.0-next.1"\n')
_write(root / "sdk/packages/rust/helpers/Cargo.toml", 'version = "0.13.0-next.1"\n')
_write(root / "sdk/packages/node/iii/package.json", '{\n "version": "0.15.0-next.1"\n}\n')
_write(root / "sdk/packages/node/iii-browser/package.json", '{\n "version": "0.15.0-next.1"\n}\n')
_write(root / "sdk/packages/node/observability/package.json", '{\n "version": "0.13.0-next.1"\n}\n')
_write(root / "sdk/packages/node/helpers/package.json", '{\n "version": "0.13.0-next.1"\n}\n')
# Python iii: only iii-helpers is pinned (iii-observability removed in
# observability-into-helpers refactor).
_write(root / "sdk/packages/python/iii/pyproject.toml", (
'version = "0.15.0.dev1"\n'
'dependencies = [\n "iii-helpers==0.13.0.dev1",\n]\n'
))
_write(root / "sdk/packages/python/observability/pyproject.toml", (
'version = "0.13.0.dev1"\n'
'dependencies = [\n "iii-helpers==0.13.0.dev1",\n]\n\n'
'[tool.uv.sources]\niii-helpers = { path = "../helpers", editable = true }\n'
))
_write(root / "sdk/packages/python/helpers/pyproject.toml", 'version = "0.13.0.dev1"\n')
_write(root / "console/packages/console-rust/Cargo.toml", 'version = "0.15.0-next.1"\n')
_write(root / "sdk/packages/go/iii/client.go", 'package iii\n\nconst sdkVersion = "0.1.0"\n')
script = Path(__file__).parent / "bump_manifests.py"
result = subprocess.run(
[sys.executable, str(script), "--root", str(root),
"--version", "0.16.0-next.2", "--python-version", "0.16.0.dev2"],
check=True, capture_output=True, text=True,
)
assert "0.16.0-next.2" in result.stdout
assert 'version = "0.16.0-next.2"' in (root / "Cargo.toml").read_text()
assert 'iii-helpers==0.16.0.dev2' in (root / "sdk/packages/python/iii/pyproject.toml").read_text()
assert 'iii-helpers==0.16.0.dev2' in (root / "sdk/packages/python/observability/pyproject.toml").read_text()
```
## /.github/scripts/test_calculate_release_version.py
```py path="/.github/scripts/test_calculate_release_version.py"
"""Unit tests for calculate_release_version.py.
Run with: python -m pytest .github/scripts/test_calculate_release_version.py -v
"""
from __future__ import annotations
import pytest
from calculate_release_version import (
bump_base,
calculate_version,
detect_current_level,
highest_inflight_prerelease_base,
latest_stable_from_tags,
next_dry_run_counter,
next_prerelease_counter,
parse_version,
to_pep440,
)
# ---------- parse_version ----------
class TestParseVersion:
def test_stable(self):
v = parse_version("1.2.3")
assert (v.major, v.minor, v.patch) == (1, 2, 3)
assert v.prerelease_label is None
assert v.prerelease_num is None
def test_prerelease(self):
v = parse_version("0.12.0-next.5")
assert (v.major, v.minor, v.patch) == (0, 12, 0)
assert v.prerelease_label == "next"
assert v.prerelease_num == 5
@pytest.mark.parametrize("label", ["alpha", "beta", "rc", "next"])
def test_all_prerelease_labels(self, label):
v = parse_version(f"1.2.3-{label}.7")
assert v.prerelease_label == label
assert v.prerelease_num == 7
def test_invalid(self):
with pytest.raises(ValueError):
parse_version("not-a-version")
# ---------- bump_base ----------
class TestBumpBase:
@pytest.mark.parametrize(
"base,bump,expected",
[
("0.11.7", "patch", "0.11.8"),
("0.11.7", "minor", "0.12.0"),
("0.11.7", "major", "1.0.0"),
("1.2.3", "minor", "1.3.0"),
("0.0.0", "patch", "0.0.1"),
],
)
def test_bump(self, base, bump, expected):
assert bump_base(base, bump) == expected
def test_unknown_bump(self):
with pytest.raises(ValueError):
bump_base("1.0.0", "bogus")
# ---------- detect_current_level ----------
class TestDetectCurrentLevel:
@pytest.mark.parametrize(
"current,stable,expected",
[
("0.11.7", "0.11.6", "patch"),
("0.12.0", "0.11.6", "minor"),
("1.0.0", "0.11.6", "major"),
("0.11.6", "0.11.6", None), # equal — no bump implied
("0.10.0", "0.11.6", None), # current older
],
)
def test_levels(self, current, stable, expected):
assert detect_current_level(current, stable) == expected
def test_no_stable(self):
assert detect_current_level("0.1.0", None) is None
# ---------- next_prerelease_counter ----------
class TestPrereleaseCounter:
def test_empty(self):
assert next_prerelease_counter("0.12.0", "next", [], "iii") == 1
def test_existing(self):
tags = ["iii/v0.12.0-next.1", "iii/v0.12.0-next.2", "iii/v0.12.0-next.3"]
assert next_prerelease_counter("0.12.0", "next", tags, "iii") == 4
def test_different_base_ignored(self):
tags = ["iii/v0.11.0-next.1", "iii/v0.13.0-next.5"]
assert next_prerelease_counter("0.12.0", "next", tags, "iii") == 1
def test_different_label_ignored(self):
tags = ["iii/v0.12.0-rc.1", "iii/v0.12.0-alpha.3"]
assert next_prerelease_counter("0.12.0", "next", tags, "iii") == 1
def test_different_prefix_ignored(self):
tags = ["other/v0.12.0-next.5"]
assert next_prerelease_counter("0.12.0", "next", tags, "iii") == 1
def test_picks_max_not_count(self):
tags = ["iii/v0.12.0-next.1", "iii/v0.12.0-next.7"]
assert next_prerelease_counter("0.12.0", "next", tags, "iii") == 8
# ---------- next_dry_run_counter ----------
class TestDryRunCounter:
def test_empty(self):
assert next_dry_run_counter("0.12.0", [], "iii") == 1
def test_existing(self):
tags = ["iii/v0.12.0-dry-run.1", "iii/v0.12.0-dry-run.4"]
assert next_dry_run_counter("0.12.0", tags, "iii") == 5
# ---------- highest_inflight_prerelease_base ----------
class TestHighestInflightPrereleaseBase:
def test_none_when_empty(self):
assert highest_inflight_prerelease_base([], "iii", "0.19.4") is None
def test_picks_base_above_stable(self):
tags = ["iii/v0.19.4", "iii/v0.19.5-next.1"]
assert highest_inflight_prerelease_base(tags, "iii", "0.19.4") == "0.19.5"
def test_ignores_base_at_or_below_stable(self):
tags = ["iii/v0.19.4-alpha.1", "iii/v0.19.3-next.9"]
assert highest_inflight_prerelease_base(tags, "iii", "0.19.4") is None
def test_picks_highest_across_trains(self):
tags = ["iii/v0.19.5-next.1", "iii/v0.20.0-rc.1"]
assert highest_inflight_prerelease_base(tags, "iii", "0.19.4") == "0.20.0"
def test_prefix_scoped(self):
tags = ["other/v0.30.0-next.1", "iii/v0.19.5-next.1"]
assert highest_inflight_prerelease_base(tags, "iii", "0.19.4") == "0.19.5"
def test_no_stable_floor_accepts_any_prerelease(self):
tags = ["iii/v0.5.0-next.1"]
assert highest_inflight_prerelease_base(tags, "iii", None) == "0.5.0"
def test_numeric_not_lexical_base_compare(self):
tags = ["iii/v0.19.5-next.1", "iii/v0.19.10-next.1"]
assert highest_inflight_prerelease_base(tags, "iii", "0.19.4") == "0.19.10"
# ---------- latest_stable_from_tags ----------
class TestLatestStable:
def test_picks_highest(self):
tags = [
"iii/v0.11.5",
"iii/v0.11.6",
"iii/v0.11.6-next.3", # prerelease ignored
"iii/v0.10.0",
]
assert latest_stable_from_tags(tags, "iii") == "0.11.6"
def test_no_stable(self):
tags = ["iii/v0.12.0-next.1", "iii/v0.12.0-next.2"]
assert latest_stable_from_tags(tags, "iii") is None
def test_empty(self):
assert latest_stable_from_tags([], "iii") is None
def test_different_prefix_ignored(self):
tags = ["other/v9.9.9", "iii/v0.1.0"]
assert latest_stable_from_tags(tags, "iii") == "0.1.0"
# ---------- calculate_version ----------
class TestCalculateVersion:
"""All the version-bump scenarios.
The naming convention is: starting_state__bump_prerelease__expected.
"""
# -- Stable starting point --
def test_stable_patch_stable(self):
assert calculate_version("0.11.6", "patch", "none", "0.11.6", [], "iii") == "0.11.7"
def test_stable_minor_stable(self):
assert calculate_version("0.11.6", "minor", "none", "0.11.6", [], "iii") == "0.12.0"
def test_stable_major_stable(self):
assert calculate_version("0.11.6", "major", "none", "0.11.6", [], "iii") == "1.0.0"
def test_stable_patch_next(self):
assert calculate_version("0.11.6", "patch", "next", "0.11.6", [], "iii") == "0.11.7-next.1"
def test_stable_minor_next(self):
assert calculate_version("0.11.6", "minor", "next", "0.11.6", [], "iii") == "0.12.0-next.1"
def test_stable_major_rc(self):
assert calculate_version("0.11.6", "major", "rc", "0.11.6", [], "iii") == "1.0.0-rc.1"
# -- Patch always advances the patch component --
def test_patch_always_bumps_patch_component(self):
"""patch is never an iteration: it always advances the patch component."""
assert (
calculate_version("0.11.7-next.2", "patch", "next", "0.11.6", [], "iii")
== "0.11.8-next.1"
)
def test_patch_from_minor_prerelease_bumps_patch_component(self):
"""Even mid-minor-train, patch advances to a new patch base."""
tags = ["iii/v0.12.0-next.1", "iii/v0.12.0-next.2"]
assert (
calculate_version("0.12.0-next.2", "patch", "next", "0.11.6", tags, "iii")
== "0.12.1-next.1"
)
def test_patch_stable_to_next(self):
assert (
calculate_version("0.11.6", "patch", "next", "0.11.6", [], "iii")
== "0.11.7-next.1"
)
# -- Patch prerelease train + non-patch bumps --
def test_patch_prerelease_minor_next_escalates(self):
"""The bug from the original report: minor must escalate, not continue counter."""
assert (
calculate_version("0.11.7-next.2", "minor", "next", "0.11.6", [], "iii")
== "0.12.0-next.1"
)
def test_patch_prerelease_major_next_escalates(self):
assert (
calculate_version("0.11.7-next.2", "major", "next", "0.11.6", [], "iii")
== "1.0.0-next.1"
)
# -- Minor prerelease train (the second bug the user reported) --
def test_minor_prerelease_minor_next_continues_train(self):
"""After escalating to 0.12.0-next.1, subsequent minor+next must iterate, not bump again."""
tags = ["iii/v0.12.0-next.1"]
assert (
calculate_version("0.12.0-next.1", "minor", "next", "0.11.6", tags, "iii")
== "0.12.0-next.2"
)
def test_minor_prerelease_major_next_escalates(self):
assert (
calculate_version("0.12.0-next.1", "major", "next", "0.11.6", [], "iii")
== "1.0.0-next.1"
)
# -- Major prerelease train --
def test_major_prerelease_major_next_continues_train(self):
tags = ["iii/v1.0.0-next.1", "iii/v1.0.0-next.2"]
assert (
calculate_version("1.0.0-next.2", "major", "next", "0.11.6", tags, "iii")
== "1.0.0-next.3"
)
def test_major_prerelease_minor_next_continues_train(self):
tags = ["iii/v1.0.0-next.1"]
assert (
calculate_version("1.0.0-next.1", "minor", "next", "0.11.6", tags, "iii")
== "1.0.0-next.2"
)
# -- Promotion to stable --
def test_promote_prerelease_to_stable_drops_suffix(self):
assert calculate_version("0.12.0-next.5", "patch", "none", "0.11.6", [], "iii") == "0.12.0"
def test_promote_with_minor_bump_still_uses_base(self):
"""Once a prerelease train decided the base, promotion uses it as-is."""
assert calculate_version("0.12.0-next.5", "minor", "none", "0.11.6", [], "iii") == "0.12.0"
# -- Channel switching --
def test_next_to_rc_starts_fresh_counter(self):
tags = ["iii/v0.12.0-next.3"]
assert (
calculate_version("0.12.0-next.3", "minor", "rc", "0.11.6", tags, "iii")
== "0.13.0-rc.1"
)
# -- No prior stable tag (bootstrap) --
def test_no_stable_minor_bumps_from_current(self):
"""Without a stable anchor, minor must still apply to the current base."""
tags = ["iii/v0.1.0-next.1"]
assert (
calculate_version("0.1.0-next.1", "minor", "next", None, tags, "iii")
== "0.2.0-next.1"
)
def test_no_stable_stable_starts_from_current(self):
assert calculate_version("0.0.1", "minor", "next", None, [], "iii") == "0.1.0-next.1"
def test_no_stable_patch_bumps_from_current(self):
tags = ["iii/v0.1.0-next.1"]
assert (
calculate_version("0.1.0-next.1", "patch", "next", None, tags, "iii")
== "0.1.1-next.1"
)
# -- Counter resumes from existing max --
def test_counter_uses_max_existing(self):
tags = [
"iii/v0.12.0-next.1",
"iii/v0.12.0-next.7",
"iii/v0.12.0-next.3",
]
assert (
calculate_version("0.12.0-next.7", "minor", "next", "0.11.6", tags, "iii")
== "0.12.0-next.8"
)
# -- Invalid inputs --
def test_unknown_bump(self):
with pytest.raises(ValueError):
calculate_version("0.1.0", "bogus", "none", "0.1.0", [], "iii")
def test_unknown_prerelease(self):
with pytest.raises(ValueError):
calculate_version("0.1.0", "patch", "weird", "0.1.0", [], "iii")
# ---------- to_pep440 ----------
class TestAdversarial:
"""Edge cases designed to break the algorithm."""
# -- Counter-bump ordering --
def test_counter_uses_numeric_max_not_lex(self):
"""Lex sort would put next.10 before next.2; integer comparison must win."""
tags = [
"iii/v0.12.0-next.1",
"iii/v0.12.0-next.2",
"iii/v0.12.0-next.10",
]
assert (
calculate_version("0.12.0-next.10", "minor", "next", "0.11.6", tags, "iii")
== "0.12.0-next.11"
)
def test_counter_picks_max_with_huge_gaps(self):
tags = ["iii/v0.12.0-next.1", "iii/v0.12.0-next.999"]
assert (
calculate_version("0.12.0-next.999", "minor", "next", "0.11.6", tags, "iii")
== "0.12.0-next.1000"
)
# -- Tag prefix isolation --
def test_other_prefix_tags_do_not_pollute_counter(self):
tags = [
"console/v0.12.0-next.50",
"other/v0.12.0-next.99",
"iii/v0.12.0-next.1",
]
assert (
calculate_version("0.12.0-next.1", "minor", "next", "0.11.6", tags, "iii")
== "0.12.0-next.2"
)
def test_substring_prefix_does_not_match(self):
"""`iii` must not match `iiii/...` or `iii-extra/...`."""
tags = ["iiii/v0.12.0-next.50", "iii-extra/v0.12.0-next.30"]
assert (
calculate_version("0.12.0-next.1", "minor", "next", "0.11.6", tags, "iii")
== "0.12.0-next.1"
)
def test_latest_stable_ignores_substring_prefix(self):
tags = ["iiii/v9.9.9", "iii-extra/v8.8.8", "iii/v0.5.0"]
assert latest_stable_from_tags(tags, "iii") == "0.5.0"
def test_tag_prefix_with_regex_special_chars(self):
"""Prefix containing `.` must be literal, not regex wildcard."""
tags = ["myXtarget/v0.12.0-next.50", "my.target/v0.12.0-next.1"]
assert (
calculate_version("0.12.0-next.1", "minor", "next", "0.11.6", tags, "my.target")
== "0.12.0-next.2"
)
# -- Cross-channel counter isolation --
def test_other_channels_do_not_affect_next_counter(self):
tags = [
"iii/v0.12.0-rc.5",
"iii/v0.12.0-alpha.7",
"iii/v0.12.0-beta.3",
"iii/v0.12.0-next.1",
]
assert (
calculate_version("0.12.0-next.1", "minor", "next", "0.11.6", tags, "iii")
== "0.12.0-next.2"
)
def test_switching_channel_starts_fresh(self):
tags = ["iii/v0.12.0-next.5"]
assert (
calculate_version("0.12.0-next.5", "minor", "rc", "0.11.6", tags, "iii")
== "0.13.0-rc.1"
)
def test_patch_on_rc_train_advances_patch_component(self):
tags = ["iii/v0.12.0-rc.1", "iii/v0.12.0-rc.2"]
assert (
calculate_version("0.12.0-rc.2", "patch", "rc", "0.11.6", tags, "iii")
== "0.12.1-rc.1"
)
# -- Promotion semantics --
def test_promote_with_major_bump_still_keeps_base(self):
"""Once a prerelease decided the base, promotion uses it regardless of bump type."""
assert (
calculate_version("0.12.0-next.5", "major", "none", "0.11.6", [], "iii")
== "0.12.0"
)
def test_promote_from_rc_to_stable(self):
assert (
calculate_version("0.12.0-rc.7", "patch", "none", "0.11.6", [], "iii")
== "0.12.0"
)
def test_stable_to_stable_does_not_trigger_promotion(self):
"""Promotion branch is gated on current having a prerelease label."""
assert (
calculate_version("0.12.0", "patch", "none", "0.12.0", [], "iii")
== "0.12.1"
)
# -- Latest_stable equal to or ahead of current --
def test_current_equals_stable_no_level_detected(self):
"""If current and stable are equal, the prerelease is degenerate; bump anyway."""
tags = ["iii/v0.12.0", "iii/v0.12.0-next.1"]
# detect_current_level returns None (cur == stable), so we apply bump to current base.
assert (
calculate_version("0.12.0-next.1", "minor", "next", "0.12.0", tags, "iii")
== "0.13.0-next.1"
)
def test_current_older_than_stable_treated_as_no_level(self):
"""Pathological: cargo says 0.10.0-next.1 but stable has moved to 0.11.6."""
assert (
calculate_version("0.10.0-next.1", "minor", "next", "0.11.6", [], "iii")
== "0.11.0-next.1"
)
# -- Zero-component edge cases --
def test_zero_base_patch(self):
assert calculate_version("0.0.0", "patch", "next", None, [], "iii") == "0.0.1-next.1"
def test_zero_base_major(self):
assert calculate_version("0.0.0", "major", "next", None, [], "iii") == "1.0.0-next.1"
def test_large_numbers(self):
assert (
calculate_version("999.999.999", "patch", "next", "999.999.998", [], "iii")
== "999.999.1000-next.1"
)
# -- Sequential walk: each result feeds the next call --
def test_full_release_sequence(self):
"""The whole release cycle the user described, in order."""
tags = ["iii/v0.11.6"]
stable = "0.11.6"
def step(current, bump, pre):
result = calculate_version(current, bump, pre, stable, tags, "iii")
tags.append(f"iii/v{result}")
return result
assert step("0.11.6", "patch", "next") == "0.11.7-next.1"
assert step("0.11.7-next.1", "patch", "next") == "0.11.8-next.1" # patch always advances
assert step("0.11.8-next.1", "minor", "next") == "0.12.0-next.1" # escalate
assert step("0.12.0-next.1", "minor", "next") == "0.12.0-next.2" # iterate
assert step("0.12.0-next.2", "minor", "next") == "0.12.0-next.3" # iterate
assert step("0.12.0-next.3", "patch", "none") == "0.12.0" # promote
def test_rc_iteration_sequence(self):
tags = ["iii/v0.11.6"]
stable = "0.11.6"
def step(current, bump, pre):
result = calculate_version(current, bump, pre, stable, tags, "iii")
tags.append(f"iii/v{result}")
return result
assert step("0.11.6", "minor", "rc") == "0.12.0-rc.1"
assert step("0.12.0-rc.1", "minor", "rc") == "0.12.0-rc.2"
assert step("0.12.0-rc.2", "major", "rc") == "1.0.0-rc.1"
assert step("1.0.0-rc.1", "major", "rc") == "1.0.0-rc.2"
assert step("1.0.0-rc.2", "patch", "none") == "1.0.0"
# -- Malformed tag handling in counter scan --
def test_malformed_tags_ignored(self):
tags = [
"iii/v0.12.0-next.1",
"iii/not-a-version",
"iii/v0.12.0-next.abc",
"garbage",
"iii/v0.12.0-next.",
]
assert (
calculate_version("0.12.0-next.1", "minor", "next", "0.11.6", tags, "iii")
== "0.12.0-next.2"
)
# -- Mixed major+minor latest_stable behavior --
def test_minor_jump_after_major_release(self):
"""After releasing 1.0.0, subsequent minor+next prereleases."""
tags = ["iii/v1.0.0"]
assert (
calculate_version("1.0.0", "minor", "next", "1.0.0", tags, "iii")
== "1.1.0-next.1"
)
tags.append("iii/v1.1.0-next.1")
assert (
calculate_version("1.1.0-next.1", "minor", "next", "1.0.0", tags, "iii")
== "1.1.0-next.2"
)
# -- Counter starts past 1 if existing tags found at new base --
def test_existing_tags_at_target_base_continue_counter(self):
"""If we escalate to a base that already has prereleases, resume the counter."""
tags = [
"iii/v0.11.6",
"iii/v0.12.0-next.1", # someone else already started this train
"iii/v0.12.0-next.2",
]
assert (
calculate_version("0.11.7-next.5", "minor", "next", "0.11.6", tags, "iii")
== "0.12.0-next.3"
)
# -- Patch + same channel with existing patch-base tags --
def test_patch_resumes_counter_at_new_patch_base(self):
"""patch advances the patch component, but if tags already exist at that base, resume."""
tags = [
"iii/v0.11.6",
"iii/v0.11.7-next.1",
]
assert (
calculate_version("0.11.6", "patch", "next", "0.11.6", tags, "iii")
== "0.11.7-next.2"
)
class TestToPep440:
@pytest.mark.parametrize(
"version,expected",
[
("0.12.0", "0.12.0"),
("0.12.0-rc.1", "0.12.0rc1"),
("0.12.0-alpha.3", "0.12.0a3"),
("0.12.0-beta.2", "0.12.0b2"),
("0.12.0-next.5", "0.12.0.dev5"),
],
)
def test_conversion(self, version, expected):
assert to_pep440(version) == expected
def test_unknown_label_passes_through(self):
# Dry-run tags aren't valid PEP 440 prereleases; we just leave them.
assert to_pep440("0.12.0-dry-run.1") == "0.12.0-dry-run.1"
def test_unknown_label_matching_regex_passes_through(self):
# `dev` is [a-z]+ so it matches PRERELEASE_RE but isn't in the channel
# mapping. Must passthrough untouched rather than producing garbage.
assert to_pep440("1.2.3-dev.4") == "1.2.3-dev.4"
assert to_pep440("1.2.3-foo.4") == "1.2.3-foo.4"
# ---------- Adversarial (post-merge hardening) ----------
#
# These tests document behavior we discovered while attacking the algorithm
# after the initial port. They lock in current behavior — including a few
# documented footguns — so future refactors don't silently drift.
class TestPromotionFootguns:
"""Promotion to `none` always returns the current base, regardless of bump.
This is intentional ("drop the prerelease suffix, keep the base") but is a
sharp edge: a user on `0.15.0-next.5` who runs `patch + none` expecting
`0.15.1` will get `0.15.0`. Documented here so the surprise is visible.
"""
def test_promote_ignores_patch_bump(self):
assert (
calculate_version("0.15.0-next.5", "patch", "none", "0.14.0", ["iii/v0.14.0"], "iii")
== "0.15.0"
)
def test_promote_ignores_minor_bump(self):
assert (
calculate_version("0.15.0-next.5", "minor", "none", "0.14.0", ["iii/v0.14.0"], "iii")
== "0.15.0"
)
def test_promote_ignores_major_bump(self):
assert (
calculate_version("0.15.0-next.5", "major", "none", "0.14.0", ["iii/v0.14.0"], "iii")
== "0.15.0"
)
def test_stable_to_stable_does_not_promote(self):
# No prerelease label on current → promotion branch skipped → real bump.
assert (
calculate_version("0.15.0", "patch", "none", "0.15.0", ["iii/v0.15.0"], "iii")
== "0.15.1"
)
class TestCurrentBehindLatestStable:
"""If engine/Cargo.toml falls behind the latest stable tag (manual
downgrade, bad merge), the algorithm anchors from the *current* base
rather than `latest_stable`. That can produce a tag that overlaps an
earlier train. Locked in here so we notice if the behavior changes.
"""
def test_minor_below_stable_anchors_from_current(self):
assert (
calculate_version(
"0.12.0-next.5", "minor", "next", "0.15.0", ["iii/v0.15.0"], "iii"
)
== "0.13.0-next.1"
)
def test_major_jump_when_current_minor_below_stable_major(self):
# Current's major < stable's major → detect_current_level returns None
# → falls into the "no level" branch → bumps the current base.
assert (
calculate_version(
"0.5.0-next.1", "minor", "next", "1.2.0", ["iii/v1.2.0"], "iii"
)
== "0.6.0-next.1"
)
def test_current_equals_stable_no_level_minor(self):
# cur == stable → detect_current_level None → bump current base.
assert (
calculate_version("0.13.0", "minor", "next", "0.13.0", ["iii/v0.13.0"], "iii")
== "0.14.0-next.1"
)
class TestChannelSwitchPicksUpExistingCounter:
"""Switching channels (e.g. rc → next) uses the next_prerelease_counter
scan on the target base, which resumes from any existing tags. This is
intentional (avoids clobbering published prereleases) but can surprise
someone who expects `.1` after a fresh switch.
"""
def test_switch_resumes_counter_from_existing_target_tag(self):
tags = ["iii/v0.16.0-next.3", "iii/v0.13.0"]
# rc.2 → minor + next bumps base to 0.16.0; existing 0.16.0-next.3
# forces counter to 4 rather than 1.
assert (
calculate_version("0.15.0-rc.2", "minor", "next", "0.13.0", tags, "iii")
== "0.16.0-next.4"
)
def test_switch_starts_at_1_when_no_existing_on_target_base(self):
tags = ["iii/v0.13.0", "iii/v0.15.0-rc.2"]
assert (
calculate_version("0.15.0-rc.2", "minor", "next", "0.13.0", tags, "iii")
== "0.16.0-next.1"
)
class TestSameChannelStaysIsolatedFromOtherChannels:
"""A `next` release must not be polluted by `rc` (or alpha/beta) tags."""
def test_next_ignores_rc_tags_at_same_base(self):
tags = [
"iii/v0.15.0-rc.5",
"iii/v0.15.0-rc.6",
"iii/v0.15.0-rc.7",
"iii/v0.15.0-next.1",
]
assert (
calculate_version("0.15.0-next.1", "minor", "next", "0.13.0", tags, "iii")
== "0.15.0-next.2"
)
def test_rc_ignores_next_tags_at_same_base(self):
tags = [
"iii/v0.15.0-next.5",
"iii/v0.15.0-next.6",
"iii/v0.15.0-rc.1",
]
assert (
calculate_version("0.15.0-rc.1", "minor", "rc", "0.13.0", tags, "iii")
== "0.15.0-rc.2"
)
class TestCounterAtZero:
"""A `.next.0` tag in the wild shouldn't break counter math."""
def test_zero_counter_increments(self):
tags = ["iii/v0.15.0-next.0"]
assert (
calculate_version("0.15.0-next.0", "minor", "next", "0.13.0", tags, "iii")
== "0.15.0-next.1"
)
class TestNoLatestStable:
"""First-ever release: no stable tag yet, current is the seed prerelease."""
def test_same_channel_minor_bumps_current_when_no_stable(self):
assert (
calculate_version("0.5.0-next.5", "minor", "next", None, ["iii/v0.5.0-next.5"], "iii")
== "0.6.0-next.1"
)
def test_same_channel_patch_advances_patch_when_no_stable(self):
assert (
calculate_version("0.5.0-next.5", "patch", "next", None, [], "iii")
== "0.5.1-next.1"
)
class TestEscalationRestartsFromStable:
"""When the requested bump escalates beyond the current train's level,
we restart from the latest stable rather than the current base.
"""
def test_major_from_minor_train_anchors_at_stable(self):
# current 0.20.0-next.1, stable 0.13.0 → current_level=minor, request=major
# → escalate → anchor=latest_stable=0.13.0 → bump major → 1.0.0
assert (
calculate_version(
"0.20.0-next.1", "major", "next", "0.13.0", ["iii/v0.13.0"], "iii"
)
== "1.0.0-next.1"
)
def test_minor_from_patch_train_anchors_at_stable(self):
# current 0.13.5-next.1 (patch train above stable 0.13.0), request minor
# → escalate → anchor=0.13.0 → bump minor → 0.14.0
assert (
calculate_version(
"0.13.5-next.1", "minor", "next", "0.13.0", ["iii/v0.13.0"], "iii"
)
== "0.14.0-next.1"
)
class TestCargoVersionParsing:
"""`_read_cargo_version` returns the FIRST line that starts with
`version = `. A Cargo.toml whose first such line is in a dependency
table (rather than `[package]`) would yield the wrong version. Locked
in so we catch any regression in the parser.
"""
def test_picks_first_line_starting_with_version(self, tmp_path):
from calculate_release_version import _read_cargo_version
p = tmp_path / "Cargo.toml"
p.write_text('[package]\nname = "iii"\nversion = "1.2.3"\n')
assert _read_cargo_version(str(p)) == "1.2.3"
def test_indented_version_lines_are_skipped(self, tmp_path):
# `startswith("version = ")` requires column-0, so indented version
# lines inside dependency tables are correctly ignored.
from calculate_release_version import _read_cargo_version
p = tmp_path / "Cargo.toml"
p.write_text(
'[dependencies.foo]\n version = "9.9.9"\n[package]\nversion = "1.2.3"\n'
)
assert _read_cargo_version(str(p)) == "1.2.3"
def test_missing_version_raises(self, tmp_path):
from calculate_release_version import _read_cargo_version
p = tmp_path / "Cargo.toml"
p.write_text("[package]\nname = \"iii\"\n")
with pytest.raises(RuntimeError):
_read_cargo_version(str(p))
class TestCalculateRejectsBadInputs:
def test_unknown_bump_type_raises(self):
with pytest.raises(ValueError):
calculate_version("0.13.0", "bogus", "next", "0.13.0", [], "iii")
def test_unknown_prerelease_raises(self):
with pytest.raises(ValueError):
calculate_version("0.13.0", "minor", "bogus", "0.13.0", [], "iii")
def test_empty_prerelease_string_raises(self):
with pytest.raises(ValueError):
calculate_version("0.13.0", "minor", "", "0.13.0", [], "iii")
def test_uppercase_prerelease_raises(self):
# Only lowercase channel labels are accepted; CLI should normalize
# upstream if it ever supports anything else.
with pytest.raises(ValueError):
calculate_version("0.13.0", "minor", "NEXT", "0.13.0", [], "iii")
def test_unparseable_current_raises(self):
with pytest.raises(ValueError):
calculate_version("not-a-version", "minor", "next", "0.13.0", [], "iii")
class TestDryRunInteraction:
"""`--dry-run` is applied after `calculate_version`. It splits on the
first `-` of whatever calculate produced and tacks on a dry-run counter.
Verify the boundary cases.
"""
def test_dry_run_counter_on_stable_base(self):
from calculate_release_version import next_dry_run_counter
tags = ["iii/v0.16.0-dry-run.1", "iii/v0.16.0-dry-run.5"]
assert next_dry_run_counter("0.16.0", tags, "iii") == 6
def test_dry_run_counter_starts_at_1_when_none(self):
from calculate_release_version import next_dry_run_counter
assert next_dry_run_counter("0.16.0", [], "iii") == 1
def test_dry_run_ignores_other_prefixes(self):
from calculate_release_version import next_dry_run_counter
tags = ["other/v0.16.0-dry-run.99", "iii/v0.16.0-dry-run.1"]
assert next_dry_run_counter("0.16.0", tags, "iii") == 2
class TestLargeCounterMath:
def test_counter_well_above_int32(self):
# Python ints are unbounded; ensure huge counters work.
tags = [f"iii/v0.15.0-next.{2**40}"]
assert (
calculate_version("0.15.0-next.1", "minor", "next", "0.13.0", tags, "iii")
== f"0.15.0-next.{2**40 + 1}"
)
class TestTagPatternRejectsImpostors:
"""Bogus or malformed entries in the tag list must not affect counters."""
def test_partial_match_does_not_count(self):
tags = [
"iii/v0.15.0-next.1abc", # trailing garbage — must be rejected
"iii/v0.15.0-next.", # missing number
"iii/v0.15.0-next", # missing dot+number
"iii/v0.15.0-NEXT.5", # uppercase channel
"iii/v0.15.0-next.1", # only this one should count
]
assert (
calculate_version("0.15.0-next.1", "minor", "next", "0.13.0", tags, "iii")
== "0.15.0-next.2"
)
def test_blank_lines_in_tag_list(self):
# `_git_tags` filters blanks but `next_prerelease_counter` should be
# robust regardless of upstream filtering.
tags = ["", "iii/v0.15.0-next.3", ""]
assert (
calculate_version("0.15.0-next.1", "minor", "next", "0.13.0", tags, "iii")
== "0.15.0-next.4"
)
# ---------- calculate_version: bump="none" (alpha anchor) ----------
class TestCalculateVersionBumpNone:
def test_anchors_on_latest_stable_keeps_base(self):
# Manifest is ahead (0.19.3-next.1) but the alpha anchors on the
# latest stable (0.19.2) and never touches the official version.
v = calculate_version(
current="0.19.3-next.1",
bump_type="none",
prerelease="alpha",
latest_stable="0.19.2",
existing_tags=[],
tag_prefix="iii-alpha",
)
assert v == "0.19.2-alpha.1"
def test_counter_accumulates_under_its_own_prefix(self):
v = calculate_version(
current="0.19.3-next.1",
bump_type="none",
prerelease="alpha",
latest_stable="0.19.2",
existing_tags=[
"iii-alpha/v0.19.2-alpha.1",
"iii-alpha/v0.19.2-alpha.2",
"iii/v0.19.2", # official stable — different prefix, ignored
],
tag_prefix="iii-alpha",
)
assert v == "0.19.2-alpha.3"
def test_falls_back_to_current_base_without_stable(self):
v = calculate_version(
current="0.19.3-next.1",
bump_type="none",
prerelease="alpha",
latest_stable=None,
existing_tags=[],
tag_prefix="iii-alpha",
)
assert v == "0.19.3-alpha.1"
def test_requires_a_prerelease_label(self):
with pytest.raises(ValueError, match="requires a prerelease"):
calculate_version(
current="0.19.3-next.1",
bump_type="none",
prerelease="none",
latest_stable="0.19.2",
existing_tags=[],
tag_prefix="iii-alpha",
)
# -- create-tag: none continues an in-flight train (the fix) --
def test_none_iterates_inflight_next_train(self):
# 0.19.5-next.1 already exists above stable 0.19.4 → none iterates it
# instead of re-bumping to 0.19.6 (the original release bug).
v = calculate_version(
current="0.19.5-next.1",
bump_type="none",
prerelease="next",
latest_stable="0.19.4",
existing_tags=["iii/v0.19.4", "iii/v0.19.5-next.1"],
tag_prefix="iii",
)
assert v == "0.19.5-next.2"
def test_none_picks_highest_inflight_base(self):
v = calculate_version(
current="0.20.0-next.1",
bump_type="none",
prerelease="next",
latest_stable="0.19.4",
existing_tags=[
"iii/v0.19.4",
"iii/v0.19.5-next.1",
"iii/v0.20.0-next.1",
],
tag_prefix="iii",
)
assert v == "0.20.0-next.2"
def test_none_shares_base_across_channels(self):
# A next train set the base; switching to rc via none keeps the base.
v = calculate_version(
current="0.19.5-next.2",
bump_type="none",
prerelease="rc",
latest_stable="0.19.4",
existing_tags=["iii/v0.19.4", "iii/v0.19.5-next.1", "iii/v0.19.5-next.2"],
tag_prefix="iii",
)
assert v == "0.19.5-rc.1"
def test_none_ignores_train_at_or_below_stable(self):
# alpha case: every prerelease sits at the current stable, so none
# anchors on the stable rather than treating it as in-flight.
v = calculate_version(
current="0.19.4-alpha.2",
bump_type="none",
prerelease="alpha",
latest_stable="0.19.4",
existing_tags=[
"iii-alpha/v0.19.4-alpha.1",
"iii-alpha/v0.19.4-alpha.2",
],
tag_prefix="iii-alpha",
)
assert v == "0.19.4-alpha.3"
def test_none_inflight_scan_is_prefix_scoped(self):
# An in-flight iii/ next train must not leak into an iii-alpha run.
v = calculate_version(
current="0.19.5-next.1",
bump_type="none",
prerelease="alpha",
latest_stable="0.19.4",
existing_tags=["iii/v0.19.4", "iii/v0.19.5-next.1"],
tag_prefix="iii-alpha",
)
assert v == "0.19.4-alpha.1"
def test_full_release_sequence_with_none_iteration(self):
"""The exact flow the user wants: patch starts the train, none iterates,
promotion lands the intended patch (0.19.5, not 0.19.6)."""
tags = ["iii/v0.19.4"]
stable = "0.19.4"
def step(current, bump, pre):
result = calculate_version(current, bump, pre, stable, tags, "iii")
tags.append(f"iii/v{result}")
return result
assert step("0.19.4", "patch", "next") == "0.19.5-next.1" # start train
assert step("0.19.5-next.1", "none", "next") == "0.19.5-next.2" # iterate
assert step("0.19.5-next.2", "none", "next") == "0.19.5-next.3" # iterate
assert step("0.19.5-next.3", "patch", "none") == "0.19.5" # promote
```
## /.github/scripts/test_collect_engine_worker_interface.py
```py path="/.github/scripts/test_collect_engine_worker_interface.py"
"""Unit tests for collect_engine_worker_interface.py.
Run with: python -m pytest .github/scripts/test_collect_engine_worker_interface.py -v
"""
from __future__ import annotations
import pytest
import collect_engine_worker_interface as collect
def _fake_run_iii(calls: list[dict[str, object]]):
def fake(function_path: str, payload: dict[str, object]) -> dict[str, object]:
assert function_path == "engine::functions::info"
calls.append(payload)
ids = payload["function_ids"]
return {"functions": [{"function_id": fn_id} for fn_id in ids]}
return fake
def test_collects_details_in_one_batch(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[dict[str, object]] = []
monkeypatch.setattr(collect, "run_iii", _fake_run_iii(calls))
details = collect.collect_function_details(["a", "b", "c"])
assert calls == [{"function_ids": ["a", "b", "c"]}]
assert [d["function_id"] for d in details] == ["a", "b", "c"]
def test_chunks_batches_at_engine_limit(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[dict[str, object]] = []
monkeypatch.setattr(collect, "run_iii", _fake_run_iii(calls))
ids = [f"fn-{i}" for i in range(collect.FUNCTIONS_INFO_BATCH_MAX + 1)]
details = collect.collect_function_details(ids)
assert len(calls) == 2
assert calls[0]["function_ids"] == ids[: collect.FUNCTIONS_INFO_BATCH_MAX]
assert calls[1]["function_ids"] == ids[collect.FUNCTIONS_INFO_BATCH_MAX :]
assert [d["function_id"] for d in details] == ids
def test_no_ids_makes_no_calls(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[dict[str, object]] = []
monkeypatch.setattr(collect, "run_iii", _fake_run_iii(calls))
assert collect.collect_function_details([]) == []
assert calls == []
def test_per_item_error_marker_fails_collection(monkeypatch: pytest.MonkeyPatch) -> None:
def fake(function_path: str, payload: dict[str, object]) -> dict[str, object]:
return {
"functions": [
{"function_id": "a"},
{"function_id": "b", "error": "not_found"},
]
}
monkeypatch.setattr(collect, "run_iii", fake)
with pytest.raises(RuntimeError, match="'b': not_found"):
collect.collect_function_details(["a", "b"])
def test_malformed_envelope_fails_collection(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(collect, "run_iii", lambda *_: {"function_id": "a"})
with pytest.raises(RuntimeError, match="missing `functions` array"):
collect.collect_function_details(["a"])
```
## /.github/scripts/test_pin_docs.py
```py path="/.github/scripts/test_pin_docs.py"
"""Unit tests for pin_docs.py.
Run with: python -m pytest .github/scripts/test_pin_docs.py -v
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from pin_docs import add_prefix
from pin_docs import copy_root_to_dir
from pin_docs import dir_prefix
from pin_docs import find_index_by_tag
from pin_docs import is_excluded
from pin_docs import minor_label
from pin_docs import next_minor
from pin_docs import parse_version
from pin_docs import replace_root_with_dir
from pin_docs import rotate
from pin_docs import sort_versions
from pin_docs import strip_prefix
from pin_docs import sync_patch
from pin_docs import validate
class TestParseVersion:
def test_stable(self):
assert parse_version("0.17.0") == (0, 17, 0)
def test_prerelease_suffix_ignored(self):
assert parse_version("1.0.0-next.5") == (1, 0, 0)
def test_dry_run_suffix_ignored(self):
assert parse_version("0.17.0-dry-run.2") == (0, 17, 0)
def test_rejects_garbage(self):
with pytest.raises(ValueError):
parse_version("nope")
class TestLabelPrefix:
def test_minor_label(self):
assert minor_label(0, 17) == "0.17.x"
assert minor_label(1, 0) == "1.0.x"
def test_dir_prefix(self):
assert dir_prefix(0, 17) == "0-17-0"
assert dir_prefix(1, 0) == "1-0-0"
def test_next_minor(self):
assert next_minor("0.17.0") == (0, 18)
assert next_minor("1.0.0") == (1, 1)
assert next_minor("0.17.0-next.1") == (0, 18)
def _tabs(*pages):
return [{"tab": "Docs", "groups": [
{"group": "G", "pages": list(pages)},
{"group": "Nested", "pages": [{"group": "Sub", "pages": ["how-to/a"]}]},
]}]
class TestAddStripPrefix:
def test_add_prefix(self):
out = add_prefix(_tabs("index", "install"), "next")
assert out[0]["groups"][0]["pages"] == ["next/index", "next/install"]
assert out[0]["groups"][1]["pages"][0]["pages"] == ["next/how-to/a"]
def test_strip_prefix(self):
prefixed = add_prefix(_tabs("index"), "next")
out = strip_prefix(prefixed, "next")
assert out[0]["groups"][0]["pages"] == ["index"]
assert out[0]["groups"][1]["pages"][0]["pages"] == ["how-to/a"]
def test_add_does_not_mutate_input(self):
tabs = _tabs("index")
add_prefix(tabs, "next")
assert tabs[0]["groups"][0]["pages"] == ["index"]
def test_strip_only_removes_matching(self):
out = strip_prefix(_tabs("index"), "next")
assert out[0]["groups"][0]["pages"] == ["index"]
def test_add_prefix_keeps_shared_changelog_at_root(self):
tabs = [{"tab": "D", "groups": [{"group": "G", "pages": [
"using-iii/workers", "changelog/index", "changelog/0-11-0/x",
]}]}]
out = add_prefix(tabs, "0-16-0")
assert out[0]["groups"][0]["pages"] == [
"0-16-0/using-iii/workers", "changelog/index", "changelog/0-11-0/x",
]
class TestFindIndexByTag:
def test_finds(self):
versions = [{"tag": "Latest"}, {"tag": "Next"}, {}]
assert find_index_by_tag(versions, "Next") == 1
assert find_index_by_tag(versions, "Latest") == 0
def test_raises_when_absent(self):
with pytest.raises(ValueError):
find_index_by_tag([{"tag": "Latest"}], "Next")
class TestSortVersions:
def test_order(self):
versions = [
{"version": "0.12.x"},
{"version": "0.16.x", "tag": "Latest", "default": True},
{"version": "0.10.x"},
{"version": "0.17.x", "tag": "Next"},
]
out = sort_versions(versions)
assert [v["version"] for v in out] == ["0.17.x", "0.16.x", "0.12.x", "0.10.x"]
assert out[0]["tag"] == "Next"
assert out[1]["tag"] == "Latest"
def test_double_digit_numeric(self):
out = sort_versions([{"version": "0.2.x"}, {"version": "0.10.x"}])
assert [v["version"] for v in out] == ["0.10.x", "0.2.x"]
class TestIsExcluded:
@pytest.mark.parametrize("name", [
"docs.json", "package.json", "node_modules", "custom.css",
"navbar-counters.js", ".gitignore", ".mintignore", ".prettierrc",
"README.md", "0-11-0", "1-0-0", "index.mdx.skill.md", "next", "changelog",
])
def test_excluded(self, name):
assert is_excluded(name) is True
@pytest.mark.parametrize("name", [
"index.mdx", "using-iii", "assets", "images",
])
def test_included(self, name):
assert is_excluded(name) is False
def _make_root(tmp_path: Path) -> Path:
docs = tmp_path / "docs"
docs.mkdir()
(docs / "index.mdx").write_text("# Home latest")
(docs / "index.mdx.skill.md").write_text("sidecar")
(docs / "docs.json").write_text("{}")
(docs / ".gitignore").write_text("node_modules/")
(docs / "using-iii").mkdir()
(docs / "using-iii" / "workers.mdx").write_text("# Workers")
return docs
class TestCopyRootToDir:
def test_copies_content_excludes_infra(self, tmp_path):
docs = _make_root(tmp_path)
copy_root_to_dir(docs, "0-16-0")
pinned = docs / "0-16-0"
assert (pinned / "index.mdx").read_text() == "# Home latest"
assert (pinned / "using-iii" / "workers.mdx").exists()
assert not (pinned / "docs.json").exists()
assert not (pinned / ".gitignore").exists()
assert not (pinned / "index.mdx.skill.md").exists()
def test_does_not_copy_next_folder(self, tmp_path):
docs = _make_root(tmp_path)
(docs / "next").mkdir()
(docs / "next" / "index.mdx").write_text("# Next")
copy_root_to_dir(docs, "0-16-0")
assert not (docs / "0-16-0" / "next").exists()
def test_overwrites_existing(self, tmp_path):
docs = _make_root(tmp_path)
(docs / "0-16-0").mkdir()
(docs / "0-16-0" / "stale.mdx").write_text("old")
copy_root_to_dir(docs, "0-16-0")
assert not (docs / "0-16-0" / "stale.mdx").exists()
class TestReplaceRootWithDir:
def test_replaces_content_keeps_infra_and_next(self, tmp_path):
docs = _make_root(tmp_path)
nxt = docs / "next"
nxt.mkdir()
(nxt / "index.mdx").write_text("# Home next")
(nxt / "newpage.mdx").write_text("# New")
replace_root_with_dir(docs, "next")
assert (docs / "index.mdx").read_text() == "# Home next"
assert (docs / "newpage.mdx").exists()
assert not (docs / "using-iii").exists()
assert (docs / "docs.json").exists()
assert (docs / ".gitignore").exists()
assert (docs / "next" / "index.mdx").exists()
def _write_docs(docs: Path, versions):
(docs / "docs.json").write_text(json.dumps({"navigation": {"versions": versions}}, indent=2))
def _ready_docs(tmp_path: Path) -> Path:
"""root = Latest 0.16.x; docs/next/ = Next (label 0.17.x)."""
docs = tmp_path / "docs"
docs.mkdir()
(docs / "index.mdx").write_text("# Latest 0.16")
nxt = docs / "next"
nxt.mkdir()
(nxt / "index.mdx").write_text("# Next")
_write_docs(docs, [
{"version": "0.16.x", "tag": "Latest", "default": True,
"tabs": [{"tab": "D", "groups": [{"group": "G", "pages": ["index"]}]}]},
{"version": "0.17.x", "tag": "Next",
"tabs": [{"tab": "D", "groups": [{"group": "G", "pages": ["next/index"]}]}]},
])
return docs
class TestValidate:
def test_ok_when_next_block_and_folder_present(self, tmp_path):
assert validate(_ready_docs(tmp_path)) == 0
def test_fails_when_folder_missing(self, tmp_path):
docs = _ready_docs(tmp_path)
import shutil as _sh
_sh.rmtree(docs / "next")
assert validate(docs) == 1
def test_fails_when_folder_empty(self, tmp_path):
docs = _ready_docs(tmp_path)
import shutil as _sh
_sh.rmtree(docs / "next")
(docs / "next").mkdir()
assert validate(docs) == 1
def test_fails_when_no_next_block(self, tmp_path):
docs = tmp_path / "docs"
docs.mkdir()
(docs / "next").mkdir()
(docs / "next" / "index.mdx").write_text("x")
_write_docs(docs, [{"version": "0.16.x", "tag": "Latest"}])
assert validate(docs) == 1
def _read_versions(docs: Path):
return json.loads((docs / "docs.json").read_text())["navigation"]["versions"]
class TestRotate:
def test_rotation_0_17_0(self, tmp_path):
docs = _ready_docs(tmp_path) # root=Latest 0.16.x; next/=Next 0.17.x
rotate(docs, "0.17.0")
assert (docs / "0-16-0" / "index.mdx").read_text() == "# Latest 0.16"
assert (docs / "index.mdx").read_text() == "# Next"
assert (docs / "next" / "index.mdx").read_text() == "# Next"
versions = _read_versions(docs)
by_tag = {v.get("tag"): v for v in versions}
assert by_tag["Latest"]["version"] == "0.17.x"
assert by_tag["Latest"]["tabs"][0]["groups"][0]["pages"] == ["index"]
assert by_tag["Next"]["version"] == "0.18.x"
assert by_tag["Next"]["tabs"][0]["groups"][0]["pages"] == ["next/index"]
archived = [v for v in versions if v.get("tag") not in ("Latest", "Next")]
old = [v for v in archived if v["version"] == "0.16.x"][0]
assert old["tabs"][0]["groups"][0]["pages"] == ["0-16-0/index"]
assert versions[0]["tag"] == "Next"
assert versions[1]["tag"] == "Latest"
assert sum(1 for v in versions if v.get("default")) == 1
def test_changelog_stays_shared_at_root(self, tmp_path):
docs = tmp_path / "docs"
docs.mkdir()
(docs / "index.mdx").write_text("# Latest 0.16")
(docs / "changelog").mkdir()
(docs / "changelog" / "index.mdx").write_text("# changelog")
nxt = docs / "next"
nxt.mkdir()
(nxt / "index.mdx").write_text("# Next")
_write_docs(docs, [
{"version": "0.16.x", "tag": "Latest", "default": True,
"tabs": [{"tab": "D", "groups": [{"group": "G", "pages": ["index"]}]},
{"tab": "Changelog", "groups": [{"group": "C", "pages": ["changelog/index"]}]}]},
{"version": "0.17.x", "tag": "Next",
"tabs": [{"tab": "D", "groups": [{"group": "G", "pages": ["next/index"]}]},
{"tab": "Changelog", "groups": [{"group": "C", "pages": ["changelog/index"]}]}]},
])
rotate(docs, "0.17.0")
# Changelog folder never copied into the archive.
assert not (docs / "0-16-0" / "changelog").exists()
# Shared changelog still at the root.
assert (docs / "changelog" / "index.mdx").exists()
# Every block's changelog tab points at the root changelog.
for block in _read_versions(docs):
cl = [t for t in block["tabs"] if t.get("tab") == "Changelog"][0]
assert cl["groups"][0]["pages"] == ["changelog/index"]
def test_patch_version_delegates_to_sync(self, tmp_path):
docs = _ready_docs(tmp_path) # root=Latest 0.16.x; next/=Next 0.17.x
before = (docs / "docs.json").read_text()
rotate(docs, "0.16.1") # patch -> sync, not rotation
# No archive folder, docs.json version blocks unchanged.
assert not (docs / "0-16-0").exists()
assert (docs / "docs.json").read_text() == before
# Root content refreshed from next/.
assert (docs / "index.mdx").read_text() == "# Next"
def test_rerun_same_version_is_idempotent(self, tmp_path):
docs = _ready_docs(tmp_path)
rotate(docs, "0.17.0")
after_first = (docs / "docs.json").read_text()
rotate(docs, "0.17.0") # re-run must be a no-op
assert (docs / "docs.json").read_text() == after_first
versions = _read_versions(docs)
assert sum(1 for v in versions if v["version"] == "0.17.x") == 1
assert sum(1 for v in versions if v.get("tag") == "Latest") == 1
def test_major_release_uses_tag_and_minor_plus_one(self, tmp_path):
docs = tmp_path / "docs"
docs.mkdir()
(docs / "index.mdx").write_text("# Latest 0.17")
nxt = docs / "next"
nxt.mkdir()
(nxt / "index.mdx").write_text("# Next 0.18")
_write_docs(docs, [
{"version": "0.17.x", "tag": "Latest", "default": True,
"tabs": [{"tab": "D", "groups": [{"group": "G", "pages": ["index"]}]}]},
{"version": "0.18.x", "tag": "Next",
"tabs": [{"tab": "D", "groups": [{"group": "G", "pages": ["next/index"]}]}]},
])
rotate(docs, "1.0.0")
assert (docs / "0-17-0" / "index.mdx").exists()
assert (docs / "index.mdx").read_text() == "# Next 0.18"
by_tag = {v.get("tag"): v for v in _read_versions(docs)}
assert by_tag["Latest"]["version"] == "1.0.x"
assert by_tag["Next"]["version"] == "1.1.x"
assert by_tag["Next"]["tabs"][0]["groups"][0]["pages"] == ["next/index"]
def test_relative_links_survive_move(self, tmp_path):
# In-content links are version-relative, so rotation moves files verbatim
# without rewriting them; cross-version/shared absolute links are untouched.
docs = tmp_path / "docs"
docs.mkdir()
(docs / "using-iii").mkdir()
(docs / "using-iii" / "workers.mdx").write_text("# root workers")
(docs / "index.mdx").write_text(
"See [workers](./using-iii/workers) and [cl](/changelog/x)."
)
nxt = docs / "next"
(nxt / "using-iii").mkdir(parents=True)
(nxt / "using-iii" / "workers.mdx").write_text("# next workers")
(nxt / "index.mdx").write_text(
"See [workers](./using-iii/workers) and [old](/0-11-0/gone)."
)
_write_docs(docs, [
{"version": "0.16.x", "tag": "Latest", "default": True,
"tabs": [{"tab": "D", "groups": [{"group": "G",
"pages": ["index", "using-iii/workers"]}]}]},
{"version": "0.17.x", "tag": "Next",
"tabs": [{"tab": "D", "groups": [{"group": "G",
"pages": ["next/index", "next/using-iii/workers"]}]}]},
])
rotate(docs, "0.17.0")
# Archived old Latest: relative link intact, shared absolute untouched.
archived = (docs / "0-16-0" / "index.mdx").read_text()
assert "](./using-iii/workers)" in archived
assert "(/changelog/x)" in archived
# Promoted Latest (root): next content moved verbatim (relative survives).
root_index = (docs / "index.mdx").read_text()
assert "](./using-iii/workers)" in root_index
assert "(/0-11-0/gone)" in root_index
class TestSyncPatch:
def test_refreshes_latest_without_rotation(self, tmp_path):
docs = tmp_path / "docs"
docs.mkdir()
(docs / "index.mdx").write_text("# old latest")
nxt = docs / "next"
(nxt / "using-iii").mkdir(parents=True)
(nxt / "using-iii" / "workers.mdx").write_text("# w")
(nxt / "index.mdx").write_text("# next [w](./using-iii/workers)")
_write_docs(docs, [
{"version": "0.17.x", "tag": "Latest", "default": True,
"tabs": [{"tab": "D", "groups": [{"group": "G", "pages": ["index"]}]}]},
{"version": "0.18.x", "tag": "Next",
"tabs": [{"tab": "D", "groups": [{"group": "G",
"pages": ["next/index", "next/using-iii/workers"]}]}]},
])
before = (docs / "docs.json").read_text()
sync_patch(docs)
# Root refreshed from next/ verbatim (relative link survives the move).
root_index = (docs / "index.mdx").read_text()
assert "# old latest" not in root_index
assert "](./using-iii/workers)" in root_index
# Next folder untouched.
assert "](./using-iii/workers)" in (docs / "next" / "index.mdx").read_text()
# No archive folder, and docs.json version blocks unchanged.
assert not (docs / "0-17-0").exists()
assert (docs / "docs.json").read_text() == before
```
## /.github/scripts/validate_release_worker_matrix.py
```py path="/.github/scripts/validate_release_worker_matrix.py"
#!/usr/bin/env python3
"""Validate release-iii publish-builtin-workers matrix against worker manifests.
Checks that every matrix entry points at an engine worker manifest and that
registry slugs (manifest ``name``) stay aligned with what skill discovery emits.
Matrix ``worker`` values must remain the engine config / runtime names used by
``_publish-engine-workers.yml`` when reloading config.yaml.
"""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
import yaml
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
from discover_engine_worker_skills import discover # noqa: E402
_MATRIX_BLOCK_RE = re.compile(
r"publish-builtin-workers:.*?matrix:\s*\n\s*include:(.*?)(?:\n\s{4}\w|\n\s{2}\w|\Z)",
re.DOTALL,
)
_MATRIX_ENTRY_RE = re.compile(
r"- worker: (?P<worker>.+)\n\s+worker_dir: (?P<worker_dir>.+)"
)
def _load_matrix(repo_root: pathlib.Path) -> list[dict[str, str]]:
workflow = (repo_root / ".github/workflows/release-iii.yml").read_text(encoding="utf-8")
block = _MATRIX_BLOCK_RE.search(workflow)
if not block:
raise ValueError("could not find publish-builtin-workers matrix in release-iii.yml")
return [
{"worker": m.group("worker").strip(), "worker_dir": m.group("worker_dir").strip()}
for m in _MATRIX_ENTRY_RE.finditer(block.group(1))
]
def validate(repo_root: pathlib.Path) -> list[str]:
errors: list[str] = []
matrix = _load_matrix(repo_root)
discovered = {entry["worker_dir"]: entry["slug"] for entry in discover(repo_root)}
for entry in matrix:
worker = entry["worker"]
worker_dir = entry["worker_dir"]
manifest_path = repo_root / worker_dir / "iii.worker.yaml"
if not manifest_path.is_file():
errors.append(f"{worker_dir}: missing iii.worker.yaml for matrix worker {worker!r}")
continue
meta = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) or {}
if meta.get("type") != "engine":
errors.append(
f"{manifest_path}: type must be 'engine' (got {meta.get('type')!r})"
)
registry_name = meta.get("name")
if not isinstance(registry_name, str) or not registry_name.strip():
errors.append(f"{manifest_path}: missing or empty name field")
continue
registry_name = registry_name.strip()
if worker_dir in discovered and discovered[worker_dir] != registry_name:
errors.append(
f"{worker_dir}: discover slug {discovered[worker_dir]!r} != "
f"manifest name {registry_name!r}"
)
return errors
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", default=".", help="Repository root (default: cwd).")
args = parser.parse_args()
repo_root = pathlib.Path(args.repo_root).resolve()
errors = validate(repo_root)
if errors:
for err in errors:
print(f"::error::{err}", file=sys.stderr)
return 1
print("::notice::release worker matrix validation passed")
return 0
if __name__ == "__main__":
sys.exit(main())
```
## /.github/workflows/WORKFLOWS.md
# CI/CD Workflows
## Overview
The workflows are organized into two categories:
- **Top-level workflows** — triggered by events (push, PR, tag, manual dispatch)
- **Reusable workflows** — prefixed with `_`, called by top-level workflows via `workflow_call`
```text
┌──────────────┐
│ create-tag │ (manual dispatch)
└──────┬───────┘
│ creates git tag
▼
┌──────────────────────────────┐
│ Tag push triggers │
├──────────────────────────────┤
│ iii/v* │
▼ │
release-iii │
│ │
│ ┌──────────────────┐ │
│ │ _npm.yml │ │
│ │ _py.yml │ │
│ │ _rust-cargo.yml │ │
│ │ _rust-binary.yml│ │
│ │ _homebrew.yml │ │
│ └──────────────────┘ │
│ (reusable workflows) │
└──────────────────────────────┘
ci.yml ◄── push to main / PRs
docker-engine.yml ◄── called by release-iii / manual
license-check.yml ◄── push to main / PRs
checklist-checker.yml ◄── PR license agreement / comments
alpha-release ◄── manual dispatch from a feature branch
│ bumps + tags iii-alpha/v* (isolated; never touches main)
▼
├─► sdk-node (pnpm) / _py / _rust-cargo / _go (SDK packages)
├─► _rust-binary.yml + init-build job (engine/worker/init binaries → iii-alpha prerelease)
└─► _publish-engine-workers / -skills (builtin workers → `alpha` registry tag)
```
---
## Top-Level Workflows
### `ci.yml` — Continuous Integration
**Triggers:** push to `main`, pull requests to `main`, manual dispatch
Runs the full test suite across the monorepo. Cancels in-progress runs for PRs.
| Job | Depends On | What it does |
|-----|-----------|--------------|
| `changes` | — | Detects changed paths (engine/crates/Cargo) for scoping downstream jobs |
| `engine-build` | — | Builds debug `iii` with all features, uploads `iii-binary` artifact (critical path) |
| `engine-test` | — | Tests `iii-worker`, `iii-filesystem`, `iii-network`, `iii-init`, and `iii --all-features` |
| `engine-coverage` | `changes` | `cargo llvm-cov` on `iii --all-features`. PRs: only when engine paths change. Push/dispatch: always |
| `engine-benches` | — | `cargo bench --benches --no-run` to verify benches compile |
| `engine-fmt` | — | `cargo fmt --all -- --check` |
| `engine-build-matrix` | — | Cross-platform build validation (macOS, Windows, Linux, musl) |
| `sdk-node-ci` | `engine-build` | Type check, build, start engine, run SDK tests |
| `sdk-python-ci` | `engine-build` | Lint (ruff), type check (mypy), start engine, run pytest. Matrix: Python 3.10/3.11/3.12 |
| `sdk-rust-ci` | `engine-build` | Fmt, clippy, start engine, run cargo tests |
| `sdk-go-ci` | `engine-build` | gofmt, vet, race unit tests, start engine, run `-tags integration` tests |
| `console-ci` | — | Lint + build frontend (Node 22), build console Rust binary |
All SDK tests download the engine binary artifact and start a live engine instance before running.
---
### `create-tag.yml` — Version Bump & Tag Creation
**Triggers:** manual dispatch only
Entry point for all releases. Provides a form with:
| Input | Options |
|-------|---------|
| `target` | `iii` |
| `bump` | `patch`, `minor`, `major` |
| `prerelease` | `none`, `alpha`, `beta`, `rc`, `next` |
| `dry_run` | boolean |
**What it does:**
1. Validates it's running on `main` and all required manifest files exist
2. Reads the current version from the canonical manifest
3. Calculates the next version (handles semver bump + prerelease labels + dry-run suffixes)
4. **Stable releases only** — validates docs are ready (`pin_docs.py validate`): `docs/docs.json` must have a `Next` block and the `docs/next/` folder must be non-empty (stable releases pull their docs from `docs/next/`). If not, the workflow posts a Slack alert and aborts **before** bumping or tagging. Runs even on dry runs.
5. Converts to PEP 440 format for Python packages (e.g., `1.0.0-alpha.1` becomes `1.0.0a1`)
6. Updates all manifest files in lockstep (Cargo.toml, package.json, pyproject.toml)
7. Updates docs from `docs/next/` on stable releases (`pin_docs.py rotate`, which dispatches on the version):
- **minor/major** — rotates: archives the old Latest into `docs/OLD-MINOR-0/` (its `Latest` block becomes archived), promotes `docs/next/` into the root as a new `Latest` block labeled with the **tag version** (the official version comes from the tag and can be anything), relabels the `Next` block to `MINOR + 1` (reusing the `docs/next/` folder), and reorders the dropdown (Next, Latest, archived newest-first — Mintlify's own ordering does not work).
- **patch** — syncs in place: replaces the root content with `docs/next/`. No archive, no `Next` bump, no version-block changes — it just refreshes the current Latest's docs.
- **all prereleases** (`alpha`/`beta`/`rc`/`next`) leave docs untouched.
- In-content links are version-relative, so files move verbatim; only `docs.json` nav paths carry the version prefix.
8. Commits the version bump (including any docs changes), creates an annotated tag, and pushes both
9. Posts a Slack notification
> **Docs layout:** `Latest` lives at the docs root (unprefixed paths); `Next` lives in the fixed `docs/next/` folder; archived versions live in `docs/MAJOR-MINOR-0/` folders. `docs/changelog/` is shared by all versions — it stays at the root, is never copied into a version folder, and every version's Changelog tab points at it. Rotation assumes this shape already exists (a `Latest` root block + a `Next` block pointing at `docs/next/`).
The tag push then triggers the corresponding release workflow.
**Tag format:** `{target}/v{version}` (e.g., `iii/v1.2.3`)
---
### `alpha-release.yml` — Isolated Per-Branch Alpha
**Triggers:** manual dispatch only — run from a feature branch via "Use workflow from: `<branch>`"
Publishes an alpha prerelease of every SDK (npm, pypi, crates, go), the engine binaries, and the builtin workers **from any feature branch, without touching `main`**. Built for testing a branch end-to-end before merging.
| Input | Options |
|-------|---------|
| `dry_run` | boolean (build + validate, no upload; still pushes the alpha tag) |
**What it does:**
1. Refuses to run on `main` (use `create-tag.yml` for official releases)
2. Calculates the version with `calculate_release_version.py --bump none --counter-tag-prefix iii-alpha`: anchors on the latest stable `iii/v*` tag and appends an accumulating `-alpha.N` suffix (`0.19.2-alpha.1`, `.2`, `.3` …). The official version is never advanced.
3. Bumps all manifests in lockstep (Cargo.toml, package.json, pyproject.toml, **Go `sdkVersion` const**) into an **ephemeral commit**
4. Pushes **only** the `iii-alpha/v{version}` tag — never a branch, never `main`
5. Publishes, all checking out that tag:
- **SDK packages** — an inline `sdk-node` job (single `pnpm -r publish` over the three node packages) plus `_py.yml`, `_rust-cargo.yml`, `_go.yml`
- **Engine binaries** — `iii` and `iii-worker` via `_rust-binary.yml`, and `iii-init` via an inline `init-build` job; all attached to a GitHub **prerelease** on the `iii-alpha/v*` tag
- **Builtin workers + skills** — `_publish-engine-workers.yml` / `_publish-worker-skills.yml` published to the workers registry under a dedicated **`alpha`** tag (never `next`/`latest`)
**Isolation:** the `iii-alpha/v*` namespace does not match `release-iii.yml`'s `iii/v*` trigger, so the official pipeline never fires. Engine binaries land on a separate prerelease (own tag namespace); workers use the dedicated `alpha` registry tag — neither collides with the official `iii/v*` releases or the `next`/`latest` channels. **Console, docker and homebrew are intentionally excluded.**
**Engine install:** because the binaries live under `iii-alpha/v*`, install them with the `III_RELEASE_TAG` override on `install.sh`, e.g. `III_RELEASE_TAG=iii-alpha/v0.19.2-alpha.1 curl -fsSL https://iii.dev/install.sh | sh`. The worker-publish job uses the same override to pin the engine CLI.
**Tag format:** `iii-alpha/v{version}` (e.g., `iii-alpha/v0.19.2-alpha.1`)
---
### `release-iii.yml` — iii Release Pipeline
**Triggers:** tag push matching `iii/v*`
Orchestrates the full iii release across all package registries and distribution channels.
```text
setup (parse tag metadata, Slack notification)
│
├─► create-iii-release (GitHub Release with auto-generated notes)
│ │
│ ├─► engine-release ──────► _rust-binary.yml (9 platform targets)
│ │ │
│ │ ├─► docker ────────► docker-engine.yml (pre-built binaries, no compilation)
│ │ │
│ │ └─► homebrew-engine ► _homebrew.yml (stable only)
│ │
│ ├─► console-frontend ───► Build React frontend for embedding
│ │ │
│ │ └─► console-release ► _rust-binary.yml (with embedded frontend)
│ │ │
│ │ └─► homebrew-console ► _homebrew.yml (stable only)
│ │
│ ├─► sdk-npm ─────────────► _npm.yml
│ ├─► sdk-py ──────────────► _py.yml
│ ├─► sdk-rust ────────────► _rust-cargo.yml
│ └─► sdk-go ──────────────► _go.yml (pushes subdir-scoped module tag)
│
├─► publish-builtin-workers ► _publish-engine-workers.yml
└─► publish-worker-skills ► _publish-worker-skills.yml
│
├─► trigger-validations (dispatch downstream smoke/quickstart on success)
│
└─► notify-complete (aggregated Slack status)
```
**Downstream validations:** once every publish job succeeds (non-dry-run), `trigger-validations` dispatches `init-smoke.yml` in `iii-hq/templates` and `quickstart-validate.yml` in `iii-hq/quickstart-validator`, passing `channel=next` for a prerelease or `channel=main` for a stable release. Both repos report results to their own Slack threads. Requires the `III_CI_APP` GitHub App installed on both repos with `actions: write`.
**Setup job** parses the tag to determine:
- `version` — stripped prefix (e.g., `iii/v1.2.3` becomes `1.2.3`)
- `is_prerelease` — true if version contains a prerelease label
- `npm_tag` — dist-tag for npm (`latest`, `alpha`, `beta`, `rc`, `dry-run`)
- `dry_run` — true if version ends with `-dry-run.N`
**Concurrency:** only one iii release runs at a time per repository.
**Skipped on dry run:** GitHub Release creation, Homebrew publish, downstream validations.
---
### `docker-engine.yml` — Docker Image Build & Publish
**Triggers:** called by `release-iii.yml` after engine binaries are built, or manual dispatch with a release tag
Downloads pre-built binaries from the GitHub Release (no Rust compilation) and packages them into a minimal distroless Docker image.
| Job | Runner | What it does |
|-----|--------|--------------|
| `setup` | `ubuntu-latest` | Parse version from release tag |
| `build` (amd64) | `ubuntu-latest` | Download pre-built binary, build + push image |
| `build` (arm64) | `ubuntu-24.04-arm` | Download pre-built binary, build + push image (native ARM runner) |
| `publish` | `ubuntu-latest` | Create multi-platform manifest, Trivy security scan, push to GHCR + DockerHub |
**Registries:** GHCR (`ghcr.io`) and DockerHub (`iiidev/iii`)
**Security:** Trivy vulnerability scanning (CRITICAL + HIGH), distroless nonroot runtime.
---
### `license-check.yml` — License Header Check
**Triggers:** push to `main`, pull requests to `main`
Uses [hawkeye](https://github.com/korandoru/hawkeye) to verify license headers across source files, configured via `engine/licenserc.toml`.
### `checklist-checker.yml` — License Agreement Check
**Triggers:** `pull_request_target` for pull request changes, `issue_comment` for PR comments
Requires external contributors to acknowledge the Apache 2.0 contributor license agreement before merge. Contributors can satisfy the gate by checking the license box in the PR description or by replying with the exact acknowledgement phrase posted by the bot. iii team members with `write`, `maintain`, or `admin` repository permission are skipped.
The workflow posts a sticky PR comment and publishes the `license-agreement` commit status on the PR head SHA. Branch protection should require the `license-agreement` status context.
---
## Reusable Workflows
All reusable workflows support `dry_run` mode and Slack thread notifications.
### `_npm.yml` — NPM Publish
Publishes a Node.js package to the npm registry.
| Input | Purpose |
|-------|---------|
| `package_path` | Directory containing the package to publish |
| `npm_tag` | dist-tag (`latest`, `alpha`, `beta`, `rc`) |
| `build_filter` | pnpm filter for building the package |
| `pre_build_filter` | pnpm filter for building dependencies first (optional) |
Uses `pnpm publish` with `--no-git-checks` and `--access public`.
### `_py.yml` — PyPI Publish
Publishes a Python package to PyPI.
Builds with `python -m build`, validates with `twine check` on dry run, publishes via `pypa/gh-action-pypi-publish`.
### `_rust-cargo.yml` — Cargo Publish
Publishes a Rust crate to crates.io via `cargo publish`.
### `_go.yml` — Go Module Publish
"Publishes" a Go module by pushing a subdirectory-scoped git tag (`sdk/packages/go/iii/vX.Y.Z`) — Go has no registry, so `go get` resolves the module from the repo via the Go proxy. No token required. Dry run runs `go build/vet/test` and `go mod verify` without tagging.
### `_rust-binary.yml` — Rust Binary Release
Cross-compiles a Rust binary for 9 platform targets and uploads them to a GitHub Release.
**Targets:**
| Platform | Runner |
|----------|--------|
| `x86_64-apple-darwin` | `macos-latest` |
| `aarch64-apple-darwin` | `macos-latest` |
| `x86_64-pc-windows-msvc` | `windows-latest` |
| `i686-pc-windows-msvc` | `windows-latest` |
| `aarch64-pc-windows-msvc` | `windows-latest` |
| `x86_64-unknown-linux-gnu` | `ubuntu-22.04` |
| `x86_64-unknown-linux-musl` | `ubuntu-latest` |
| `aarch64-unknown-linux-gnu` | `ubuntu-22.04` |
| `armv7-unknown-linux-gnueabihf` | `ubuntu-22.04` |
Supports downloading a pre-built artifact (used by console to embed the frontend build).
Uses `taiki-e/upload-rust-binary-action` for building and uploading.
### `_homebrew.yml` — Homebrew Formula Publish
Generates and publishes a Homebrew formula to the `iii-hq/homebrew-tap` repository.
1. Downloads release tarballs from GitHub Releases
2. Calculates SHA256 checksums
3. Generates a Ruby formula file with platform-specific URLs
4. Tests the formula locally (`brew audit`, `brew install`, version check)
5. Commits and pushes to the tap repository
Only runs for stable (non-prerelease) versions.
### `_publish-engine-workers.yml` — Publish Builtin Engine Worker
Collects a worker's live interface from a running III engine and POSTs to `POST /publish`. Called by `release-iii.yml` for each builtin engine worker.
### `_publish-worker-skills.yml` — Publish Worker Skills
Discovers worker directories with an `iii.worker.yaml` manifest and a non-empty `skills/` tree (repo-wide, including e.g. `crates/iii-worker/src/sandbox_daemon`), builds payloads via `build_skills_payload.py`, and POSTs to `POST /w/{slug}/skills`. Called by `release-iii.yml` (after builtin worker publish) and by the manual `publish-worker-skills.yml` workflow.
| Input | Purpose |
|-------|---------|
| `registry_tag` | Version tag on the registry (`latest`, `next`) |
| `api_url` | Workers registry base URL |
---
### `publish-worker-skills.yml` — Manual Skills Publish
**Triggers:** `workflow_dispatch` only
Publishes skill markdown for all workers with an `iii.worker.yaml` manifest and a non-empty `skills/` tree. Choose `registry_tag` (`latest` or `next`) at dispatch time.
---
## Secrets
| Secret | Used by |
|--------|---------|
| `III_CI_APP_ID` / `III_CI_APP_PRIVATE_KEY` | GitHub App token for pushing tags, creating releases, updating homebrew-tap |
| `NPM_TOKEN` | npm registry authentication |
| `PYPI_API_TOKEN` | PyPI publishing |
| `CARGO_REGISTRY_TOKEN` | crates.io publishing |
| `DOCKERHUB_USERNAME` / `DOCKERHUB_PASSWORD` | DockerHub publishing |
| `SLACK_BOT_TOKEN` / `SLACK_CHANNEL_ID` | Slack release notifications |
| `SLACK_WEBHOOK_URL` | Slack Docker notifications |
| `WORKERS_REGISTRY_API_KEY` | Workers registry publish (`_publish-engine-workers`, `_publish-worker-skills`) |
---
## Release Flow (End to End)
1. Developer triggers `create-tag` workflow manually, selecting target/bump/prerelease
2. Workflow bumps versions across all manifests, commits, and pushes a tag
3. Tag push triggers the `release-iii` workflow
4. Release workflow fans out to reusable workflows in parallel
5. Each reusable workflow posts progress to a Slack thread
6. Final job aggregates results and updates the parent Slack message with overall status
## /.github/workflows/_go.yml
```yml path="/.github/workflows/_go.yml"
name: Go Module Publish
# Publishing a Go module is not an upload: there is no registry. A module is "published"
# by pushing a git tag, after which `go get <module>@<version>` resolves it straight from
# the repo via proxy.golang.org. So unlike _npm.yml / _rust-cargo.yml there is no token —
# the only side effect is the tag push, and dry-run runs the full validation without it.
#
# Tag shape (the one hard constraint): the module lives in a subdirectory, so Go requires
# its tag to be PREFIXED with the module path relative to the repo root, i.e.
# `sdk/packages/go/iii/vX.Y.Z`. The repo's release tag `iii/vX.Y.Z` does NOT make a
# subdirectory module resolvable (`go get …@vX.Y.Z` would fail). This job therefore pushes
# its own subdir-scoped tag in lockstep with the engine release (issue iii-hq/iii#1719).
on:
workflow_call:
inputs:
package_path:
description: 'Path to the Go module (e.g., sdk/packages/go/iii)'
required: true
type: string
module_path:
description: 'Go module import path (e.g., github.com/iii-hq/iii/sdk/packages/go/iii)'
required: true
type: string
version:
description: 'Semantic version without the v prefix (e.g., 0.1.0)'
required: true
type: string
ref:
description: 'Git ref to checkout (default: the triggering ref)'
required: false
type: string
default: ''
dry_run:
description: 'Build and validate without pushing the tag'
required: false
type: boolean
default: false
slack_thread_ts:
description: 'Slack parent message timestamp for thread replies (optional)'
required: false
type: string
default: ''
slack_label:
description: 'Label for this step in Slack notifications (optional)'
required: false
type: string
default: ''
secrets:
SLACK_BOT_TOKEN:
required: false
SLACK_CHANNEL_ID:
required: false
jobs:
publish:
name: Publish Go module
runs-on: ubuntu-latest
permissions:
contents: write # to push the version tag
steps:
- name: Notify Slack — in progress
if: inputs.slack_thread_ts != ''
id: slack
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
thread_ts: "${{ inputs.slack_thread_ts }}"
text: ":large_yellow_circle: ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — in progress"
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history so we can tag
ref: ${{ inputs.ref }}
# Don't leave the write-scoped token in git config: the caller
# controls `ref` and we run go build/test before pushing. Auth is
# supplied only to the tag-push step below.
persist-credentials: false
- uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: ${{ inputs.package_path }}/go.sum
# Validation — runs in BOTH dry-run and real release.
- name: Build, vet, test, verify
working-directory: ${{ inputs.package_path }}
run: |
go build ./...
go vet ./...
go test ./...
go mod verify
# The only side effect — skipped on dry-run. Subdir-scoped tag (see header note).
- name: Push subdirectory-scoped module tag
if: ${{ inputs.dry_run == false }}
env:
TAG: ${{ inputs.package_path }}/v${{ inputs.version }}
GH_TOKEN: ${{ github.token }}
run: |
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "Tag $TAG already exists, nothing to push."
else
git tag "$TAG"
# Auth scoped to this push only (checkout used persist-credentials: false).
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$TAG"
fi
- name: Verify the proxy resolves the published version
if: ${{ inputs.dry_run == false }}
env:
GOPROXY: proxy.golang.org
GOFLAGS: -mod=mod
MODULE_PATH: ${{ inputs.module_path }}
VERSION: ${{ inputs.version }}
# Pass inputs through quoted env vars rather than interpolating them into the
# command, and validate the version shape, so a malformed input can't be abused by
# the shell. The proxy is eventually-consistent right after a tag push, so retry a
# few times before failing.
run: |
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?{{contextString}}#39;; then
echo "Refusing to resolve: invalid version '$VERSION'" >&2
exit 1
fi
for attempt in 1 2 3 4 5; do
if go list -m "${MODULE_PATH}@v${VERSION}"; then
exit 0
fi
echo "proxy not ready (attempt $attempt), retrying in 10s..." >&2
sleep 10
done
echo "proxy did not resolve ${MODULE_PATH}@v${VERSION} after retries" >&2
exit 1
- name: Notify Slack — result
if: always() && steps.slack.outputs.ts != ''
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.update
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
ts: "${{ steps.slack.outputs.ts }}"
text: "${{ job.status == 'success' && ':large_green_circle:' || ':red_circle:' }} ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — ${{ job.status }}"
```
## /.github/workflows/_homebrew.yml
```yml path="/.github/workflows/_homebrew.yml"
name: Homebrew Publish
on:
workflow_call:
inputs:
bin_name:
description: 'Binary name (e.g., iii, iii-console)'
required: true
type: string
formula_class:
description: 'Ruby class name for the formula (e.g., Iii, IiiConsole)'
required: true
type: string
formula_path:
description: 'Path inside homebrew-tap repo (e.g., Formula/iii.rb)'
required: true
type: string
description:
description: 'Formula description'
required: true
type: string
homepage:
description: 'Homepage URL for the formula'
required: true
type: string
release_repo:
description: 'GitHub repo hosting the release assets (e.g., iii-hq/iii)'
required: true
type: string
release_tag:
description: 'GitHub release tag for downloading assets (e.g., iii/v1.0.0)'
required: true
type: string
version:
description: 'Version number without v prefix (e.g., 1.0.0)'
required: true
type: string
include_linux:
description: 'Whether to include Linux targets in the formula'
required: false
type: boolean
default: false
dry_run:
description: 'Run in dry-run mode (skip actual publish)'
required: false
type: boolean
default: false
slack_thread_ts:
description: 'Slack parent message timestamp for thread replies (optional)'
required: false
type: string
default: ''
slack_label:
description: 'Label for this step in Slack notifications (optional)'
required: false
type: string
default: ''
secrets:
III_CI_APP_ID:
required: true
III_CI_APP_PRIVATE_KEY:
required: true
SLACK_BOT_TOKEN:
required: false
SLACK_CHANNEL_ID:
required: false
env:
TAP_REPO: iii-hq/homebrew-tap
jobs:
publish-homebrew:
name: Update Homebrew Formula
runs-on: macos-latest
permissions:
contents: read
steps:
- name: Notify Slack — in progress
if: inputs.slack_thread_ts != ''
id: slack
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
thread_ts: "${{ inputs.slack_thread_ts }}"
text: ":large_yellow_circle: ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — in progress"
- name: Validate version format
env:
VERSION: ${{ inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid version format. Expected: X.Y.Z (got $VERSION)"
exit 1
fi
echo "Version validated: $VERSION"
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.III_CI_APP_ID }}
private-key: ${{ secrets.III_CI_APP_PRIVATE_KEY }}
owner: iii-hq
repositories: homebrew-tap
permission-contents: write
- name: Checkout tap repository
uses: actions/checkout@v4
with:
repository: ${{ env.TAP_REPO }}
token: ${{ steps.generate_token.outputs.token }}
path: homebrew-tap
- name: Download release assets
env:
RELEASE_REPO: ${{ inputs.release_repo }}
RELEASE_TAG: ${{ inputs.release_tag }}
BIN_NAME: ${{ inputs.bin_name }}
INCLUDE_LINUX: ${{ inputs.include_linux }}
run: |
BASE_URL="https://github.com/${RELEASE_REPO}/releases/download/${RELEASE_TAG}"
curl -L --fail -o "${BIN_NAME}-x86_64-apple-darwin.tar.gz" \
"${BASE_URL}/${BIN_NAME}-x86_64-apple-darwin.tar.gz"
curl -L --fail -o "${BIN_NAME}-aarch64-apple-darwin.tar.gz" \
"${BASE_URL}/${BIN_NAME}-aarch64-apple-darwin.tar.gz"
REQUIRED_FILES="${BIN_NAME}-x86_64-apple-darwin.tar.gz ${BIN_NAME}-aarch64-apple-darwin.tar.gz"
if [[ "$INCLUDE_LINUX" == "true" ]]; then
curl -L --fail -o "${BIN_NAME}-x86_64-unknown-linux-gnu.tar.gz" \
"${BASE_URL}/${BIN_NAME}-x86_64-unknown-linux-gnu.tar.gz"
curl -L --fail -o "${BIN_NAME}-aarch64-unknown-linux-gnu.tar.gz" \
"${BASE_URL}/${BIN_NAME}-aarch64-unknown-linux-gnu.tar.gz"
REQUIRED_FILES="$REQUIRED_FILES ${BIN_NAME}-x86_64-unknown-linux-gnu.tar.gz ${BIN_NAME}-aarch64-unknown-linux-gnu.tar.gz"
fi
for file in $REQUIRED_FILES; do
if [ ! -s "$file" ]; then
echo "::error::$file is missing or empty"
exit 1
fi
done
ls -lh *.tar.gz
- name: Calculate SHA256 checksums
id: checksums
env:
BIN_NAME: ${{ inputs.bin_name }}
INCLUDE_LINUX: ${{ inputs.include_linux }}
run: |
MACOS_X86_SHA=$(shasum -a 256 "${BIN_NAME}-x86_64-apple-darwin.tar.gz" | awk '{print $1}')
MACOS_ARM_SHA=$(shasum -a 256 "${BIN_NAME}-aarch64-apple-darwin.tar.gz" | awk '{print $1}')
echo "macos_x86_64_sha256=${MACOS_X86_SHA}" >> "$GITHUB_OUTPUT"
echo "macos_aarch64_sha256=${MACOS_ARM_SHA}" >> "$GITHUB_OUTPUT"
if [[ "$INCLUDE_LINUX" == "true" ]]; then
LINUX_X86_SHA=$(shasum -a 256 "${BIN_NAME}-x86_64-unknown-linux-gnu.tar.gz" | awk '{print $1}')
LINUX_ARM_SHA=$(shasum -a 256 "${BIN_NAME}-aarch64-unknown-linux-gnu.tar.gz" | awk '{print $1}')
echo "linux_x86_64_sha256=${LINUX_X86_SHA}" >> "$GITHUB_OUTPUT"
echo "linux_aarch64_sha256=${LINUX_ARM_SHA}" >> "$GITHUB_OUTPUT"
fi
- name: Generate formula
env:
BIN_NAME: ${{ inputs.bin_name }}
FORMULA_CLASS: ${{ inputs.formula_class }}
FORMULA_FILE: ${{ inputs.formula_path }}
DESCRIPTION: ${{ inputs.description }}
HOMEPAGE: ${{ inputs.homepage }}
VERSION: ${{ inputs.version }}
RELEASE_REPO: ${{ inputs.release_repo }}
RELEASE_TAG: ${{ inputs.release_tag }}
INCLUDE_LINUX: ${{ inputs.include_linux }}
MACOS_X86_SHA: ${{ steps.checksums.outputs.macos_x86_64_sha256 }}
MACOS_ARM_SHA: ${{ steps.checksums.outputs.macos_aarch64_sha256 }}
LINUX_X86_SHA: ${{ steps.checksums.outputs.linux_x86_64_sha256 }}
LINUX_ARM_SHA: ${{ steps.checksums.outputs.linux_aarch64_sha256 }}
run: |
cd homebrew-tap
mkdir -p "$(dirname "$FORMULA_FILE")"
BASE_URL="https://github.com/${RELEASE_REPO}/releases/download/${RELEASE_TAG}"
{
echo "class ${FORMULA_CLASS} < Formula"
echo " desc \"${DESCRIPTION}\""
echo " homepage \"${HOMEPAGE}\""
echo " version \"${VERSION}\""
echo " license \"Apache-2.0\""
echo ""
echo " on_macos do"
echo " if Hardware::CPU.arm?"
echo " url \"${BASE_URL}/${BIN_NAME}-aarch64-apple-darwin.tar.gz\""
echo " sha256 \"${MACOS_ARM_SHA}\""
echo " else"
echo " url \"${BASE_URL}/${BIN_NAME}-x86_64-apple-darwin.tar.gz\""
echo " sha256 \"${MACOS_X86_SHA}\""
echo " end"
echo " end"
if [[ "$INCLUDE_LINUX" == "true" ]]; then
echo ""
echo " on_linux do"
echo " if Hardware::CPU.arm?"
echo " url \"${BASE_URL}/${BIN_NAME}-aarch64-unknown-linux-gnu.tar.gz\""
echo " sha256 \"${LINUX_ARM_SHA}\""
echo " else"
echo " url \"${BASE_URL}/${BIN_NAME}-x86_64-unknown-linux-gnu.tar.gz\""
echo " sha256 \"${LINUX_X86_SHA}\""
echo " end"
echo " end"
fi
echo ""
echo " def install"
echo " bin.install \"${BIN_NAME}\""
echo " end"
echo ""
echo " test do"
echo " assert_match version.to_s, shell_output(\"\#{bin}/${BIN_NAME} --version\")"
echo " end"
echo "end"
} > "$FORMULA_FILE"
echo "Formula generated:"
cat "$FORMULA_FILE"
- name: Test formula locally
env:
BIN_NAME: ${{ inputs.bin_name }}
FORMULA_FILE: ${{ inputs.formula_path }}
run: |
brew tap iii-hq/tap
cp "$GITHUB_WORKSPACE/homebrew-tap/${FORMULA_FILE}" \
"$(brew --repo iii-hq/tap)/${FORMULA_FILE}"
brew audit --new "iii-hq/tap/${BIN_NAME}" || true
brew install "iii-hq/tap/${BIN_NAME}"
"${BIN_NAME}" --version
brew uninstall "${BIN_NAME}"
brew untap iii-hq/tap
- name: Commit and push changes
if: inputs.dry_run != true
env:
BIN_NAME: ${{ inputs.bin_name }}
VERSION: ${{ inputs.version }}
FORMULA_FILE: ${{ inputs.formula_path }}
run: |
cd homebrew-tap
git config user.name "iii-ci[bot]"
git config user.email "iii-ci[bot]@users.noreply.github.com"
git add "$FORMULA_FILE"
git commit -m "chore: update ${BIN_NAME} to v${VERSION}
Automated update from release workflow."
git push origin main
echo "Formula published to homebrew-tap!"
- name: Dry-run summary
if: inputs.dry_run == true
env:
BIN_NAME: ${{ inputs.bin_name }}
VERSION: ${{ inputs.version }}
run: |
echo "DRY RUN MODE - No changes pushed"
echo ""
echo "Formula validated successfully for ${BIN_NAME} v${VERSION}"
echo "Set dry_run=false to publish for real."
- name: Notify Slack — result
if: always() && steps.slack.outputs.ts != ''
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.update
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
ts: "${{ steps.slack.outputs.ts }}"
text: "${{ job.status == 'success' && ':large_green_circle:' || ':red_circle:' }} ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — ${{ job.status }}"
```
## /.github/workflows/_npm.yml
```yml path="/.github/workflows/_npm.yml"
name: NPM Publish
on:
workflow_call:
inputs:
package_path:
description: 'Package directory to publish (e.g., sdk/packages/node/iii)'
required: true
type: string
npm_tag:
description: 'NPM dist-tag (latest, alpha, beta, rc)'
required: true
type: string
build_filter:
description: 'pnpm filter for building the package (e.g., iii-sdk)'
required: true
type: string
ref:
description: 'Git ref to checkout (default: the triggering ref)'
required: false
type: string
default: ''
pre_build_filter:
description: 'pnpm filter for building dependencies first (optional)'
required: false
type: string
default: ''
dry_run:
description: 'Build and validate without publishing'
required: false
type: boolean
default: false
slack_thread_ts:
description: 'Slack parent message timestamp for thread replies (optional)'
required: false
type: string
default: ''
slack_label:
description: 'Label for this step in Slack notifications (optional)'
required: false
type: string
default: ''
secrets:
NPM_TOKEN:
required: true
SLACK_BOT_TOKEN:
required: false
SLACK_CHANNEL_ID:
required: false
jobs:
publish:
name: Publish to NPM
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Notify Slack — in progress
if: inputs.slack_thread_ts != ''
id: slack
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
thread_ts: "${{ inputs.slack_thread_ts }}"
text: ":large_yellow_circle: ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — in progress"
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build dependencies
if: inputs.pre_build_filter != ''
run: pnpm --filter "${{ inputs.pre_build_filter }}" build
- name: Build package
run: pnpm --filter "${{ inputs.build_filter }}" build
- name: Setup NPM authentication
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
- name: Publish to NPM
working-directory: ${{ inputs.package_path }}
run: pnpm publish --no-git-checks --tag "${{ inputs.npm_tag }}" --access public ${{ inputs.dry_run == true && '--dry-run' || '' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Notify Slack — result
if: always() && steps.slack.outputs.ts != ''
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.update
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
ts: "${{ steps.slack.outputs.ts }}"
text: "${{ job.status == 'success' && ':large_green_circle:' || ':red_circle:' }} ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — ${{ job.status }}"
```
## /.github/workflows/_publish-engine-workers.yml
```yml path="/.github/workflows/_publish-engine-workers.yml"
name: Publish engine worker to registry
on:
workflow_call:
inputs:
worker:
description: 'Worker name (e.g. iii-http)'
required: true
type: string
worker_dir:
description: 'Path to the worker source directory containing iii.worker.yaml'
required: true
type: string
version:
description: 'Expected iii engine semver version'
required: false
type: string
default: ''
release_tag:
description: 'Exact release tag to install the engine from (e.g. iii-alpha/v0.19.2-alpha.1). When set, the CLI is pinned to this tag via III_RELEASE_TAG instead of the iii/v<version> default. Used by the isolated alpha flow.'
required: false
type: string
default: ''
ref:
description: 'Git ref to checkout (default: the triggering ref). The payload builder asserts engine/Cargo.toml matches the published version, so this must point at the version-bumped commit.'
required: false
type: string
default: ''
registry_tag:
description: 'Registry tag (latest, next, ...)'
required: false
type: string
default: latest
api_url:
description: 'Workers registry base URL'
required: false
type: string
default: 'https://api.workers.iii.dev'
secrets:
WORKERS_REGISTRY_API_KEY:
description: 'API key for the workers registry'
required: false
jobs:
publish:
name: POST /publish (${{ inputs.worker }})
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install pyyaml
run: pip install --quiet pyyaml
# Pin the iii CLI to the version we are publishing. Without this the
# install script picks "latest stable", which can be older than the
# tag being released and may lack features that the publish step
# depends on (for example `engine::workers::list` exposing engine
# internal workers landed in PR #1585). The install script also
# bundles `iii-worker`, which iii-sandbox needs at runtime.
- name: Install iii CLI
env:
VERSION_INPUT: ${{ inputs.version }}
RELEASE_TAG_INPUT: ${{ inputs.release_tag }}
# install.sh queries the GitHub releases API; unauthenticated calls
# share a 60/hr per-IP limit that hosted runners routinely exhaust.
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
curl -fsSL https://install.iii.dev/iii/main/install.sh -o /tmp/install-iii.sh
if [[ -n "$RELEASE_TAG_INPUT" ]]; then
# Isolated alpha: install the exact tag from its own namespace
# (e.g. iii-alpha/v0.19.2-alpha.1), which the iii/v<version>
# default cannot resolve.
III_RELEASE_TAG="$RELEASE_TAG_INPUT" sh /tmp/install-iii.sh
else
expected="$VERSION_INPUT"
if [[ -z "$expected" && "${GITHUB_REF_NAME:-}" == iii/v* ]]; then
expected="${GITHUB_REF_NAME#iii/v}"
fi
install_args=()
if [[ "$expected" == *-* ]]; then
install_args+=("--next")
fi
VERSION="$expected" sh /tmp/install-iii.sh "${install_args[@]}"
fi
{
echo "$HOME/.local/bin"
echo "$HOME/.iii/bin"
} >> "$GITHUB_PATH"
export PATH="$HOME/.local/bin:$HOME/.iii/bin:$PATH"
iii --version
# Boot the engine with no workers so we can snapshot the trigger
# types registered by `mandatory` workers (e.g. iii-observability's
# `log` trigger type). The collect step diffs those out so the target
# worker's payload only carries the types it actually contributes.
- name: Start III engine
run: |
set -euo pipefail
printf 'workers: []\n' > config.yaml
iii --config config.yaml --no-update-check > iii-engine.log 2>&1 &
echo "$!" > iii-engine.pid
for _ in {1..60}; do
if ! kill -0 "$(cat iii-engine.pid)" 2>/dev/null; then
echo "::error::iii engine exited before becoming ready"
cat iii-engine.log
exit 1
fi
if iii trigger 'engine::workers::list' --json '{}' >/tmp/iii-workers.json 2>/tmp/iii-trigger.err; then
cat /tmp/iii-workers.json
exit 0
fi
sleep 1
done
echo "::error::iii engine did not become ready"
cat /tmp/iii-trigger.err || true
cat iii-engine.log || true
exit 1
- name: Snapshot engine trigger types baseline
run: |
set -euo pipefail
# `engine::trigger-types::list` was retired in the engine_fn rework.
# `engine::triggers::list` now returns trigger TYPES (templates).
iii trigger \
'engine::triggers::list' \
--json '{"include_internal": false}' \
> trigger-types-baseline.json
cat trigger-types-baseline.json
# Reload the engine with the target worker by overwriting config.yaml.
# The engine watches config.yaml and reloads on change, so the target
# worker registers without restarting.
- name: Reload engine with target worker (${{ inputs.worker }})
env:
WORKER: ${{ inputs.worker }}
WORKER_DIR: ${{ inputs.worker_dir }}
run: |
set -euo pipefail
python3 - <<'PY'
import os, pathlib, yaml
worker = os.environ["WORKER"]
worker_dir = pathlib.Path(os.environ["WORKER_DIR"])
manifest = worker_dir / "iii.worker.yaml"
meta = yaml.safe_load(manifest.read_text(encoding="utf-8")) or {}
entry = {"name": worker}
if meta.get("config") is not None:
entry["config"] = meta["config"]
config = {"workers": [entry]}
pathlib.Path("config.yaml").write_text(yaml.dump(config, sort_keys=False), encoding="utf-8")
PY
cat config.yaml
- name: Collect worker interface
env:
WORKER: ${{ inputs.worker }}
run: |
python3 .github/scripts/collect_engine_worker_interface.py \
--worker "$WORKER" \
--out worker-interface.json \
--wait-seconds 120 \
--trigger-types-baseline trigger-types-baseline.json
- name: Build payload
id: payload
env:
WORKER: ${{ inputs.worker }}
WORKER_DIR: ${{ inputs.worker_dir }}
VERSION: ${{ inputs.version }}
REGISTRY_TAG: ${{ inputs.registry_tag }}
REPO_URL: ${{ format('https://github.com/{0}', github.repository) }}
run: |
set -euo pipefail
expected="$VERSION"
if [[ -z "$expected" && "${GITHUB_REF_NAME:-}" == iii/v* ]]; then
expected="${GITHUB_REF_NAME#iii/v}"
fi
python3 .github/scripts/build_engine_publish_payload.py \
--worker "$WORKER" \
--worker-dir "$WORKER_DIR" \
--expected-version "$expected" \
--registry-tag "$REGISTRY_TAG" \
--repo-url "$REPO_URL" \
--interface-json worker-interface.json \
--out payload.json
version=$(python3 -c 'import json; print(json.load(open("payload.json"))["version"])')
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: POST /publish
env:
API_URL: ${{ inputs.api_url }}
API_KEY: ${{ secrets.WORKERS_REGISTRY_API_KEY }}
WORKER: ${{ inputs.worker }}
PUBLISHED_VERSION: ${{ steps.payload.outputs.version }}
run: |
if [[ -z "$API_KEY" ]]; then
echo "::error::WORKERS_REGISTRY_API_KEY secret is not set"
exit 1
fi
http=$(curl -sS -o response.json -w '%{http_code}' \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-X POST "$API_URL/publish" \
--data-binary @payload.json)
echo "HTTP $http"
cat response.json
if [[ "$http" != "200" && "$http" != "409" ]]; then
echo "::error::publish failed with HTTP $http"
exit 1
fi
if [[ "$http" == "409" ]]; then
echo "::notice::${WORKER} v${PUBLISHED_VERSION} already published (409) — skipping"
fi
- name: Dump III logs
if: failure()
run: |
echo "::group::iii engine log"
# Full log instead of tail: iii-sandbox spawns iii-worker as a
# subprocess whose stderr is forwarded into this file, and the
# interesting output is at the start (initial spawn) — a tail
# would only show the post-mortem reconnect loop.
cat iii-engine.log || true
echo "::endgroup::"
- name: Stop III engine
if: always()
run: |
if [[ -f iii-engine.pid ]]; then
kill "$(cat iii-engine.pid)" 2>/dev/null || true
fi
```
## /.github/workflows/_publish-worker-skills.yml
```yml path="/.github/workflows/_publish-worker-skills.yml"
name: Publish worker skills to registry
on:
workflow_call:
inputs:
registry_tag:
description: 'Registry version tag (latest, next, ...)'
required: false
type: string
default: latest
api_url:
description: 'Workers registry base URL'
required: false
type: string
default: 'https://api.workers.iii.dev'
secrets:
WORKERS_REGISTRY_API_KEY:
description: 'API key for the workers registry'
required: false
jobs:
discover:
name: Discover workers with skills
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.discover.outputs.matrix }}
has_workers: ${{ steps.discover.outputs.has_workers }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install pyyaml
run: pip install --quiet pyyaml
- name: Discover worker skill bundles
id: discover
run: |
python3 .github/scripts/discover_engine_worker_skills.py --repo-root .
publish:
name: POST /w/${{ matrix.slug }}/skills
needs: discover
if: needs.discover.outputs.has_workers == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.discover.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Build skills payload (${{ matrix.slug }})
id: payload
env:
REGISTRY_TAG: ${{ inputs.registry_tag }}
run: |
set -euo pipefail
python3 .github/scripts/build_skills_payload.py \
--worker-dir "${{ matrix.worker_dir }}" \
--version "$REGISTRY_TAG" \
--out skills-payload.json
- name: POST /w/${{ matrix.slug }}/skills
if: steps.payload.outputs.skip != 'true'
env:
API_URL: ${{ inputs.api_url }}
API_KEY: ${{ secrets.WORKERS_REGISTRY_API_KEY }}
SLUG: ${{ matrix.slug }}
run: |
set -euo pipefail
if [[ -z "$API_KEY" ]]; then
echo "::error::WORKERS_REGISTRY_API_KEY secret is not set"
exit 1
fi
http=$(curl -sS -o response.json -w '%{http_code}' \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-X POST "$API_URL/w/$SLUG/skills" \
--data-binary @skills-payload.json)
echo "HTTP $http"
cat response.json
if [[ "$http" != "200" ]]; then
echo "::error::publish skills failed for $SLUG with HTTP $http"
exit 1
fi
echo "::notice::published skills for $SLUG (version tag in payload)"
```
## /.github/workflows/_py.yml
```yml path="/.github/workflows/_py.yml"
name: PyPI Publish
on:
workflow_call:
inputs:
package_path:
description: 'Package directory (e.g., sdk/packages/python/iii)'
required: true
type: string
ref:
description: 'Git ref to checkout (default: the triggering ref)'
required: false
type: string
default: ''
dry_run:
description: 'Build and validate without publishing'
required: false
type: boolean
default: false
slack_thread_ts:
description: 'Slack parent message timestamp for thread replies (optional)'
required: false
type: string
default: ''
slack_label:
description: 'Label for this step in Slack notifications (optional)'
required: false
type: string
default: ''
secrets:
PYPI_API_TOKEN:
required: true
SLACK_BOT_TOKEN:
required: false
SLACK_CHANNEL_ID:
required: false
jobs:
publish:
name: Publish to PyPI
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Notify Slack — in progress
if: inputs.slack_thread_ts != ''
id: slack
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
thread_ts: "${{ inputs.slack_thread_ts }}"
text: ":large_yellow_circle: ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — in progress"
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install build tools
run: pip install build twine
- name: Build package
working-directory: ${{ inputs.package_path }}
run: python -m build
- name: Validate package
if: inputs.dry_run == true
run: twine check ${{ inputs.package_path }}/dist/*
- name: Publish to PyPI
if: inputs.dry_run != true
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: ${{ inputs.package_path }}/dist/
password: ${{ secrets.PYPI_API_TOKEN }}
- name: Notify Slack — result
if: always() && steps.slack.outputs.ts != ''
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.update
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
ts: "${{ steps.slack.outputs.ts }}"
text: "${{ job.status == 'success' && ':large_green_circle:' || ':red_circle:' }} ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — ${{ job.status }}"
```
## /.github/workflows/_rust-binary.yml
```yml path="/.github/workflows/_rust-binary.yml"
name: Rust Binary Release
on:
workflow_call:
inputs:
bin_name:
description: 'Binary name (e.g., iii, iii-console)'
required: true
type: string
manifest_path:
description: 'Path to Cargo.toml (e.g., engine/Cargo.toml)'
required: true
type: string
tag_name:
description: 'Git tag for the GitHub Release (e.g., iii/v1.0.0)'
required: true
type: string
tag_prefix:
description: 'Tag prefix used to filter stable tags for release-note diff (e.g., "iii/" or "motia/"). Leave empty to skip previous-stable lookup and fall back to GitHub default.'
required: false
type: string
default: ''
is_prerelease:
description: 'Mark as pre-release'
required: false
type: boolean
default: false
skip_create_release:
description: 'Skip GH release creation (assumes release already exists for the tag)'
required: false
type: boolean
default: false
artifact_name:
description: 'Pre-built artifact to download before building (optional)'
required: false
type: string
default: ''
artifact_dest:
description: 'Destination path for the downloaded artifact (optional)'
required: false
type: string
default: ''
features:
description: 'Cargo features to enable (e.g., iii-filesystem/embed-init)'
required: false
type: string
default: ''
init_artifacts:
description: 'Download iii-init cross-compiled artifacts for embedding'
required: false
type: boolean
default: false
dry_run:
description: 'Build binaries without uploading or creating releases'
required: false
type: boolean
default: false
slack_thread_ts:
description: 'Slack parent message timestamp for thread replies (optional)'
required: false
type: string
default: ''
targets:
description: 'JSON array of target triples to build. When provided, only matching targets from the default matrix are built. Leave empty for full 9-target matrix.'
required: false
type: string
default: ''
slack_label:
description: 'Label for this step in Slack notifications (optional)'
required: false
type: string
default: ''
system_deps:
description: 'Space-separated list of additional apt packages to install on Linux (e.g., libcap-ng-dev)'
required: false
type: string
default: ''
secrets:
III_CI_APP_ID:
required: true
III_CI_APP_PRIVATE_KEY:
required: true
SLACK_BOT_TOKEN:
required: false
SLACK_CHANNEL_ID:
required: false
env:
CARGO_TERM_COLOR: always
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_PKG_CONFIG_ALLOW_CROSS: '1'
PKG_CONFIG_SYSROOT_DIR_aarch64_unknown_linux_gnu: /usr/aarch64-linux-gnu
PKG_CONFIG_PATH_aarch64_unknown_linux_gnu: /usr/lib/aarch64-linux-gnu/pkgconfig
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS: '-C link-arg=-L/usr/lib/aarch64-linux-gnu'
jobs:
prepare-matrix:
name: Prepare Build Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Compute build matrix
id: set-matrix
env:
TARGETS_INPUT: ${{ inputs.targets }}
run: |
# Default 9-target matrix
DEFAULT='{"include":[{"target":"x86_64-apple-darwin","os":"macos-latest"},{"target":"aarch64-apple-darwin","os":"macos-latest"},{"target":"x86_64-pc-windows-msvc","os":"windows-latest"},{"target":"i686-pc-windows-msvc","os":"windows-latest"},{"target":"aarch64-pc-windows-msvc","os":"windows-latest"},{"target":"x86_64-unknown-linux-gnu","os":"ubuntu-22.04"},{"target":"x86_64-unknown-linux-musl","os":"ubuntu-latest"},{"target":"aarch64-unknown-linux-gnu","os":"ubuntu-22.04"},{"target":"armv7-unknown-linux-gnueabihf","os":"ubuntu-22.04"}]}'
if [ -z "$TARGETS_INPUT" ]; then
echo "matrix=$DEFAULT" >> "$GITHUB_OUTPUT"
echo "Using default 9-target matrix"
else
# Filter default matrix to only include targets in the provided JSON array
FILTERED=$(echo "$DEFAULT" | jq -c --argjson targets "$TARGETS_INPUT" '
.include |= [ .[] | select(.target as $t | $targets | index($t)) ]
')
echo "matrix=$FILTERED" >> "$GITHUB_OUTPUT"
echo "Filtered matrix to targets: $TARGETS_INPUT"
fi
pre-build:
name: Pre-build
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
slack_ts: ${{ steps.slack.outputs.ts }}
steps:
- name: Notify Slack — in progress
if: inputs.slack_thread_ts != ''
id: slack
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
thread_ts: "${{ inputs.slack_thread_ts }}"
text: ":large_yellow_circle: ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — in progress"
- name: Generate token
if: inputs.skip_create_release != true && inputs.dry_run != true
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.III_CI_APP_ID }}
private-key: ${{ secrets.III_CI_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
if: inputs.skip_create_release != true && inputs.dry_run != true
with:
token: ${{ steps.generate_token.outputs.token }}
fetch-tags: true
- name: Compute previous stable tag
id: prev
if: inputs.skip_create_release != true && inputs.dry_run != true && inputs.tag_prefix != ''
env:
TAG_PREFIX: ${{ inputs.tag_prefix }}
CURRENT_TAG: ${{ inputs.tag_name }}
run: |
PREFIX="${TAG_PREFIX}v"
PREV_STABLE=$(git tag --list "${PREFIX}*" --sort=-v:refname \
| grep -E "^${PREFIX}[0-9]+\.[0-9]+\.[0-9]+\{{contextString}}quot; \
| grep -v "^${CURRENT_TAG}\{{contextString}}quot; \
| head -n 1 || true)
echo "previous_tag=${PREV_STABLE}" >> "$GITHUB_OUTPUT"
echo "::notice::Previous stable tag for release notes: ${PREV_STABLE:-<none, GitHub default>}"
- name: Create GitHub Release
if: inputs.skip_create_release != true && inputs.dry_run != true
uses: softprops/action-gh-release@v2
with:
token: ${{ steps.generate_token.outputs.token }}
tag_name: ${{ inputs.tag_name }}
name: ${{ inputs.bin_name }} ${{ inputs.tag_name }}
draft: false
prerelease: ${{ inputs.is_prerelease }}
generate_release_notes: true
previous_tag: ${{ steps.prev.outputs.previous_tag }}
build:
name: Build ${{ matrix.target }}
needs: [prepare-matrix, pre-build]
runs-on: ${{ matrix.os }}
permissions:
contents: write
env:
SKIP_FRONTEND_BUILD: ${{ inputs.artifact_name != '' && '1' || '' }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare-matrix.outputs.matrix) }}
steps:
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.III_CI_APP_ID }}
private-key: ${{ secrets.III_CI_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Download pre-built artifact
if: inputs.artifact_name != ''
uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact_name }}
path: ${{ inputs.artifact_dest }}
- name: Download iii-init (x86_64-musl)
if: inputs.init_artifacts == true
uses: actions/download-artifact@v4
with:
name: iii-init-x86_64-unknown-linux-musl
path: target/x86_64-unknown-linux-musl/release/
- name: Download iii-init (aarch64-musl)
if: inputs.init_artifacts == true
uses: actions/download-artifact@v4
with:
name: iii-init-aarch64-unknown-linux-musl
path: target/aarch64-unknown-linux-musl/release/
- name: Install additional system dependencies
if: runner.os == 'Linux' && inputs.system_deps != ''
env:
BUILD_TARGET: ${{ matrix.target }}
run: |
sudo apt-get update
case "$BUILD_TARGET" in
aarch64-unknown-linux-gnu)
sudo dpkg --add-architecture arm64
sudo sed -i 's/^deb /deb [arch=amd64] /' /etc/apt/sources.list
echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -cs) main restricted universe" | sudo tee /etc/apt/sources.list.d/arm64.list
echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -cs)-updates main restricted universe" | sudo tee -a /etc/apt/sources.list.d/arm64.list
sudo apt-get update
for pkg in ${{ inputs.system_deps }}; do
sudo apt-get install -y "${pkg}:arm64"
done
;;
*)
sudo apt-get install -y ${{ inputs.system_deps }}
;;
esac
- name: Install cross-compilation tools
if: runner.os == 'Linux'
env:
BUILD_TARGET: ${{ matrix.target }}
run: |
sudo apt-get update
case "$BUILD_TARGET" in
x86_64-unknown-linux-musl)
sudo apt-get install -y musl-tools
;;
aarch64-unknown-linux-gnu)
sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross
;;
armv7-unknown-linux-gnueabihf)
sudo apt-get install -y gcc-arm-linux-gnueabihf libc6-dev-armhf-cross
;;
esac
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache cargo registry & build
uses: Swatinem/rust-cache@v2
with:
key: ${{ inputs.bin_name }}-${{ matrix.target }}
- name: Build and upload binary
uses: taiki-e/upload-rust-binary-action@v1
with:
bin: ${{ inputs.bin_name }}
target: ${{ matrix.target }}
features: ${{ inputs.features }}
ref: refs/tags/${{ inputs.tag_name }}
tar: unix
zip: windows
checksum: sha256
manifest-path: ${{ inputs.manifest_path }}
token: ${{ steps.generate_token.outputs.token }}
dry-run: ${{ inputs.dry_run }}
notify-result:
name: Notify Result
needs: [pre-build, build]
if: always() && needs.pre-build.outputs.slack_ts != ''
runs-on: ubuntu-latest
steps:
- name: Update thread message
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.update
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
ts: "${{ needs.pre-build.outputs.slack_ts }}"
text: "${{ needs.build.result == 'success' && ':large_green_circle:' || ':red_circle:' }} ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — ${{ needs.build.result }}"
```
## /.github/workflows/_rust-cargo.yml
```yml path="/.github/workflows/_rust-cargo.yml"
name: Rust Cargo Publish
on:
workflow_call:
inputs:
package_path:
description: 'Path to the crate directory (e.g., sdk/packages/rust/iii)'
required: true
type: string
ref:
description: 'Git ref to checkout (default: the triggering ref)'
required: false
type: string
default: ''
dry_run:
description: 'Build and validate without publishing'
required: false
type: boolean
default: false
slack_thread_ts:
description: 'Slack parent message timestamp for thread replies (optional)'
required: false
type: string
default: ''
slack_label:
description: 'Label for this step in Slack notifications (optional)'
required: false
type: string
default: ''
secrets:
CARGO_REGISTRY_TOKEN:
required: true
SLACK_BOT_TOKEN:
required: false
SLACK_CHANNEL_ID:
required: false
env:
CARGO_TERM_COLOR: always
jobs:
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
# cargo publish authenticates to crates.io via CARGO_REGISTRY_TOKEN, not
# git. Drop the inherited write scope so a verify-build can't reuse the
# token (matches _npm.yml / _py.yml).
permissions:
contents: read
steps:
- name: Notify Slack — in progress
if: inputs.slack_thread_ts != ''
id: slack
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
thread_ts: "${{ inputs.slack_thread_ts }}"
text: ":large_yellow_circle: ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — in progress"
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
# cargo publish doesn't push to git; don't leave the token in config.
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Publish to crates.io
working-directory: ${{ inputs.package_path }}
run: cargo publish ${{ inputs.dry_run == true && '--dry-run' || '' }}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Notify Slack — result
if: always() && steps.slack.outputs.ts != ''
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.update
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
ts: "${{ steps.slack.outputs.ts }}"
text: "${{ job.status == 'success' && ':large_green_circle:' || ':red_circle:' }} ${{ inputs.slack_label }}${{ inputs.dry_run == true && ' (dry run)' || '' }} — ${{ job.status }}"
```
## /.github/workflows/alpha-release.yml
```yml path="/.github/workflows/alpha-release.yml"
name: Alpha Release
# Publishes an isolated alpha prerelease of every SDK + engine-internal lib
# from ANY feature branch, without touching main. The version anchors on the
# latest stable tag and appends an accumulating -alpha.N suffix
# (0.19.2-alpha.1, .2, .3 ...).
#
# Publishes:
# * SDK packages — npm + pypi + crates + go.
# * Engine binaries — iii / iii-worker / iii-init, attached to a GitHub
# prerelease on the iii-alpha/v* tag.
# * Builtin workers + skills — to the workers registry under a dedicated
# `alpha` tag (never next/latest).
# Console, docker and homebrew are intentionally excluded.
#
# Isolation guarantees:
# * Tags live under the iii-alpha/v* namespace, so release-iii.yml
# (trigger iii/v*) never fires.
# * No commit or push to main or to the feature branch — the version bump
# lives only in an ephemeral commit that the alpha tag points at.
# * Engine binaries land on their own prerelease and workers use the
# dedicated `alpha` registry tag, so nothing collides with the official
# iii/v* releases or the next/latest channels.
#
# Run from: Actions -> Alpha Release -> "Use workflow from: <feature-branch>".
# Note: dry_run still creates the iii-alpha tag (publishers need it to check
# out the bumped commit) and increments the counter; it just skips uploads.
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Build and validate without publishing (still pushes the iii-alpha tag)'
required: false
type: boolean
default: false
permissions:
contents: write # prepare tag push, go tag, GitHub prerelease + binary uploads
id-token: write # OIDC / npm + pypi provenance (sdk-node, _py.yml)
jobs:
prepare:
name: Bump & tag alpha
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
tag: ${{ steps.versions.outputs.tag }}
version: ${{ steps.versions.outputs.version }}
python_version: ${{ steps.versions.outputs.python_version }}
npm_tag: ${{ steps.versions.outputs.npm_tag }}
dry_run: ${{ inputs.dry_run }}
steps:
- name: Checkout selected branch
uses: actions/checkout@v4
with:
fetch-depth: 0
# Default GITHUB_TOKEN (contents: write) is persisted in git config
# and reused by the tag push below. No app token needed: we only
# push a tag in the fresh iii-alpha/* namespace, never a branch.
- name: Refuse to run on main
run: |
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "$BRANCH" == "main" ]]; then
echo "::error::alpha-release is for feature branches; use create-tag for main releases"
exit 1
fi
echo "Alpha release from branch: $BRANCH"
- name: Calculate alpha version
id: versions
run: |
python3 .github/scripts/calculate_release_version.py \
--target iii \
--bump none \
--prerelease alpha \
--counter-tag-prefix iii-alpha \
--current-version-file engine/Cargo.toml
- uses: dtolnay/rust-toolchain@stable
- name: Configure git identity
run: |
git config user.name "iii-ci[bot]"
git config user.email "iii-ci[bot]@users.noreply.github.com"
- name: Bump all manifests in lockstep
env:
VERSION: ${{ steps.versions.outputs.version }}
PY_VERSION: ${{ steps.versions.outputs.python_version }}
run: |
python3 .github/scripts/bump_manifests.py \
--root . \
--version "$VERSION" \
--python-version "$PY_VERSION"
- name: Sync Cargo.lock
run: cargo update --workspace
- name: Commit bump (ephemeral) and push the alpha tag only
env:
TAG: ${{ steps.versions.outputs.tag }}
run: |
git add -A
git commit -m "chore(alpha): $TAG [skip ci]"
git tag -a "$TAG" -m "Alpha release $TAG"
# Push ONLY the tag. The branch (and main) are never modified on
# the remote — the bump commit is reachable solely via this tag.
git push origin "$TAG"
echo "Pushed alpha tag: $TAG"
sdk-node:
name: SDK Node (npm)
needs: prepare
if: ${{ !failure() && !cancelled() }}
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.tag }}
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build node SDK packages
run: >-
pnpm
--filter "@iii-dev/observability"
--filter "@iii-dev/helpers"
--filter iii-sdk
--filter iii-browser-sdk
build
- name: Setup NPM authentication
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
# Recursive publish: pnpm resolves topological order (observability
# before iii / iii-browser) and rewrites `workspace:*` deps to the
# concrete alpha version automatically — no manual ordering needed.
- name: Publish node SDK packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: >-
pnpm
--filter "@iii-dev/observability"
--filter "@iii-dev/helpers"
--filter iii-sdk
--filter iii-browser-sdk
-r publish
--no-git-checks
--tag "${{ needs.prepare.outputs.npm_tag }}"
--access public
${{ needs.prepare.outputs.dry_run == 'true' && '--dry-run' || '' }}
observability-py:
name: Observability Python (pypi)
# The shim pins iii-helpers at the release version; publish helpers first so
# PyPI can resolve the dependency.
needs: [prepare, helpers-py]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_py.yml
with:
package_path: sdk/packages/python/observability
ref: ${{ needs.prepare.outputs.tag }}
dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
secrets:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
helpers-py:
name: Helpers Python (pypi)
needs: prepare
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_py.yml
with:
package_path: sdk/packages/python/helpers
ref: ${{ needs.prepare.outputs.tag }}
dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
secrets:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
sdk-py:
name: SDK Python (pypi)
# iii depends on iii-observability and iii-helpers; publish them first
# so the package is installable from PyPI.
needs: [prepare, observability-py, helpers-py]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_py.yml
with:
package_path: sdk/packages/python/iii
ref: ${{ needs.prepare.outputs.tag }}
dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
secrets:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
observability-rust:
name: Observability Rust (cargo)
# The shim depends on iii-helpers; cargo publish resolves it from crates.io,
# so helpers must be published first.
needs: [prepare, helpers-rust]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_rust-cargo.yml
with:
package_path: sdk/packages/rust/observability
ref: ${{ needs.prepare.outputs.tag }}
dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
secrets:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
helpers-rust:
name: Helpers Rust (cargo)
needs: prepare
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_rust-cargo.yml
with:
package_path: sdk/packages/rust/helpers
ref: ${{ needs.prepare.outputs.tag }}
dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
secrets:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
sdk-rust:
name: SDK Rust (cargo)
# iii-sdk depends on both iii-observability and iii-helpers; cargo
# publish resolves those from crates.io, so both must be published first.
needs: [prepare, observability-rust, helpers-rust]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_rust-cargo.yml
with:
package_path: sdk/packages/rust/iii
ref: ${{ needs.prepare.outputs.tag }}
dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
secrets:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
sdk-go:
name: SDK Go (module)
needs: prepare
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_go.yml
with:
package_path: sdk/packages/go/iii
module_path: github.com/iii-hq/iii/sdk/packages/go/iii
version: ${{ needs.prepare.outputs.version }}
ref: ${{ needs.prepare.outputs.tag }}
dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
# ──────────────────────────────────────────────────────────────
# Engine pipeline (isolated): binaries -> GitHub prerelease on the
# iii-alpha/v* tag, builtin workers -> dedicated `alpha` registry tag.
# Nothing here touches the official iii/v* releases or the next/latest
# worker channels. Console, docker and homebrew are intentionally out.
# ──────────────────────────────────────────────────────────────
create-alpha-release:
name: Create alpha GitHub prerelease
needs: prepare
if: ${{ !failure() && !cancelled() && needs.prepare.outputs.dry_run != 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
# The tag was already pushed by `prepare`; this attaches a prerelease
# to it. Uses the default GITHUB_TOKEN (contents: write).
- name: Create GitHub prerelease
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.tag }}
name: iii ${{ needs.prepare.outputs.version }} (alpha)
draft: false
prerelease: true
generate_release_notes: false
init-build:
name: Build iii-init (${{ matrix.target }})
needs: [prepare, create-alpha-release]
if: ${{ !failure() && !cancelled() }}
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
publish_aliases: 'x86_64-unknown-linux-gnu x86_64-apple-darwin'
- target: aarch64-unknown-linux-musl
publish_aliases: 'aarch64-unknown-linux-gnu aarch64-apple-darwin'
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.tag }}
# Build runs checked-out code; uploads go through softprops (token
# via env), so the write token never needs to sit in git config.
persist-credentials: false
- name: Install cross-compilation tools
run: |
sudo apt-get update
sudo apt-get install -y musl-tools
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: iii-init-${{ matrix.target }}
- name: Build iii-init
run: cargo build -p iii-init --target ${{ matrix.target }} --release
- name: Upload init binary as workflow artifact
uses: actions/upload-artifact@v4
with:
name: iii-init-${{ matrix.target }}
path: target/${{ matrix.target }}/release/iii-init
retention-days: 1
- name: Package and upload to release
if: needs.prepare.outputs.dry_run != 'true'
run: |
cd target/${{ matrix.target }}/release
tar czf iii-init-${{ matrix.target }}.tar.gz iii-init
sha256sum iii-init-${{ matrix.target }}.tar.gz | awk '{print $1}' > iii-init-${{ matrix.target }}.sha256
for alias in ${{ matrix.publish_aliases }}; do
cp iii-init-${{ matrix.target }}.tar.gz "iii-init-${alias}.tar.gz"
sha256sum "iii-init-${alias}.tar.gz" | awk '{print $1}' > "iii-init-${alias}.sha256"
done
- name: Upload release assets
if: needs.prepare.outputs.dry_run != 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.tag }}
files: target/${{ matrix.target }}/release/iii-init-*.tar.gz,target/${{ matrix.target }}/release/iii-init-*.sha256
engine-release:
name: Engine Binary (alpha)
needs: [prepare, create-alpha-release]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_rust-binary.yml
with:
bin_name: iii
manifest_path: engine/Cargo.toml
tag_name: ${{ needs.prepare.outputs.tag }}
tag_prefix: iii-alpha/
is_prerelease: true
skip_create_release: true
dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
secrets:
III_CI_APP_ID: ${{ secrets.III_CI_APP_ID }}
III_CI_APP_PRIVATE_KEY: ${{ secrets.III_CI_APP_PRIVATE_KEY }}
worker-release:
name: Worker Binary (alpha)
needs: [prepare, create-alpha-release, init-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_rust-binary.yml
with:
bin_name: iii-worker
manifest_path: crates/iii-worker/Cargo.toml
tag_name: ${{ needs.prepare.outputs.tag }}
tag_prefix: iii-alpha/
is_prerelease: true
skip_create_release: true
dry_run: ${{ needs.prepare.outputs.dry_run == 'true' }}
features: embed-init,embed-libkrunfw
init_artifacts: true
targets: '["aarch64-apple-darwin","x86_64-unknown-linux-gnu","aarch64-unknown-linux-gnu"]'
system_deps: libcap-ng-dev
secrets:
III_CI_APP_ID: ${{ secrets.III_CI_APP_ID }}
III_CI_APP_PRIVATE_KEY: ${{ secrets.III_CI_APP_PRIVATE_KEY }}
publish-builtin-workers:
name: Publish builtin workers (alpha)
needs: [prepare, create-alpha-release, engine-release, worker-release]
if: ${{ !failure() && !cancelled() && needs.prepare.outputs.dry_run != 'true' }}
strategy:
fail-fast: false
matrix:
include:
- { worker: iii-worker-manager, worker_dir: engine/src/workers/worker }
- { worker: iii-http, worker_dir: engine/src/workers/rest_api }
- { worker: iii-stream, worker_dir: engine/src/workers/stream }
- { worker: iii-state, worker_dir: engine/src/workers/state }
- { worker: iii-pubsub, worker_dir: engine/src/workers/pubsub }
- { worker: iii-cron, worker_dir: engine/src/workers/cron }
- { worker: iii-observability, worker_dir: engine/src/workers/observability }
- { worker: iii-exec, worker_dir: engine/src/workers/shell }
- { worker: iii-bridge, worker_dir: engine/src/workers/bridge_client }
- { worker: configuration, worker_dir: engine/src/workers/configuration }
- { worker: iii-sandbox, worker_dir: crates/iii-worker/src/sandbox_daemon }
- { worker: iii-engine-functions, worker_dir: engine/src/workers/engine_fn }
uses: ./.github/workflows/_publish-engine-workers.yml
with:
worker: ${{ matrix.worker }}
worker_dir: ${{ matrix.worker_dir }}
version: ${{ needs.prepare.outputs.version }}
release_tag: ${{ needs.prepare.outputs.tag }}
# Checkout the version-bumped commit: build_engine_publish_payload.py
# asserts engine/Cargo.toml matches the published version.
ref: ${{ needs.prepare.outputs.tag }}
registry_tag: alpha
secrets:
WORKERS_REGISTRY_API_KEY: ${{ secrets.WORKERS_REGISTRY_API_KEY }}
publish-worker-skills:
name: Publish worker skills (alpha)
needs: [prepare, publish-builtin-workers]
if: ${{ !failure() && !cancelled() && needs.prepare.outputs.dry_run != 'true' }}
uses: ./.github/workflows/_publish-worker-skills.yml
with:
registry_tag: alpha
secrets:
WORKERS_REGISTRY_API_KEY: ${{ secrets.WORKERS_REGISTRY_API_KEY }}
```
## /.github/workflows/bench-release.yml
```yml path="/.github/workflows/bench-release.yml"
name: Benchmark Release
on:
push:
tags:
- 'iii/v*'
workflow_dispatch:
permissions:
contents: write
concurrency:
group: bench-release-${{ github.repository }}
env:
CARGO_TERM_COLOR: always
III_TELEMETRY_ENABLED: "false"
jobs:
benchmark:
name: Run Benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Run benchmarks
run: cargo bench -p iii --bench '*' -- --output-format bencher 2>&1 | tee bench-output.txt
- name: Extract version from tag
id: meta
run: |
if [[ "$GITHUB_REF_NAME" == iii/v* ]]; then
VERSION="${GITHUB_REF_NAME#iii/v}"
else
VERSION="manual-$(date +%Y%m%d-%H%M%S)"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Store benchmark result
uses: benchmark-action/github-action-benchmark@v1
with:
name: iii Engine Benchmarks
tool: 'cargo'
output-file-path: bench-output.txt
gh-pages-branch: gh-pages
benchmark-data-dir-path: dev/bench
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
alert-threshold: '120%'
comment-on-alert: true
summary-always: true
alert-comment-cc-users: '@iii-hq/engine'
- name: Upload raw criterion data
uses: actions/upload-artifact@v4
with:
name: criterion-data-${{ steps.meta.outputs.version }}
path: target/criterion/
retention-days: 90
- name: Generate benchmark summary
if: always()
id: summary
run: |
VERSION="${{ steps.meta.outputs.version }}"
BENCH_COUNT=$(grep -c "^test " bench-output.txt 2>/dev/null || echo "0")
if [[ "${{ job.status }}" == "success" ]]; then
STATUS_ICON=":large_green_circle:"
else
STATUS_ICON=":red_circle:"
fi
PAGES_URL="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/dev/bench/"
WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
{
echo "body<<EOFSLACK"
printf '%s %s\n%s\n%s' \
"${STATUS_ICON}" "*Benchmark Results* — iii v${VERSION}" \
"${BENCH_COUNT} benchmarks collected" \
"<${PAGES_URL}|View historical charts> | <${WORKFLOW_URL}|View workflow>"
echo ""
echo "EOFSLACK"
} >> "$GITHUB_OUTPUT"
- name: Post to Slack
if: always()
continue-on-error: true
uses: slackapi/slack-github-action@v2.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "Benchmark results for iii v${{ steps.meta.outputs.version }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "${{ steps.summary.outputs.body }}"
```
## /.github/workflows/checklist-checker.yml
```yml path="/.github/workflows/checklist-checker.yml"
name: License Agreement Check
on:
pull_request_target:
types: [opened, edited, synchronize, reopened, ready_for_review]
permissions:
contents: read
concurrency:
group: license-agreement-${{ github.event.pull_request.number }}
# Let an in-flight run finish so it can post its final commit status. With
# `cancel-in-progress: true` a synchronize event would cancel the prior run
# mid-flight, leaving a CANCELLED check on the PR rollup even for team-member
# PRs that the script would otherwise mark `success`.
cancel-in-progress: false
jobs:
license-agreement:
# Skip bot-authored PRs. Org membership is NOT gated here: the event payload's
# author_association conceals private org members, so membership is checked
# authoritatively in the script using the App token (which can see them).
if: |
github.actor != 'github-actions[bot]' &&
github.event.pull_request.user.type != 'Bot'
runs-on: ubuntu-latest
# SECURITY: this is a `pull_request_target` job, so it runs in the base repo
# context with access to secrets (APP_PRIVATE_KEY). It is safe ONLY because it
# checks out the base branch and runs our own trusted script against the PR as
# data. NEVER check out the PR head or execute fork-provided code (npm install,
# build, PR-supplied actions) in this job, doing so would expose the secret to
# untrusted contributors. Run untrusted PR code in a separate `pull_request` job.
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '22'
# Mint a short-lived installation token. Unlike GITHUB_TOKEN, an App
# installation token can write to the base repo on fork PRs, which is the
# only kind of PR this workflow ever processes.
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ secrets.III_CI_APP_ID }}
private-key: ${{ secrets.III_CI_APP_PRIVATE_KEY }}
- name: Check license agreement
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
run: node .github/scripts/license-agreement-check.mjs
```
## /.github/workflows/cloudfront-functions-test.yml
```yml path="/.github/workflows/cloudfront-functions-test.yml"
name: CloudFront Functions Tests
on:
pull_request:
paths:
- 'infra/terraform/website/cloudfront_functions/**'
- '.github/workflows/cloudfront-functions-test.yml'
push:
branches: [main]
paths:
- 'infra/terraform/website/cloudfront_functions/**'
jobs:
test:
name: node --test
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Run CloudFront Function unit tests
run: node --test infra/terraform/website/cloudfront_functions/redirects.test.js
```
## /.github/workflows/license-check.yml
```yml path="/.github/workflows/license-check.yml"
name: License Header Check
on:
push:
branches: [main]
paths-ignore:
- 'docs/**'
- 'skills/**'
- 'website/**'
- 'blog/**'
- '**/*.md'
- '**/*.mdx'
- '.cursor/**'
pull_request:
branches: [main]
paths-ignore:
- 'docs/**'
- 'skills/**'
- 'website/**'
- 'blog/**'
- '**/*.md'
- '**/*.mdx'
- '.cursor/**'
jobs:
check-license-headers:
name: Check License Headers
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check license headers
uses: korandoru/hawkeye@v6
with:
config: engine/licenserc.toml
```
## /.github/workflows/publish-worker-skills.yml
```yml path="/.github/workflows/publish-worker-skills.yml"
name: Publish worker skills
on:
workflow_dispatch:
inputs:
registry_tag:
description: 'Registry version tag to attach skills to'
required: true
type: choice
options:
- latest
- next
default: latest
jobs:
publish:
uses: ./.github/workflows/_publish-worker-skills.yml
with:
registry_tag: ${{ inputs.registry_tag }}
secrets: inherit
```
## /.github/workflows/skill-check.yml
```yml path="/.github/workflows/skill-check.yml"
name: Skill check
on:
pull_request:
paths:
- "docs/next/**/*.md"
- "docs/next/**/*.mdx"
- ".skill-check.yaml"
- ".github/workflows/skill-check.yml"
push:
branches: [main]
paths:
- "docs/next/**/*.md"
- "docs/next/**/*.mdx"
- ".skill-check.yaml"
- ".github/workflows/skill-check.yml"
permissions:
contents: read # report-only; the action no longer commits back
pull-requests: write # for the sticky PR comment
actions: write # for the persistent AI skip-cache (v0.3+)
jobs:
verify:
name: Verify docs skills
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Run iii-skill-check
uses: iii-hq/skills-and-validation@v0.4
with:
write: false
scope: pr-diff
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
```
## /.prettierrc
```prettierrc path="/.prettierrc"
{
"proseWrap": "always",
"printWidth": 100
}
```
## /blog/.nvmrc
```nvmrc path="/blog/.nvmrc"
22
```
The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.