shepherd-cli 6.8.0

Small installed-plugin diagnostics for the Shepherd workflow
# shepherd-cli

`shepherd init --root PROJECT` creates the focused `.shepherd/runs/` workspace.
`shepherd lane --root PROJECT --run RUN --name LANE` adds a no-clobber lane plan,
ledger, and evidence directory. These commands do not select topology or create
a registry. Existing edited records are preserved and initialization refuses to
replace them; resume reads those records instead of calling `lane` again. The
scaffold separates the filesystem lane ID from the full branch ref, absolute
worktree/common directory, owner, base, integration target, accepted plan revision,
allowed paths, tested candidate, and evidence custody. Blank fields are not verified facts.

`shepherd lane-check --root PROJECT --worktree ABS_WORKTREE
--common-dir ABS_COMMON_DIR --branch refs/heads/lane/NAME.INDEX
--base-ref refs/heads/v6.7.7 --base-sha FULL_SHA [--head FULL_SHA]
[--require-clean]` compares native Git against an explicit recorded identity.
It inspects the explicit absolute `--worktree`, which must be its Git top-level.
`--root` supplies project context and may be another checkout or a subdirectory
in the same repository; a different Git common directory fails explicitly.
The command compares canonical absolute worktree/common-directory paths and the full branch,
checks that the qualified base ref still resolves to the expected SHA and is an
ancestor of HEAD, and binds the branch's non-prunable registration to this
canonical path while detecting a branch checked out in multiple worktrees.
`--head` additionally pins the candidate. Use qualified base refs even if a branch
and tag have the same name. A relocated worktree requires a freshly verified mapping.

Successfully parsed invocations emit JSON, schema `shepherd.lane-check.v1`, with `ok`, `worktree`,
`common_dir`, `branch`, `head`, `base_ref`, `base_sha`, `dirty`, `untracked`,
`findings`, and `ownership: "not_assessed"`. Unavailable observations are null.
Exit 0 means the requested Git checks passed, exit 1 means a mismatch or inspection
failure. Exit 2 means invalid CLI usage: Clap writes human-readable stderr and
does not emit a JSON report. Dirty work includes every initialized recursive
submodule, even when an ancestor's submodule configuration sets `ignore=all`;
`untracked` identifies superproject untracked entries, while submodule untracked
files also make `dirty` true. Nested `status.showUntrackedFiles=no` is overridden.
The command compares each indexed regular file's filtered content hash and each
indexed symlink target with its index blob, in the root and every initialized
submodule. Same-size edits with restored mtimes are detected even under
`core.trustctime=false` or `core.checkStat=minimal`. Native status also checks
staged, mode, and untracked changes. Git-ignored files remain excluded.
Work is preserved; `--require-clean` makes dirty work a failing condition.
Index `assume-unchanged` or `skip-worktree` entries can hide tracked edits.
The command declines inspection when these flags occur in the root or an
initialized recursive submodule, including unchanged flagged files; cleanliness
is unknown rather than certified. It does not clear flags, repair an index or
discard files. Sparse checkouts using those flags need another verified custody
route before a clean-worktree claim.
An uninitialized or deinitialized gitlink at any depth also makes cleanliness
unknown, even when its directory is empty or absent. Native Git skips such
directories during recursive inspection, so the command exits 1 with `ok:false`
and null `dirty`/`untracked` both with and without `--require-clean`. It neither
initializes submodules nor fetches their contents. Git's executable-bit check is
forced on for command-scoped inspection, including initialized submodules, so
`core.fileMode=false` cannot hide a tracked 0644-to-0755 change. Repository and
submodule configuration and file modes are left intact. Missing tracked files,
unexpected file types, and unsupported symlink targets leave cleanliness unknown
instead of certifying a clean tree. Hashing uses Git without `-w`; it does not
write new objects or refresh indexes.
Git 2.52.0's recursive submodule wrapper cannot inspect an initialized submodule
from a newline-ending root; this input also fails closed with unknown cleanliness.
Newline-ending roots without submodules remain supported by the native probes.
Inherited Git environment variables are removed case-insensitively, including
Windows caller spellings such as `Git_Dir`. Legacy graft overlays are disabled;
real trailing path whitespace is preserved. The command requires Git's
`--no-lazy-fetch` option: unsupported Git fails before inspection instead of
silently fetching. Cloud setup checks this capability directly rather than
assuming it from a version string. Missing promisor objects fail without populating the object
store; inherited stdin and credential prompts are disabled. Git 2.52.0 is tested here;
native Windows remains unverified. Non-UTF-8 Git paths or porcelain filenames
are unsupported: inspection exits 1 with `ok:false` and unavailable fields null,
including unknown cleanliness, rather than accepting a lossy path or clean tree.
The CLI requests observations without intentionally changing files, index, refs
or working trees. Use it only with operator-trusted Git, repository configuration,
attributes and configured helpers, including those in initialized submodules.
Native Git inspection is not an execution sandbox: external clean/process
filters may run during status or content hashing and can write files, disclose data or
block. The optional-lock, fsmonitor, no-fetch and prompt controls do not prevent
all configured subprocess execution. If this trust boundary is unsuitable, use
an operator-approved alternative before inspecting the repository. Do not silently
disable filters and certify cleanliness; their normalization may be needed for
a correct comparison. Git inventories are streamed completely, with tracked
content hashed in batches. Scalar control output and each individual inventory
record are limited to 1 MiB; total inventory size has no such limit. Inspection
errors name the Git operation. This is not a process deadline or atomic snapshot.
It does not establish owner identity, process liveness, permission,
or integration acceptance. Root still reconciles surviving writers and evidence.

`shepherd doctor --host HOST --plugin-root ABS_PATH` inspects an installed
Claude, Codex, or Pi Shepherd carrier. It reports the helper and plugin versions,
required resources (nine roles and 23 skills, including `using-shepherd`), the
selected host hook, and optional `jq` availability.
It does not authorize tools or prove host trust and event delivery.

`shepherd handoff --host HOST --plugin-root ABS_PATH --context JSON_PATH`
renders the installed `templates/handoff.md` from a flat JSON object. The
current template uses `task`, `objective`, `artifact_root`, `status`,
`changed_paths_or_commit`, `verification`, `review`, `evidence_location`,
`remaining_work`, and `next_action`; `branch`, `base`, `candidate`, `rollback`,
and `date` are optional. Missing referenced fields fail before output.
Optional fields set to JSON null are treated as absent by the current template;
they do not assert a supplied Git tuple, review history or activation receipt.
Default output is stdout. `--output PATH` creates one complete file atomically and
refuses to replace different existing bytes; an identical retry leaves the
file untouched. Rendering does not verify the truth of the supplied result.

The CLI has no run registry, daemon, broker, or extracted runtime dependency.

Install from the Shepherd checkout with `cargo install --path crates/cli --locked`.
Use the installed `shepherd --version` to confirm the helper version before
running doctor against the actual plugin installation path.

CI uses `scripts/ci-candidate.py` for its offline-tested event/ref policy. Allowed
relations are lane to patch or legacy/namespaced dev, matching dev to patch, and
patch to main/master. Version coordinates and dev iteration use digits 0 through
9 inclusive; lane incarnation numbers have no one-digit bound. Dispatch payloads
require `ref`, `sha`, `target`, and `target_sha`: qualified refs and exact commits.
Shorthand and raw-SHA-only dispatch are rejected. Repository dispatch acquires
policy from default-branch code; manual workflow dispatch acquires it from
`github.workflow_sha`, the immutable code revision of the authorized selected
workflow. The payload-selected candidate cannot choose its policy revision, and
dispatch preflight runs before the candidate gate. PR/push policy and workflow
code can come from the candidate, just like ordinary repository tests. These
checks assume the host's runner and permission controls; they are not an
independent security attestation against malicious candidate code.
For private repositories the workflow supplies its read-only token only to the
pre-gate policy step; native fetch uses a
transient, origin-server-scoped HTTP header, without persisting credentials or
placing them in command arguments. PR validation binds current base,
head, candidate merge commit and tree. Dispatch accepts only a proven target-ancestor
pair, so its candidate head tree is the exact fast-forward integration tree. Candidate
history is fetched to a bounded depth of 256; an unproven ancestor is rejected,
including incomplete history. Diverged candidates use an exact PR merge candidate
instead. Dispatch concurrency keys include the candidate ref/SHA and target
ref/SHA, rather than the selected workflow-policy ref.

Preflight records use `candidate_sha`, `candidate_merge_sha` and `candidate_tree`,
with `acceptance: "not_assessed"`; they do not claim that tests have already run.
The workflow no longer gives a candidate-writable post-gate helper a fresh token
or lets it issue acceptance. GitHub reports the actual job conclusion separately.
A green self-check cannot replace independent review or root's fresh head/base/
target reconciliation before integration. Moved targets or changed candidates
invalidate the applicable evidence; do not reuse stale green checks.
Metadata-only edits use a distinct check name and do not grant candidate acceptance.
Offline fixtures do not establish hosted branch-protection or ruleset settings.