v6.1.8
v6.1.8 — 2026-06-17
Field-shape deduplication — shctx dups (#157). The third leg of the
mechanical shape-gate set (with dep-hygiene and check-impls-defs). Name-matching
dedup (index_symbols / dedup-check.sql / dedup_write_guard.sh / the conductor
DEDUP-GATE) catches a duplicate ONLY when the second definition reuses the first
one's name. It is blind to the dominant large-workspace rot: a second type for an
existing concept under a different name — the rename-to-evade-dedup shadow that
compiles green and slips every name-keyed gate (a 2026-06-17 audit of one workspace
found 22 such clusters). shctx dups closes that gap by clustering on field
shape, and gives subagents a way to recognize a pre-built struct they can reuse
without remembering its name — the match is surfaced, by shape, at authoring time.
New — shctx dups <scan|check|registry>
scan— workspace census: parses everypub struct/pub enum, fingerprints
each by its(field_name, normalized_type)set, clusters by similarity, and
reports each cluster with members (file:line+ consumer count), pairwise
similarity, and a suggested canonical (lowest dep tier).--updatepersists
the corpus toindex_struct_shapes;--fail-on {medium|high|foundation-blocking}
is a non-zero CLOSE/CI gate. The headlinefoundation-blockingseverity flags an
orphan canonical (zero consumers) sitting beside a live shadow.check <file> | --stdin --as <path>— authoring-time gate: matches a
candidate's NEW defs against the corpus and reports any same-shape existing type
("…is 0.85-similar topkg::X— reuse it?"). Exits5above the block
threshold. Used by the PreToolUse hook and as a coder Phase-0 self-check.registry show|allow|unallow|pin|unpin|update— curatedconcept→canonical
pins + a DO-NOT-MERGE allow-list for intentional distinct-role twins (a venue
Fillvs a backtestSimFill). Tracked at<ns>/dups-registry.json.
Similarity, parsing, storage
- Metric:
sim = name_weight·jaccard(field_names) + (1−name_weight)·jaccard(typed_pairs).
The field-NAME blend catches a shadow that restatedUuid→String/DateTime→String
/f64field-for-field under a new name. Field-less (marker) shapes and shapes
belowdups_min_fieldsare excluded. - Parser (
skills/context/scripts/dups-core.py, stdlib python3) — a brace /
generic / attribute-aware scanner over Rust source realizes the proposal's
"Rust + syn" intent without a build step and is deterministic + unit-testable.
Tree-sitter multi-language is a later extension; the shape model + similarity +
clustering are language-agnostic. DB I/O routes through python'ssqlite3
module (no dependency on thesqlite3binary). - Schema:
migrations/0015_struct_shapes.sqladdsindex_struct_shapes
(the field-shape corpus, sibling ofindex_symbols/index_concepts). - Refresh:
shctx refresh --scope=shapes(folded into--all, hence
sprint open) keeps the corpus current.
Enforcement + integration
- New PreToolUse(Write|Edit) hook
hooks/scripts/dups_write_guard.sh— the
shape-shaped sibling ofdedup_write_guard.sh.@coder.rswrites only;
config[dups].dups_hook = off | warn (default) | block. Fails open at every
step (non-coder, non-rust, no python3, empty corpus → silent pass); it can only
block on a real shape match. - New doctrine
doctrines/shape-dedup.md;zero-duplicate-tolerance.mdgains
Layer 4 + a cross-link. - Config
[dups](docs/configuration.md+ both exampleshepherd.tomls):
dups_threshold,dups_block,dups_name_weight,dups_min_fields,
dups_hook,dups_registry(keys aredups_-prefixed becausecfg_getis
section-agnostic).rust-servicewires ashape-dedupclose gate into
[gates].extra. - Tests:
skills/context/tests/test_cmd_dups.sh(scan/check/registry/gate/persist)
andhooks/tests/test_dups_write_guard.sh(block/warn/off + fast-paths), plus
smoke cases.
Fix — the shctx absent false negative
A live v6.1.7 session reported shctx absent. Root cause: shctx is plugin-local
and never on $PATH — it is invoked by the absolute path
${CLAUDE_PLUGIN_ROOT}/skills/context/scripts/shctx. A command -v shctx /
which shctx probe returns absent by design, and when $CLAUDE_PLUGIN_ROOT
does not propagate into the agent's Bash env (some remote/web launches) even the
full-path invocation fails. Neither is evidence of absence.
hooks/scripts/session_open.shnow surfaces, at SessionStart, the absolute
shctxpath resolved from the hook's own location (hooks/scripts → ../..),
correct regardless of$CLAUDE_PLUGIN_ROOT, with the explicit note that
command -v shctxreturns absent by design. Config-gated
[context].announce_shctx_path = on (default) | off.skills/context/SKILL.mddocuments the rule authoritatively (never PATH;
command -vis the #1 false-negative; invoke by absolute path).- Test:
hooks/tests/test_shctx_locator.sh(surfaces the path with
$CLAUDE_PLUGIN_ROOTunset; off-switch suppresses).
Fix — staged-handoff (v6.1.7) never actually worked
Verifying the v6.1.7 staged-handoff feature (/shepherd:spawn --staged +
seed-ready mailbox signal) surfaced a shipped defect: the mailbox.kind CHECK
constraint (from 0007) was a closed enum
(heartbeat_payload|escalation|ack|status|generic), so
shctx mailbox send --kind=seed-ready was rejected by the schema — the signal
could never be sent. Every future doctrine adding a routing tag would have hit the
same wall, silently.
migrations/0016_mailbox_kind_relax.sqlrebuildsmailboxwith a permissive
CHECK(kind <> '')(root-cause fix, not a one-value patch), preserving columns,
data, the FK, both partial indexes, and the unread view.doctrines/staged-handoff.mdjqconsume snippet corrected to iterate the
JSON array (.[] | select(...);recvemits an array).- Test:
skills/context/tests/test_staged_handoff.shdrives the full
send → recv --unread-only --mark-read → ackseed-ready round-trip.
Both full suites green (hooks 44/44, shctx 44/44).