{"id":48879297,"url":"https://github.com/pasunboneleve/devloop","last_synced_at":"2026-04-16T02:04:10.790Z","repository":{"id":346502701,"uuid":"1190271520","full_name":"pasunboneleve/devloop","owner":"pasunboneleve","description":"Stateful orchestration for local dev workflows.","archived":false,"fork":false,"pushed_at":"2026-04-06T10:09:57.000Z","size":423,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-06T11:29:45.332Z","etag":null,"topics":["cheap-to-change","composability","developer-experience","fast-feedback-cycle","inner-loop","local-development","multi-process","platform-engineering","system-design","unix-philosophy","workflow"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pasunboneleve.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-24T05:53:02.000Z","updated_at":"2026-04-06T10:10:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pasunboneleve/devloop","commit_stats":null,"previous_names":["pasunboneleve/devloop"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pasunboneleve/devloop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pasunboneleve%2Fdevloop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pasunboneleve%2Fdevloop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pasunboneleve%2Fdevloop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pasunboneleve%2Fdevloop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pasunboneleve","download_url":"https://codeload.github.com/pasunboneleve/devloop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pasunboneleve%2Fdevloop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31867716,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T15:24:51.572Z","status":"online","status_checked_at":"2026-04-16T02:00:06.042Z","response_time":69,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cheap-to-change","composability","developer-experience","fast-feedback-cycle","inner-loop","local-development","multi-process","platform-engineering","system-design","unix-philosophy","workflow"],"created_at":"2026-04-16T02:04:09.049Z","updated_at":"2026-04-16T02:04:10.783Z","avatar_url":"https://github.com/pasunboneleve.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# devloop\n\n[![Linux CI](https://github.com/pasunboneleve/devloop/actions/workflows/linux-ci.yml/badge.svg)](https://github.com/pasunboneleve/devloop/actions/workflows/linux-ci.yml)\n[![macOS CI](https://github.com/pasunboneleve/devloop/actions/workflows/macos-ci.yml/badge.svg)](https://github.com/pasunboneleve/devloop/actions/workflows/macos-ci.yml)\n\n**Keep the local loop cheap.**\n\n`devloop` is a config-driven tool for running multi-process systems\nlocally.\n\nMost local setups become expensive to change:\n\n- restarting everything\n- losing state\n- waiting for rebuilds\n- coordinating multiple services\n\n`devloop` keeps everything alive so you can change one thing at a\ntime.\n\n\u003cbr\u003e\n\n\u003cp align=\"center\" style=\"margin: 0.35rem 0 0.35rem 0;\"\u003e\n  \u003ca href=\"https://patents.google.com/patent/US1948860A\"\n  target=\"_blank\"\n  rel=\"noopener noreferrer\"\u003e\n    \u003cimg\n        src=\"docs/images/us1948860a-page1-drawing-mid-yellow-card.png\"\n        alt=\"Ball bearing patent drawing\"\n        style=\"width:58.5%;\"\n        /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\" style=\"margin: 0 0 1.25rem 0;\"\u003e\n    \u003csub\u003eMotion is constrained. Parts keep moving.\u003c/sub\u003e\n\u003c/p\u003e\n\n## A concrete example\n\nWorking on a blog post with a public preview:\n\n- change Rust -\u003e the server rebuilds and restarts\n- the browser reconnects and reloads\n- `cloudflared` restarts -\u003e new public URL\n- the current page path is combined with the new tunnel URL\n- the final URL is printed, ready to paste into LinkedIn validator\n\nOne change -\u003e everything updates -\u003e copy and paste once.\n\nWithout `devloop`:\n\n- restarting the server does not automatically coordinate the rest of the loop\n- you still need to manage CSS rebuilds separately\n- the tunnel keeps the old URL unless you restart `cloudflared` yourself\n- you rebuild the full public URL by hand every time\n\nThe pieces exist.\\\nThey just don’t know about each other.\n\n## Install\n\nInstall the latest published `main` branch directly from GitHub:\n\n```bash\ncargo install --git https://github.com/pasunboneleve/devloop.git\n```\n\nTagged releases are also published automatically on GitHub with\nprebuilt release archives for Linux x86_64 and macOS Apple Silicon.\nEach supported platform publishes its release asset independently, so a\nfailure on one platform does not block the other asset from being\nattached to the GitHub release.\n\nSupported prebuilt release targets:\n\n- `x86_64-unknown-linux-gnu`\n- `aarch64-apple-darwin`\n\nFor local development from a checkout:\n\n```bash\ncargo install --path .\n```\n\n## Usage\n\nRun `devloop` in a repository with a `devloop.toml` config:\n\n```bash\ndevloop run\n```\nThe tool will:\n* start declared processes\n* watch configured paths\n* execute workflows on change\n\nBuilt-in reference docs are also available from the CLI:\n\n```bash\ndevloop docs config\ndevloop docs behavior\ndevloop docs development\ndevloop docs security\n```\n\n## Design\n\nThe tool has three layers:\n\n1. Engine\n   Watches files, supervises processes, executes workflows, and stores\n   session state.\n\n2. Repository config\n   Declares watch groups, named processes, workflow steps, and hook\n   commands.\n\n3. Repository hooks\n   Small commands that answer project-specific questions such as \"what is\n   the current post slug?\" or \"what public URL should be printed now?\"\n\nInternally, `devloop` is being refactored toward a pure core plus an\nimperative shell: workflow orchestration is planned as explicit\nstate/effect transitions, runtime scheduling and process-supervision\ndecisions are planned the same way, and replaceable adapters interpret\nthose effects at the edges.\n\nThe session state file is owned by `devloop` while it is running.\nExternal edits to that file are not merged back into the live session;\nrestart the supervisor if you need to seed a different initial state.\n\n## Example use case\n\nUsed as the primary local development workflow for\n[`gcp-rust-blog-public`](https://github.com/pasunboneleve/gcp-rust-blog-public).\n\nThe generic example config lives at:\n\n[`examples/blog/devloop.toml`](examples/blog/devloop.toml)\n\nThe real client config lives in the client repository itself:\n\n[`gcp-rust-blog-public/devloop.toml`](https://github.com/pasunboneleve/gcp-rust-blog-public/blob/main/devloop.toml)\n\nIt models a blog workflow as configuration:\n\n* `rust` changes restart the server, wait for health, refresh the\n  current post slug, restart the tunnel, and publish the current post URL\n* `content` changes refresh the current post slug, restart the tunnel,\n  and republish the current post URL\n* CSS is handled by a long-running Tailwind watch process started by the\n  startup workflow\n\nThe example expects repo-owned helper scripts:\n\n* `./scripts/build-css.sh`\n* `./scripts/current-post-slug.sh`\n\nAt the same time, the tunnel itself is described as a managed process:\n\n* `cloudflared` is started directly by the engine\n* stdout is scanned with regex rules\n* the matched tunnel URL is written into session state\n* readiness waits for the state key to be populated\n* restart policy keeps the process alive if it exits\n* inherited process output is source-labeled without wrapper scripts\n\nWhen you need to identify which managed process emitted a line in mixed\noutput, inherited process lines include the executable first and the\nconfigured process name second. The label is color-coded per process,\nand the body style is configurable:\n\n```toml\n[process.tunnel]\ncommand = [\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:18080\"]\noutput = { inherit = true, body_style = \"plain\", rules = [{ state_key = \"tunnel_url\", extract = \"url_token\" }] }\n```\n\nThat renders inherited lines with the executable and process name, for\nexample `[cloudflared tunnel] ...`, using ANSI color when stdout is a\nterminal and `NO_COLOR` is not set.\n\nFor the runtime behavior reference, see\n[`docs/behavior.md`](docs/behavior.md).\n\nFor the full configuration reference, see\n[`docs/configuration.md`](docs/configuration.md).\n\nFor local contributor workflow details, including the opt-in watch\nflake smoke test, see [`docs/development.md`](docs/development.md).\n\nFor the external-event trust model and push-versus-polling tradeoffs,\nsee [`docs/security.md`](docs/security.md).\n\nThe client config can then compose derived values with `write_state`\nsteps, for example:\n\n```toml\nstep = { action = \"write_state\", key = \"current_post_url\", value = \"{{tunnel_url}}/posts/{{current_post_slug}}\"}\n```\n\nWorkflows can also emit rendered log lines:\n\n```toml\nstep = { action = \"log\", message = \"current post url: {{current_post_url}}\"}\n```\n\nFor high-visibility output in a mixed process log, use the boxed style:\n\n```toml\nstep = { action = \"log\", message = \"current post url: {{current_post_url}}\", style = \"boxed\"}\n```\n\nRepeated setup can be factored into helper workflows and reused with\n`run_workflow`, for example a `publish_post_url` workflow that waits for\nthe tunnel and then writes the derived URL.\n\nDownstream orchestration should usually be declared with workflow\n`triggers`, so users can read directly what a successful workflow\ncauses next:\n\n```toml\n[workflow.css]\nsteps = [\n  { action = \"run_hook\", hook = \"build_css\" },\n]\ntriggers = [\"browser_reload\"]\n\n[workflow.browser_reload]\nsteps = [\n  { action = \"notify_reload\" },\n]\n```\n\nWorkflows can also trigger a generic browser refresh after successful\nrebuild/restart steps:\n\n```toml\nstep = { action = \"notify_reload\" }\n```\n\nIf any workflow uses `notify_reload`, `devloop` starts a localhost SSE\nendpoint and exposes its URL to child processes as\n`DEVLOOP_BROWSER_EVENTS_URL` so client repositories can attach a tiny\nbrowser-side `EventSource` listener.\n\nTriggered workflows are deduplicated within one execution. If two\ntrigger paths both reach the same workflow, `devloop` runs it once from\nthe first path that reaches it. Config validation also rejects graphs\nwhere a direct trigger target is separately reachable through\n`run_workflow`, because that would make ordering ambiguous.\n\nHooks can also be observed on the runtime tick when external state\nchanges are not represented by file edits. For example:\n\n```toml\n[hook.current_post_slug]\ncommand = [\"./scripts/current-post-slug.sh\"]\ncapture = \"text\"\nstate_key = \"current_post_slug\"\nobserve = { workflow = \"publish_post_url\", interval_ms = 1000 }\n```\n\nThat lets a helper hook refresh session state from something like a\ndevelopment server endpoint, and rerun the follow-up workflow only when\nthe state actually changes.\n\nFor more precise local event flows, `devloop` can also accept\ncapability-scoped pushed events over a localhost HTTP server. A trusted\nclient process can post a value to a configured event, `devloop`\nupdates the mapped session-state key, and then runs the mapped workflow\nif the value changed. This is the preferred model for things like\nbrowser-path updates, while observed hooks remain a simpler fallback.\n\n---\n\n## Known gap\n\nReal working configs should live in the client repository, not under\n`devloop/examples/`. The example here is intentionally generic.\n\n---\n\n## Development\n\nQuality gates:\n\n```bash\ncargo fmt\ncargo test\ncargo clippy --all-targets --all-features -- -D warnings\n```\n\nGit hook setup:\n\n```bash\ngit config core.hooksPath .githooks\n```\n\nThat enables the versioned pre-commit hook in [`.githooks/pre-commit`](.githooks/pre-commit),\nwhich runs `cargo fmt` before each commit.\n\nTask tracking:\n\n```bash\nbd ready\nbd show \u003cissue\u003e\nbd update \u003cissue\u003e --status in_progress\nbd close \u003cissue\u003e\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpasunboneleve%2Fdevloop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpasunboneleve%2Fdevloop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpasunboneleve%2Fdevloop/lists"}