The sorrel CLI
A persistent, single-user local VCS over the real engine. Every command reads and
writes content-addressed objects under .sorrel/; state survives process
restarts; everything supports --json for tools and agents.
The core loop
$ sorrel init # .sorrel/: object store + manifest + HEAD
$ echo "hello" > a.txt
$ sorrel status # dirty: added a.txt (real diff vs HEAD)
$ sorrel change create -m "add a" # snapshot, diff, Change object, advance HEAD
$ echo "world" >> a.txt
$ sorrel diff # line-level unified hunks vs HEAD
$ sorrel change create -m "edit a"
$ sorrel log # snapshot DAG walk with change ids,
# authors, and messages
status and change create use the engine's stat cache, so
unchanged files are never re-hashed. log resolves each snapshot to the
Change that produced it through a small index in .sorrel/changes.index.
Lanes: parallel work without branch noise
$ sorrel lane create --name agent/feature
$ sorrel lane list # every lane, its head, the active one marked
$ sorrel lane switch <lane-id> # restores that lane's tree (refuses if dirty)
$ sorrel lane submit # create a Hub proposal for the active lane
$ sorrel stack create --name series <change-id>
$ sorrel stack show <stack-id>
Each lane has its own head under .sorrel/heads/. Two lanes advance
independently — exactly what parallel agents need to work without stepping on each
other.
Merge: fast-forward or real three-way
$ sorrel merge <lane-id>
# fast-forward when the active lane has not diverged
# otherwise: engine three-way merge (merge base + line merge)
# on conflicts:
# - conflicted files get <<<<<<< ours / ======= / >>>>>>> theirs markers
# - .sorrel/MERGE_STATE records the stored MergeResult object
# - HEAD does not move
$ sorrel merge --continue # after resolving markers, complete the merge
# or:
$ sorrel merge --abort # restore the pre-merge tree
Clean merges write a merged snapshot with both parents and record a merge Change. Conflicted merges leave inspectable state: marker-annotated files in the tree and first-class Conflict objects in the store.
Git bridge: import, export, and colocated sync
$ sorrel git import # Git commits → Sorrel snapshots
$ sorrel git export ./mirror.git # Sorrel snapshots → Git commits
$ sorrel git sync # align colocated .git/ and .sorrel/
Import and export are incremental and record SHA-to-snapshot mappings in
.sorrel/git-map.json. Colocated sync fast-forwards the side that has
not moved; true divergence is imported onto a normal git/<branch>
lane for the usual Sorrel merge flow.
Hub sync: push and pull
$ sorrel remote add origin http://127.0.0.1:3000
$ sorrel push origin # negotiate missing objects, upload, advance ref
$ sorrel pull origin # fetch refs, download closure, verify ids
Push seeds the local snapshot closure, asks the Hub which objects are missing, uploads only those, and advances the remote ref with fast-forward and closure checks. Pull verifies every downloaded object's content id before trusting it.
Workflows, policy, slices
$ sorrel workflow validate # parse + check sorrel.workflow.yml
$ sorrel workflow run <job> # execute locally, gated by Core policy
$ sorrel policy evaluate ... # Core policy decision for an action
$ sorrel grant create ... # record a real Core grant decision
$ sorrel slice create ... # slice manifest for a subproject
Secrets, environments, and run logs
$ sorrel secret list # handles only
$ sorrel secret sync # refresh secretspec.toml
$ sorrel secret check --provider dotenv:.env
$ sorrel env info # devenv or local-fallback
$ sorrel workflow run test
$ sorrel run list
$ sorrel run show <run-id>
$ sorrel run logs <run-id>
Secret resolution and injection use upstream SecretSpec only after Core grant
checks. Values are injected into the child process, never Sorrel objects, and
persisted output under .sorrel/runs/ is redacted. Full devenv task
mapping, Hub log streaming, and a hosted secret backend remain future work.