Thanks for your interest! DevFlow is an opinionated AI-driven development workflow automation CLI written in Rust — it bakes in one developer's specific take on branching, gating, and verification rather than trying to be a universal platform.
git clone https://github.com/denniyahh/devflow.git
cd devflow
cargo build
cargo testA tracked hook at scripts/hooks/pre-push runs the
same fmt/clippy/test checks CI requires, before the push leaves your machine:
git config core.hooksPath scripts/hooksTwo things this hook does that are load-bearing:
- It clears git's repository-local
GIT_*variables before running the tests. Git exportsGIT_DIRinto hooks when you push from a linked worktree, andGIT_DIRoutranks a process's working directory when git decides which repository to act on — so without the scrub, every test fixture that shells out to git in a tempdir retargets your real checkout instead. This is not hypothetical: it once flipped this repository tocore.bare=true, moved a worktree's HEAD onto a fixture branch and rewrote the committer identity. scripts/hooks/pre-commitdelegates to whatever pre-commit hook you already had.core.hooksPathreplaces the hooks directory wholesale, so without that shim the command above would silently switch off a global secret scanner. It is a no-op if you have no such hook.pre-commitruns its own secret scan,gitleaks git --pre-commit --staged --redact, on every commit, so install gitleaks before settingcore.hooksPath: without it onPATHthe hook refuses the commit rather than skip the scan. A finding refuses the commit with the secret redacted; a false positive goes in.gitleaksignoreby fingerprint.scripts/hooks/commit-msgenforces Conventional Commit formats (feat:,fix:,chore:, etc.) on every commit subject and warns if the subject exceeds 72 characters, keeping git history and automated changelog generation clean. It also rejects a subject ending in a period.pre-commitalso refuses acrates/**commit made outside the active phase's worktree (scripts/lint-phase-worktree.sh; setDEVFLOW_ALLOW_OFF_WORKTREE=1when that is intended). It does nothing without a.planning/STATE.md.scripts/phase-worktree.sh <N>creates the worktree from your workspace branch.pre-pushalso runsscripts/check-workspace-divergence.shin a checkout that carries.workspace-divergence(Workflow B): every difference fromdevelopmust be declared there, and every hook and top-level script must be committed executable.pre-commitandpre-pusheach run any executables inscripts/hooks/pre-commit.d/andscripts/hooks/pre-push.d/, in name order, with the hook's arguments; the first failure fails the hook.developships none. A personal workspace branch adds its own checks there as new files instead of editing the shared hooks (see Workflow B). Apre-pushdrop-in gets no stdin, because the hook has already read the ref list.
Git cannot version .git/config, so settings that must survive a fresh clone
live in the tracked .gitconfig and are pulled in once:
git config --local include.path ../.gitconfig(the path is relative to .git/config). This turns on SSH-format signing for
both commits and tags, and configures the git-flow branch model. It
deliberately does not set user.signingkey — that is per-contributor and
this repository is public.
Because the model is supplied by that include, do not run git flow init -d:
its default production branch is master and this repository's is main, so
the defaults would misconfigure git-flow against a branch that does not exist,
surfacing only later at release or hotfix time. With the include in place,
git flow feature start <name> works directly.
Releases and main are restricted to the maintainer's key, enforced by
scripts/hooks/pre-push.
This matters here more than in most repositories because DevFlow is developed with an AI agent that commits under its own signing key. Two keys are therefore in play, and they are not interchangeable:
| Config | Signs | Whose key |
|---|---|---|
user.signingkey |
ordinary commits | the agent's, on a machine where it works |
devflow.releaseSigningKey |
release tags, main |
the maintainer's — the only key permitted |
Both are configured with the same user.email, so a tag signed with the wrong
key looks correct everywhere a human checks: git log, git tag -v's signer
line, and GitHub's "Verified" badge all show the maintainer. Only the key
fingerprint differs, which is why the hook compares fingerprints.
If you cut releases, set your key once:
git config --local devflow.releaseSigningKey ~/.ssh/<your-key>.pubThen tag with it explicitly, since user.signingkey may point elsewhere:
git -c user.signingkey="$(git config --get devflow.releaseSigningKey)" \
tag -s vX.Y.Z -m "vX.Y.Z"Leaving devflow.releaseSigningKey unset disables the check entirely, so a
contributor who never cuts a release needs no configuration. Once set there is
no override flag — an override is what a mistaken release would reach for. To
push a rejected tag, re-sign it with the right key.
If you use distrobox, you can create an isolated environment:
distrobox create --name devflow-dev --image fedora:41
distrobox enter devflow-dev
# install Rust, build, test as aboveThe repo includes a .devcontainer/devcontainer.json
with the rust-toolchain.toml-pinned stable toolchain (clippy + rustfmt) preinstalled and
cargo registry/target caches persisted across rebuilds. Open the repo in VS Code and choose
"Reopen in Container", or run devcontainer up --workspace-folder . (via the
Dev Containers CLI) to get a reproducible build/test
environment without installing Rust locally.
# Build
cargo build
# Run all tests
cargo test
# Lint (must include --all-targets, or test code goes unlinted)
cargo clippy --workspace --all-targets -- -D warnings
# Format check
cargo fmt --check
# Run a specific command
cargo run -- statusThe git-flow tests create throwaway fixture repositories. If you sign commits or tags globally, disable signing for these fixtures so the tests don't block on a GPG prompt. The test harness sets this per-fixture, but if you run any manual git steps against a fixture, use:
git config commit.gpgsign false
git config tag.gpgsign falseSee § AI Change Acceptance for what a test must demonstrate to be accepted, and which test shapes are rejected outright.
DevFlow uses a Dual-Tier Workspace Architecture to keep shared branches clean, contributor-friendly, and reproducible, while allowing maintainers and AI agent operators to version-control their personal workflows, prompts, and planning databases.
gitGraph
commit id: "v2.9.0"
branch develop
checkout develop
commit id: "feature-code"
branch "workspace/contributor"
checkout "workspace/contributor"
commit id: "agent-prompts-and-gsd"
checkout develop
commit id: "upstream-fix"
checkout "workspace/contributor"
merge develop id: "sync-code"
The default shared branches contain only source code, tests, documentation, and the baseline development container. They are kept strictly free of personal AI agent configs, prompts, local SQLite planning databases, and machine-specific MCP tool definitions.
- Pre-commit & Pre-push guards (enforced):
scripts/hooks/pre-commitandscripts/hooks/pre-pushactively refuse commits and pushes that contain personal artifacts (.agents/,.codex/,.claude/,.planning/,.gsd/,.mcp.json,CLAUDE.md,skills/) on any non-workspace branch.
If you use AI agent harnesses (Claude Code, Codex, Antigravity, Hermes), custom MCP servers, or local workflow planners (GSD), keep them versioned on a personal workspace branch:
- Name your branch after your GitHub handle (e.g.
workspace/<handle>orpersonal/<handle>). - You can commit your agent definitions, custom prompts, skills, and planning dossiers to your workspace branch and push it to GitHub so your environment is accessible anywhere.
- Fork and clone the repository.
- Cut a feature branch from
develop:git checkout -b feature/my-feature develop # or with tracked alias: git feature-start my-feature - Implement changes, add regression tests, and run
cargo test. - Submit a PR against
develop.
-
Keep your primary orchestrator/agent checkout on your personal workspace branch (
workspace/<handle>). Ensure it is up to date:git checkout workspace/<handle> ./scripts/sync-workspace.sh
-
Fork your feature worktree directly from
workspace/<handle>usingworkspace/<feature>(e.g.workspace/my-featureorworkspace/<handle>-my-feature) so agent harnesses,.planning/, and skills remain available without triggering hook rejections (avoid nested slashes likeworkspace/<handle>/<feature>due to Git ref D/F conflicts with the base branch):git worktree add .worktrees/my-feature -b workspace/my-feature workspace/<handle> cd .worktrees/my-feature
-
Implement changes, run agent planning and verification, and commit code and planning artifacts freely.
-
When ready to submit upstream, extract a clean, review-ready PR branch:
./scripts/cut-pr-branch.sh # Cuts a pristine feature/my-feature branch off origin/develop, # cherry-picks code commits, and strips personal/planning artifacts.
On a DevFlow phase branch (
feature/phase-N) it runs in phase mode: it replays the commits scoped to that phase (feat(N-07): ...), wherever they were committed, ontofeature/phase-N-pr. It ends with a fidelity report; review any path it lists as differing from your branch. -
Test, push, and submit the clean PR:
git checkout feature/my-feature ./scripts/check-in-container.sh git push -u origin feature/my-feature gh pr create --base develop --head feature/my-feature
-
After your PR is merged upstream:
# In your base checkout: git checkout workspace/<handle> # 1. Archive planning notes and verification records from the feature git merge workspace/my-feature -m "chore: archive my-feature planning records" # 2. Sync latest code from develop ./scripts/sync-workspace.sh # 3. Clean up the feature worktree and branch git worktree remove .worktrees/my-feature git branch -d workspace/my-feature git push origin workspace/<handle>
Keeping the two tiers apart. The workspace branch adds files; it does not edit files that
developalso carries, except the ones it declares. Declare workspace-only paths and deliberately modified shared files in a committed, workspace-only.workspace-divergencefile, one per line (only <path>ormodified <path>).cut-pr-branch.shnever replays anonlypath. Put workspace-only hook checks inscripts/hooks/pre-commit.d/orscripts/hooks/pre-push.d/rather than editing the shared hooks. A per-checkoutdevflow.toml(for examplebase_branch) is ignored ondevelop; a workspace branch that tracks.planning/may track its own.
crates/
├── devflow-core/ ← Library crate: state machine, config, git, versioning, agents
└── devflow-cli/ ← Binary crate: clap CLI wrapper
- Rust edition 2024
- All public items must be documented
- Error handling via
thiserror - No
unwrap()in library code — useResult - Structured output from core, formatting in CLI
- Fork the repo
- Create a feature branch:
git checkout -b feature/my-feature develop - Write code, add tests
- Ensure
cargo testpasses andcargo clippy --workspace --all-targets -- -D warningsis clean cargo fmt- Submit a PR against
develop - CI runs tests + clippy + format check
Required checks — a PR must pass all four CI jobs before it can merge
(mirrors .github/workflows/ci.yml and .github/workflows/devcontainer.yml):
cargo testcargo clippy --workspace --all-targets -- -D warningscargo fmt --checkBuild + test in devcontainer
devflow test runs these checks locally, and
scripts/hooks/pre-push runs them before a push.
The --all-targets scope is load-bearing: the narrower cargo clippy -- -D warnings does not compile test targets, so lints inside #[cfg(test)]
modules pass it and then fail CI. Regression guards for this live in
crates/devflow-cli/tests/devcontainer_ci_failfast.rs.
Ordinary code contributions need no agent credentials or API keys — the build
and the full test suite run offline. Agent CLIs (Claude, Codex, OpenCode) are
only needed to exercise devflow start against a live agent, not to build,
test, or pass CI.
Effectively every change to this repository is authored or substantially modified by an AI agent — that is the default, not a special case — so the acceptance bar for a change is written down here rather than assumed.
A change is accepted only when it demonstrates all five of the following:
- A regression test that fails before the change.
- At least one assertion at a public or otherwise stable boundary, not only a private helper.
- Evidence the test fails for the intended reason — not a setup error, a compile failure, or an unrelated panic.
- Full affected-package tests,
cargo clippy --workspace --all-targets -- -D warnings, andcargo fmt --check, all clean. - Independent review of both the implementation and the test signal — a correct-looking implementation reviewed alone is exactly how a test that cannot fail gets through.
A test is rejected outright, regardless of whether it currently passes, if it does any of the following:
- Asserts only constants, never an implementation's actual output.
- Reproduces the production algorithm inside the test body, so the test and the code fail together and pass together.
- Compares a function call with itself.
- Greps implementation text as a substitute for a runtime contract that could have been asserted directly.
Enforcement lives at the existing review-before-Ship gate:
/gsd-code-review runs before Ship and refuses to ship on Critical findings
— this contract is applied there, not aspirational.
The single source of truth for this contract is
.claude/skills/ai-change-acceptance/,
which spells out the check a reviewer performs and the failure signature for
each requirement and rejection pattern above, plus worked examples drawn from
this repository's own history. If this section and the skill ever disagree,
the skill wins — this section is prose for a human contributor, not a
second copy of the contract.
Both develop and main are protected branches — direct pushes are
rejected ("Changes must be made through a pull request", plus required status
checks) even for the maintainer. Every step below that changes a branch goes
through a PR.
Commit guard (enforced, not just documented):
scripts/hooks/pre-commitrefuses to commit directly ondevelopormain— both are PR-protected, so such a commit would be rejected at push time anyway. There is no override; the fix isgit switch -c <branch>first. This makes step 2's "put the work on a branch" mandatory at the commit level rather than a convention an inattentive release cut can skip.
-
Bump the version in two places in the root
Cargo.toml:versionunder[workspace.package], anddevflow-core'sversionunder[workspace.dependencies]. Bumping only the first is the easy miss;crates/devflow-cli/tests/workspace_version_pin.rsguards the pair, but only after the fact. Thencargo buildto syncCargo.lock, and add a new top## X.Y.Zsection toCHANGELOG.md. -
Since
developis protected, put step 1 (and any work being released) on a branch and open a PR intodevelop. Merge it once CI is green. -
Open a PR from
developintomaintitledrelease: vX.Y.Z — <short description>. -
Once CI is green, squash-merge it (this repo's branch settings only allow squash merges into
main— real merge commits are disabled). -
Tag the resulting commit on
mainwith the maintainer key, using the same explicit key-selection form documented in § Release signing — never a baregit tag -s, which signs with whateveruser.signingkeyhappens to be on the machine running it (the agent's, on a machine where the agent works):git -c user.signingkey="$(git config --get devflow.releaseSigningKey)" \ tag -s vX.Y.Z <commit> -m "vX.Y.Z" git push origin vX.Y.Z
Use
-swith the explicit key selection, not-a: signing is already on by repository policy (.gitconfig's[tag] gpgsign = true), so the risk here is not an unsigned tag but a tag signed with the wrong key — one that looks correct ingit log,git tag -v's signer line, and GitHub's "Verified" badge, because both keys share the sameuser.email. Only the key fingerprint differs, which is whyscripts/hooks/pre-pushcompares fingerprints rather than trusting the signer string. Verify withgit tag -v vX.Y.Z. -
Immediately run
scripts/sync-main-to-develop.shfrom a cleandevelopcheckout. It produces a merge commit locally; becausedevelopis protected you cannot push it directly — put it on async/branch and open a PR intodevelop.The sync PR must be merged with a merge commit, NOT squashed. Squashing collapses the two parents into one and discards the ancestry link, which is the entire point of the step.
Do not use auto-merge on this PR. It defaults to squash, which is exactly how the v2.0.0 sync failed on 2026-07-27 — the PR merged, the link was destroyed, and a second PR was needed to repair it. Use the "Create a merge commit" button explicitly. This is not a repository restriction to work around:
allow_merge_commitistrueand thedevelopruleset permits["merge","squash"]; auto-merge simply does not pick the one this step requires.Confirm the step actually worked — a squashed sync looks successful:
git merge-base --is-ancestor origin/main origin/develop && echo OK
If that prints nothing, the link was not created and the sync must be redone.
-
Create a GitHub Release for the tag (convention since v1.7.0, and how the CHANGELOG section reaches users who don't read the repo): run
scripts/cut-release.sh github-release.
Step 6 is not optional. Because main only accepts squash merges, its new
release commit has no parent relationship back to develop — skip this
step and the next release PR will conflict against a stale merge-base
(this happened going into v1.5.0: main and develop had silently diverged
since v1.4.0, producing conflicts across 11 files including core Rust
source). The script performs a content-preserving -X ours merge — it
verifies the resulting tree is byte-identical to develop's before allowing
itself to proceed — so it only ever links history, never changes content.
Confirm it worked with git merge-base --is-ancestor origin/main origin/develop.
To publish to crates.io after tagging: cargo publish -p devflow-core
first, then cargo publish -p devflow. This ordering is a hard
requirement, not a convention — devflow's manifest depends on
devflow-core by version rather than by path once packaged, and as of
v1.8.1 devflow-cli also carries a dev-dependency on it for the
test-support feature. Publishing out of order fails to build. cargo publish waits for the registry to make the crate available before
returning, so the second command can follow immediately.
To deploy updated documentation to GitHub Pages after release: run
scripts/cut-release.sh docs (which runs scripts/deploy-docs.sh
to build the autwicky-scaffolded MkDocs wiki and push to gh-pages).
DevFlow strictly enforces Conventional Commits:
type(scope): description, imperative mood, lowercase description, no period at the end.
- Allowed types:
feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert,release,merge,sync. - Breaking changes: Add
!before the colon, e.g.feat(api)!: change default timeout. - Enforcement (enforced locally via hook):
scripts/hooks/commit-msgautomatically validates every commit message on commit creation whengit config core.hooksPath scripts/hooksis configured. - Automated SemVer & Changelog: Commits authored with Conventional Commits allow
scripts/cut-release.shand DevFlow's version engine to automatically calculate the next SemVer bump and draft release notes.
DevFlow uses the tracing crate for structured
diagnostic logging. All log output goes to stderr; stdout is reserved for
agent/system output.
use tracing::{info, debug, warn, error};
// State transitions and milestones
info!(before = %old_step, after = %new_step, phase = phase, "step_entered");
// I/O and detail operations
debug!(path = %path, "saved state to disk");
// Recoverable anomalies
warn!("force-pushing branch {branch}");
// Fatal conditions
error!("failed to load config: {err}");State transitions in workflow.rs should emit paired step_entered /
step_exited events at INFO level with (before, after, phase) fields:
info!(before = %current, after = %next, phase = state.phase, "step_entered");| Variable | Purpose |
|---|---|
RUST_LOG |
Controls verbosity. Set to error, warn, info, debug, or trace. Use targeted directives like devflow_core=debug,devflow=info to filter by crate. |
DEVFLOW_LOG_FORMAT |
Set to json for machine-readable JSON output (one JSON object per line on stderr). |
- Do use
tracingmacros (info!,debug!,warn!,error!) — neverprintln!oreprintln!for diagnostic output. - Do log to stderr; reserve stdout for structured results and agent output.
- Do use structured fields (
field = value) instead of string interpolation for machine-parseable log entries. - Do add
#[tracing::instrument]to key state-machine functions so call chains appear in log output. - Don't log secrets, tokens, or API keys.
See ARCHITECTURE.md for design documentation.
DevFlow supports four agents today (Claude Code, Codex, OpenCode, Pi) through the
modular AgentDriver contract; agent-specific code lives only under
crates/devflow-core/src/agents/. Adding a backend today is
a short checklist — keep these in sync or tests/builds fail:
- Add a driver file in
crates/devflow-core/src/agents/implementing theAgentDrivertrait - Add a variant to the
AgentKindenum instate.rs - Update the
FromStrparser,Display, andAgentParseErrortext instate.rs - Add a match arm in
agents::driver_for() - Add the
pub mod/pub useexports inagents/mod.rs - Extend tests (driver name, parser aliases, prompt-sharing)
- Update docs (README, this file, ARCHITECTURE.md, DEPENDENCIES.md)
See ARCHITECTURE.md for the authoritative version of this checklist.
Open an issue or start a discussion.