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.