forkctl
Explicit, audited lifecycle control for downstream forks carried as StGit patch stacks.
Forkctl is a Rust policy CLI over real git and stg commands. The operator declares patch intent; forkctl owns staged capture, targeted refresh, generated exports/ledger/manifest, recovery evidence, exact-lease publication, and typed CLI/API output.
Clap and the local JSON API execute the same typed handlers. Domain modules never print, detect terminals, or construct tables. A centralized Anstyle/Comfy Table renderer owns pretty output and colored width-aware help; Serde/Schemars own the versioned JSON contract and JSON Schema 2020-12.
Consumer setup
= "2026.7.7"
[]
= true
= true
[]
= "patches/fork.json"
[]
= [
"git::https://github.com/victor-software-house/forkctl.git//tasks/fork?ref=<immutable-ref>",
"mise-tasks",
]
The remote catalog exposes one mounted task with the full forkctl grammar:
The task uses dir = "{{cwd}}", exact task-local tools, exec forkctl "$@", and a Usage spec generated directly from Clap. task_config.includes replaces mise's default task directories, so retain mise-tasks only when the repository has local file tasks.
Patch workflow
# edit normally
patch create records metadata-only active intent. patch refresh captures the index by default, targets the correct StGit patch, runs the consumer pre-commit hook, regenerates deterministic evidence, refreshes bookkeeping, and leaves the patch active for more edits. patch finish requires no remaining changes, runs the full check, and clears active state.
Explicit alternatives:
Persistent ownership uses scope globs (* stays within a segment; ** crosses directories). One-shot capture uses Git pathspecs. Forkctl never guesses intent.
Declarative contracts may be added after their files exist:
Without --clear, entries append uniquely. --clear explicitly replaces the complete contract set after validating all supplied required text.
Check, rebase, and publish
Rebase creates an immutable annotated recovery tag, captures the remote lease and old ordered stack, delegates to stg rebase --merged, generates a Git-private range-diff report, and records dropped patches in operation-level recovery-bound history. It never publishes.
operation status, continue, and abort expose the typed in-flight journal. operation abort -n reports the restoration plan; operation abort -y restores and checks old state before clearing the journal.
Publish performs one atomic explicit-ref push of branch plus recovery tag with an exact lease. There is no force, lease, or atomic fallback and no provider ruleset administration.
Hooks
Forkctl exposes ordinary read-only checks and does not own a hook manager:
pre-commit:
commands:
forkctl-staged:
run: mise run fork check -s
pre-push:
commands:
forkctl-check:
run: mise run fork check -q
Checks never stage or rewrite. Forkctl's production process layer clears Git repository-local hook variables before nested/foreign repository commands while preserving transport and authentication variables.
CLI, help, and completion
Most long options have collision-audited mnemonic shorts. Leaf parameters are grouped by subject, metadata/scope, capture, execution, and output. Help is generated from Clap metadata into colored width-aware panels; no second parameter specification exists.
Completion supports bash, elvish, fish, Nushell, PowerShell, and zsh, including commands, flags, enum values, files, local Git remotes/refs, live patch names, and current operation values. Candidate lookup is local and fail-silent.
Local JSON API
|
Requests use dotted command names such as patch.refresh and operation.abort, command-specific typed arguments, and mode: execute|plan. Success, plan, notice, error, and error-detail types are schema-derived. JSON stdout is exactly one response and stderr is empty.
Schema kinds: bundle, manifest, invocation, response, active-state, operation.
Manifest
Every patch commit carries matching Downstream-Reason, Upstream-Status, and Drop-When trailers. Source patches precede tooling patches. Every source export is generated deterministically as <exports>/<order>-<name>.patch; tooling patches have no export. The final tooling patch owns manifest, ledger, exports, and integration files.
Bootstrap and clone hydration
Without a manifest, init requires explicit repository/base/document/bookkeeping arguments and HEAD exactly at the resolved base. Repeatable --allow-base GLOB and --required-text PATH=TEXT options initialize declarative contracts without manual manifest edits. It creates the initial bookkeeping patch and never imports legacy commits.
With a manifest, init idempotently reconstructs StGit metadata and fetches only exact recovery refs named by history before running the full check.
Direct installation
Provide supported git and stg executables on PATH; the mounted mise task provisions exact versions automatically.
Development
[workspace.package].version is the sole forkctl release source. Root mise.toml is the sole source for the minimum mise, Rust, StGit, Lefthook, Usage, and GitHub CLI versions. After changing either source, run mise run version:sync; it regenerates mise.lock and every operational pin. The verification gate rejects drift and runs rustfmt, denied-warning workspace Clippy, API/schema/help/completion tests, and disposable real Git/StGit lifecycle tests.