forkctl 0.0.6

Control audited StGit downstream patch stacks
forkctl-0.0.6 is not a library.

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

min_version = "2026.7.7"

[settings]
experimental = true
lockfile = true

[env]
FORK_MANIFEST = "patches/fork.json"

[task_config]
includes = [
  "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:

mise run fork --help
mise run fork status
mise run fork check
mise run fork patch refresh

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

mise run fork patch create downstream-change \
  -k source \
  -p 'Describe why this downstream change exists.' \
  -u not-submitted \
  -d 'Upstream provides the required behavior.' \
  -s 'src/**' -s 'tests/**'

# edit normally
git add src/example.rs tests/example.rs
mise run fork check -s
mise run fork patch refresh
mise run fork patch finish

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:

mise run fork patch refresh --all
mise run fork patch refresh -p src/example.rs -p tests/example.rs
mise run fork patch refresh -n             # semantic dry-run plan

Persistent ownership uses scope globs (* stays within a segment; ** crosses directories). One-shot capture uses Git pathspecs. Forkctl never guesses intent.

Check, rebase, and publish

mise run fork status
mise run fork check                         # full clean-repository audit
mise run fork check -s                      # staged index against active patch
mise run fork rebase -o refs/heads/main
mise run fork operation status
mise run fork operation continue
mise run fork 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

forkctl patch refresh --help
forkctl completion zsh
forkctl completion nu
forkctl --usage-spec=fork

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

forkctl status --format json
forkctl api schema --kind bundle
printf '%s' '{"protocol_version":1,"mode":"execute","request":{"command":"check","arguments":{"scope":"repository"}}}' \
  | forkctl api call

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

{
  "schema": 1,
  "downstream": {
    "remote": "origin",
    "branch": "main",
    "recovery_tag_prefix": "forkctl/recovery"
  },
  "upstream": {
    "remote": "upstream",
    "url": "https://github.com/example/project.git",
    "fetch_ref": "refs/heads/main"
  },
  "base": {
    "target": {
      "kind": "branch",
      "selector": "refs/heads/main",
      "commit": "0000000000000000000000000000000000000000"
    },
    "canonical": "0000000000000000000000000000000000000000",
    "stack": "0000000000000000000000000000000000000000"
  },
  "documents": {
    "ledger": "PATCHES.md",
    "exports": "patches/downstream"
  },
  "bookkeeping_patch": "fork-tooling",
  "patches": [
    {
      "name": "downstream-change",
      "kind": "source",
      "purpose": "Describe why this downstream change exists.",
      "upstream_status": "not-submitted",
      "drop_when": "Upstream provides the required behavior.",
      "scope": ["src/**", "tests/**"]
    }
  ],
  "history": [],
  "contracts": {
    "allow_base": [],
    "required_text": []
  }
}

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. 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

cargo install forkctl --locked

Provide supported git and stg executables on PATH; the mounted mise task provisions exact versions automatically.

Development

mise install --locked
mise exec -- lefthook install
mise run verify
mise run build

[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.