Spec-driven development on OpenSpec, with mechanical spec-as-source enforcement: a custom 'spec-as-source' OpenSpec schema adds file-ownership (targets) and test-verification ([@test]) metadata to every capability spec, three scripts (link check, ownership check, manifest build) keep code and specs from drifting apart, plus requirement-gathering, spec-writer, work-review, and a session-handoff skill with a proactive context-warning hook and a packaged handoff memory: the skill ships the exporter, importer, graph model, facts pipeline, Neo4j Compose runtime and operating guide to load handoffs into a local, authenticated Neo4j graph and query them.
68
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
The Markdown handoffs in .handoff/HANDOFF-NNN.md are the canonical record.
Every saved handoff also has an E21 sidecar HANDOFF-NNN.graph.json
(scripts/graph_export.py of the handoff skill, formats v1 and v2). This page explains how to
load those sidecars into a local, authenticated Neo4j Community graph, browse
them from project, session or handoff, and rebuild the graph from scratch.
The graph is a derived index: you can delete it and rebuild it at any time. Nothing in it is needed to read or resume a handoff; the Markdown files remain directly usable without Neo4j.
The local runtime provides no MCP access, wiki or session-YAML import, or
cloud service. Sidecar imports assert explicit records. Additional record relationships come
only from declared graph-model rules: deterministic pattern rules or the AI
facts pipeline described in graph-ai-executor.md.
docker compose version).python3 --version).NEO4J_HTTP_PORT / NEO4J_BOLT_PORT before every docker compose
command; the ports stay bound to 127.0.0.1.Pinned versions: image neo4j:5.26.31-community (5.26 LTS) in
runtime/compose.yaml, driver neo4j==6.3.0 in runtime/requirements.txt,
both inside the handoff skill folder.
Everything the memory needs ships inside the handoff skill folder — the
folder that holds the skill's SKILL.md: scripts/ (exporter, importer, model
engine, facts pipeline), runtime/ (Compose file and driver requirement) and
references/ (graph model, schemas, this guide). In the spec-as-source
repository it is skills/handoff; in an installed plugin it is wherever your
plugin manager placed the handoff skill. Set it once per shell:
HANDOFF_SKILL=/path/to/the/handoff/skill # the folder with SKILL.md, scripts/, runtime/, references/The scripts find their own model, schemas and helper scripts from their
location inside that folder, so they work from any copy of it. Run the commands
from your project folder (the one with .handoff/): handoff paths such as
.handoff are resolved from the current directory, skill files never are.
<skill> in SKILL.md is the same folder as HANDOFF_SKILL.
Create a virtual environment outside the repository (never commit it):
python3 -m venv ~/.venvs/handoff-memory
~/.venvs/handoff-memory/bin/pip install -r "$HANDOFF_SKILL"/runtime/requirements.txtvalidate needs no driver; import and rebuild do. The commands below use
PY=~/.venvs/handoff-memory/bin/python.
Workflow B calls the importer after export and quality checks, using the memory
virtualenv above when available (python3 otherwise):
PY=~/.venvs/handoff-memory/bin/python
[ -x "$PY" ] || PY=python3
"$PY" "$HANDOFF_SKILL"/scripts/import_handoff_graph.py import-saved .handoff/HANDOFF-NNN.md
"$PY" "$HANDOFF_SKILL"/scripts/import_handoff_graph.py import-pendingimport-saved reports imported or da importare, plus the pending count.
The existing sibling sidecar is validated and durably queued before connecting.
Offline service, transaction failure, missing driver or credential all retain
pending work and return 0 for a durable save. Invalid current input returns
1; unsafe or failed queue storage returns 3 and must not be described as a
completed save. Source Markdown, sidecar, metadata and technical identity files
remain unchanged, even on failure. A successful database commit with a failed
queue acknowledgement is reported accurately and leaves an idempotent retry.
These two commands use a nonempty NEO4J_PASSWORD first, otherwise the local
~/.config/handoff-memory/neo4j-password file (line terminator removed).
Missing, unreadable or empty credentials leave pending work. Ordinary import
and rebuild remain environment-only. Never put credentials in commands, URIs,
tracked files, queue state or logs; endpoints remain loopback-only.
The private path-only queue is
${XDG_STATE_HOME:-~/.local/state}/handoff-memory/pending-imports.json.
HANDOFF_MEMORY_QUEUE_FILE can override it to a dedicated private directory.
Queue files contain normalized absolute source paths, so never version them.
Inside a Git tree, the queue, sibling lock and all replacement files must be
ignored: use an ignored private directory. Parent symlinks are resolved before
this protection check. New queue directories and state files use 0700 and 0600; an existing
queue directory must already be private (0700), otherwise it is rejected
without changing its permissions. An
interprocess lock protects enqueue, selection, transaction and acknowledgement.
Malformed or inaccessible state is never replaced with an empty list: repair
or restore that local file before retrying. Queue writes fsync the file and parent directory around atomic replacement.
Unsupported directory synchronization fails before replacement; a post-replace
failure restores prior queue bytes. If storage also prevents restoration, a
private .tmp-prior-* copy remains for recovery rather than losing prior work.
Restore that local recovery copy before retrying; no source file is changed.
import-pending needs no path arguments. It retries every independent entry,
continues after failures and returns 3 if entries remain (0 when empty).
The next saved handoff also retries the backlog. A deleted or moved source stays
pending; restore its existing sidecar or deliberately repair the local queue.
Successful explicit import acknowledges only supplied sidecars already queued;
absent queues are never created by it. Corrupt, unsafe or inaccessible queues
produce a safe warning without preventing the legacy import. validate,
rebuild and facts imports do not acknowledge pending sidecars.
Only explicit v2 metadata.continues references select existing sibling
predecessor sidecars recursively. Their local snapshots govern even if already
stored, reconciling deterministic content while retaining derived edges under
the existing importer rules. An absent predecessor remains an external endpoint:
its matching stored identity must exist; no placeholder is generated. Retry
never regenerates sources or scans unrelated folders.
There is no default password and no unauthenticated mode. Type it without echo and export it for this shell only (Neo4j requires at least 8 characters):
read -rs -p 'Neo4j password: ' NEO4J_PASSWORD; echo; export NEO4J_PASSWORDEvery docker compose command for this project reads NEO4J_PASSWORD;
without it Compose stops with required variable NEO4J_PASSWORD is missing
and nothing starts. Do not run docker compose config without --quiet: it
prints the resolved password. When you are done: unset NEO4J_PASSWORD.
The password initializes a new data volume only. Changing the variable
later does not rotate the password of an existing database; change it from
Neo4j Browser (:server change-password) and use the new value afterwards.
docker compose -f "$HANDOFF_SKILL"/runtime/compose.yaml up -d
until curl -fsS http://127.0.0.1:${NEO4J_HTTP_PORT:-7474} >/dev/null; do sleep 2; done
docker compose -f "$HANDOFF_SKILL"/runtime/compose.yaml psData lives in the named volume handoff-memory_handoff-memory-data, mounted at
/data.
python3 "$HANDOFF_SKILL"/scripts/import_handoff_graph.py validate .handoffArguments are sidecar files or directories; a directory contributes only its
direct HANDOFF-*.graph.json children (no recursive scan). Every input is
checked against the selected unchanged v1 or v2 sidecar schema and for
whole-input consistency
(duplicate keys, versions, repeated or conflicting IDs, filename/provenance
mismatches). Identical duplicates collapse to one. An empty selection fails.
The output ends with a payload digest: the same inputs in any order give the
same digest.
$PY "$HANDOFF_SKILL"/scripts/import_handoff_graph.py import .handoffOptions: --uri (default bolt://127.0.0.1:7687; only loopback bolt:// or
neo4j:// URIs are accepted), --user (default neo4j), --database
(default neo4j). The password is read from NEO4J_PASSWORD only, never from
the command line.
Behaviour:
spec-as-source/handoff/v1; E21 IDs are copied unchanged;HandoffMemoryControl).Exit codes: 0 success, 1 invalid input (nothing connected), 3
configuration or database failure (nothing committed).
What the graph contains is declared in references/graph-model.yaml of the
handoff skill (E24), not in the importer code.
The tables below describe the starting model, which reproduces the E23
graph exactly; see Extending the model to add node and
relationship types.
| Node labels | Properties |
|---|---|
HandoffMemory:HandoffProject | namespace, id |
HandoffMemory:HandoffSession | namespace, id |
HandoffMemory:HandoffDocument | namespace, id, filename, schema_version |
HandoffMemory:HandoffRecord:Decision / :DurableFact / :NextAction / :Failure | namespace, id, type, text, source_file, source_section |
HandoffMemory:Client | namespace, id, name |
HandoffMemory:WorkContext | namespace, id, name |
HandoffMemory:AgentSession | namespace, id, registry_id (exact agent-registry id) |
HandoffMemory:Track | namespace, id, name, project_id |
HandoffMemory:PlanEntry | namespace, id, entry, project_id |
Schema v2 sidecars (E23) enrich the same nodes: HandoffProject gains the
canonical project_id; HandoffDocument gains project_id,
source_directory, source_collection, metadata_profile, metadata_origin,
date, continues, agent_session, parent_session, client,
work_context, track, plan_entry, change, deadline and
operator_person / operator_provider / operator_models. A property that is
absent means the metadata value is null: unknown for historical handoffs
(no invented dates, predecessors, sessions or contexts), "none" for new ones.
An empty work_context list is a known empty list, not an unknown.
V2 relationships (each with namespace), created only from explicit metadata:
handoff FOR_CLIENT client, IN_CONTEXT work context, AUTHORED_BY agent
session, ON_TRACK track, FOR_PLAN_ENTRY plan entry, CONTINUES its declared
predecessor in the same folder (skips and branches allowed), and agent session
CHILD_OF parent agent session. The synthetic HandoffSession stays distinct
from the real AgentSession. Contextual node IDs are typed (a client and a work
context with the same text never collide) and track/plan entry are scoped to
the canonical project.
Identity and copied folders: node IDs come from the technical project UUID
(graph-project-id, or the identity envelope of a historical .meta.yaml) and
the filename, so the same filename in two folders gives two documents. Copies
of the same Markdown in distinct folders stay independent HandoffDocuments
through their technical identity (and collection), even when they share the
same source_directory; group them by canonical project_id.
source_directory is relative to the repository root of its project: the
handoff folder relative to the nearest ancestor holding .git (.handoff,
sub/.handoff, . for the root), or the folder's own name outside any git
work tree. It is never an absolute path.
Mixed v1/v2 input is accepted, but two different definitions of the same
handoff in one batch are rejected.
Relationships (each with namespace): project HAS_SESSION session, session
HAS_HANDOFF handoff, handoff HAS_RECORD record. source_file and
source_section are the exact Markdown pointer from the sidecar provenance.
If the Markdown file is not next to the sidecar, the pointer is kept as is: look
for source_file in the .handoff/ directory of the project whose
.handoff/graph-project-id equals the project id.
The model file has three mappings, keyed by id, in write order:
nodes: each type has labels, identity (source = E21 id taken from the
handoff; key = stable id computed from the listed key properties, equal on
every machine) and optionally write: enrich (merge properties instead of
replacing them);relationships: each type has from and to node types and optionally
containment: true, max_out: N, acyclic: true;rules: how each node and relationship is produced from a handoff. Kinds:
document (project, session, handoff and their containment), record (one
node per record of a record_type), metadata (one keyed node per value of a
metadata field, edge from the handoff or from another rule's node with
from_rule), handoff_ref (edge to another handoff named by a field, e.g.
continues), pattern (one keyed node per regex match in record text, edge
from the record).Example — which files do handoffs talk about? Add to the model:
nodes:
file:
labels: [File]
identity: key
key: [path]
relationships:
MENTIONS:
from: [decision, durable_fact, next_action, failure]
to: [file]
rules:
file_mentions:
kind: pattern
sources: [decision, durable_fact, next_action, failure]
regex: '`([A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+)`'
group: 1
node: file
props:
path: value
edge: MENTIONSThen validate and re-import — no code changes:
$PY "$HANDOFF_SKILL"/scripts/handoff_graph_facts.py validate-model
$PY "$HANDOFF_SKILL"/scripts/import_handoff_graph.py import .handoffThe validator rejects, naming the offending key, anything it cannot guarantee:
undeclared endpoints, reserved or malformed labels, duplicate rules, unused
types, regexes that do not compile. Only declared labels and relationship types
are ever written. In the spec-as-source repository a model change is a spec
change: edit openspec/specs/handoff-graph-model/spec.md in the same change. Removing or
renaming a type is not reconciled incrementally yet: use rebuild.
The pipeline reads handoffs where they are and writes one
HANDOFF-NNN.facts.json beside each, with the nodes and edges the model
produces and, per record, source_line_start, source_line_end and
text_sha256 pointing into the Markdown:
$PY "$HANDOFF_SKILL"/scripts/handoff_graph_facts.py collect ~/project-a ~/project-b # list + SHA-256
$PY "$HANDOFF_SKILL"/scripts/handoff_graph_facts.py extract ~/project-a ~/project-b # write facts
$PY "$HANDOFF_SKILL"/scripts/import_handoff_graph.py import --facts ~/project-a/.handoff ~/project-b/.handoffA root is a project directory with .handoff/, or the .handoff/ directory
itself. Markdown, .meta.yaml and sidecars are never modified; facts are
written only after every handoff of every root was extracted and validated.
graph-project-id is never created unless you pass --create-identity.
Facts carry the model version: after changing the model, run extract again
(the importer refuses facts from another model version). Directory inputs read
*.graph.json by default and *.facts.json with --facts; supplying both
forms of one handoff in a batch is rejected. --model <file> selects another
model file on both commands.
Some relationships are written in prose and need a model to recognise. The
model declares them as ai rules (kind: ai): the repository model has
supersedes (decision SUPERSEDES an earlier decision of the same project)
and resolved_by (failure RESOLVED_BY a decision of the same or a later
handoff). No script calls a model; the flow goes through files:
$PY "$HANDOFF_SKILL"/scripts/handoff_graph_facts.py ai-requests ~/project-a --out requests.jsonl
# an executor answers: "$HANDOFF_SKILL"/references/graph-ai-executor.md
$PY "$HANDOFF_SKILL"/scripts/handoff_graph_facts.py ai-apply ~/project-a --responses responses.jsonl
$PY "$HANDOFF_SKILL"/scripts/handoff_graph_facts.py extract ~/project-a
$PY "$HANDOFF_SKILL"/scripts/import_handoff_graph.py import --facts ~/project-a/.handoffinstruction and, separately, data with
the source record and its candidates; record texts are untrusted data, never
instructions.ai-apply rejects the whole file on any invalid answer (unknown request,
target outside the candidates, confidence outside 0–1) and otherwise stores
ids, model and confidences in .handoff/graph-ai-cache.json — no text.ai-requests lists only unanswered requests. The request id covers the
rule, its version and instruction and the texts of source and candidates:
editing any of them asks again; anything else is reused from the cache, so
extract and rebuild repeat without new calls.derived_by_rule, derived_by_version,
derived_by_model, confidence and derived_request; deterministic edges
carry none. Below the rule's min_confidence no edge is created..graph.json sidecar leaves them in place; importing its facts again
reconciles them to the current cache.Open http://127.0.0.1:7474, connect to neo4j://127.0.0.1:7687 with user
neo4j and your password.
Either list them in Browser:
MATCH (p:HandoffProject {namespace: 'spec-as-source/handoff/v1'})-[:HAS_SESSION]->(s:HandoffSession)-[:HAS_HANDOFF]->(h:HandoffDocument)
RETURN p.id AS projectId, s.id AS sessionId, h.id AS handoffId, h.filename AS filename
ORDER BY filenameor read them from a sidecar:
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d["project"]["id"], d["session"]["id"], d["handoff"]["id"])' .handoff/HANDOFF-029.graph.jsonThen set the parameter in Browser, replacing the value with an ID from the list, for example:
:param projectId => '00000000-0000-4000-8000-000000000000'
:param sessionId => '00000000-0000-4000-8000-000000000000'
:param handoffId => '00000000-0000-4000-8000-000000000000'(the zero UUID is a placeholder; it matches nothing).
MATCH (p:HandoffProject {namespace: 'spec-as-source/handoff/v1', id: $projectId})-[:HAS_SESSION]->(:HandoffSession)-[:HAS_HANDOFF]->(h:HandoffDocument)-[:HAS_RECORD]->(r:HandoffRecord)
RETURN h.filename AS handoff, r.type AS type, r.text AS text, r.source_file AS source_file, r.source_section AS source_section
ORDER BY handoff, type, source_section, textMATCH (s:HandoffSession {namespace: 'spec-as-source/handoff/v1', id: $sessionId})-[:HAS_HANDOFF]->(h:HandoffDocument)-[:HAS_RECORD]->(r:HandoffRecord)
RETURN h.filename AS handoff, r.type AS type, r.text AS text, r.source_file AS source_file, r.source_section AS source_section
ORDER BY handoff, type, source_section, textMATCH (h:HandoffDocument {namespace: 'spec-as-source/handoff/v1', id: $handoffId})-[:HAS_RECORD]->(r:HandoffRecord)
RETURN h.filename AS handoff, r.type AS type, r.text AS text, r.source_file AS source_file, r.source_section AS source_section
ORDER BY handoff, type, source_section, textTo see only one type, add a label to r: (r:Decision), (r:DurableFact),
(r:NextAction) or (r:Failure). For a picture of the graph, return the path instead:
MATCH path = (h:HandoffDocument {id: $handoffId})-[:HAS_RECORD]->() RETURN path.
Set the canonical project id, e.g. :param projectId => 'spec-as-source'.
MATCH (h:HandoffDocument {namespace: 'spec-as-source/handoff/v1', project_id: $projectId})
OPTIONAL MATCH (h)-[:CONTINUES]->(prev:HandoffDocument)
RETURN h.source_directory AS folder, h.filename AS handoff, prev.filename AS continues, h.date AS date
ORDER BY folder, handoff:param client => 'Lizia''s Cake'
MATCH (c:Client {namespace: 'spec-as-source/handoff/v1', name: $client})<-[:FOR_CLIENT]-(h:HandoffDocument)
RETURN h.project_id AS project, h.source_directory AS folder, h.filename AS handoff, h.date AS date
ORDER BY project, folder, handoff:param context => 'sviluppo'
MATCH (w:WorkContext {namespace: 'spec-as-source/handoff/v1', name: $context})<-[:IN_CONTEXT]-(h:HandoffDocument)-[:HAS_RECORD]->(f:Failure)
RETURN h.filename AS handoff, f.text AS failure, f.source_file AS source_file, f.source_section AS source_section
ORDER BY handoff, failure// derived relationships at or above $minConfidence
MATCH (a:HandoffMemory)-[r]->(b:HandoffMemory)
WHERE r.derived_by_rule IS NOT NULL AND r.confidence >= $minConfidence
RETURN type(r) AS rel, a.text AS source, b.text AS target, r.confidence AS confidence,
r.derived_by_model AS model, a.source_file AS sourceFile, b.source_file AS targetFile
ORDER BY confidence DESCSet the threshold first, e.g. :param minConfidence => 0.7. Decisions still in
force (not superseded with that confidence):
MATCH (d:HandoffMemory:Decision)
WHERE NOT EXISTS { MATCH (:Decision)-[r:SUPERSEDES]->(d) WHERE r.confidence >= $minConfidence }
RETURN d.text, d.source_file ORDER BY d.source_file DESC LIMIT 50The E23 recovery turns old handoffs into v2 without touching their Markdown:
Freeze the inventory (paths, filenames, SHA-256) and the explicit mapping
collection → canonical project_id → technical UUID. An existing
graph-project-id is reused unchanged; a folder without one gets a
deterministic UUID persisted in the manifest and in each .meta.yaml
identity envelope, never as a new external identity file. Audit files and
candidates name other projects, so they live in the git-ignored
output/private/handoff-v2/recovery/, never under a tracked path.
Stage candidates in this repository (sources are read-only):
python3 "$HANDOFF_SKILL"/scripts/graph_export.py recover \
--manifest output/private/handoff-v2/recovery/manifest.json \
--inventory output/private/handoff-v2/recovery/inventory.json \
--classification output/private/handoff-v2/recovery/classification.json \
--out output/private/handoff-v2/recovery/candidatesWithout --out, recover writes to output/private/ of the current git
work tree (and refuses to run outside one). Any destination inside a git work
tree that git does not ignore is refused before anything is written.
Mechanical fields come from the header first, then from an explicit body
statement; unknowns stay null with a diagnostic, date-only dates keep their
precision, and an explicit reference to a missing handoff stops the run.
work_context and track come from the Sonnet classification (a header
Filone wins over it). recovery-report.json lists null counts, continuation
edges, drift and held destinations. Re-running with the same inputs gives the
same bytes.
Publish create-only next to each Markdown (take the agent-registry lock on the files first):
python3 "$HANDOFF_SKILL"/scripts/graph_export.py publish \
--candidates output/private/handoff-v2/recovery/candidates \
--manifest output/private/handoff-v2/recovery/manifest.jsonOnly absent .meta.yaml / .graph.json files are created (an atomic link,
so a concurrent writer wins without truncation). An occupied destination is
reported under held and left byte-identical. After someone reviews the
candidate and its diff and explicitly approves the replacement, pass
--replace <destination>=<sha256 of the reviewed current file>: the file is
replaced only if it still has that hash.
Validate and import the full corpus twice, then compare sorted snapshots.
Precedence for any handoff: Markdown frontmatter, then the sibling
HANDOFF-NNN.meta.yaml, then null. The Markdown stays canonical; .meta.yaml
holds recovered metadata; sidecar and graph are derived and can be regenerated.
docker compose -f "$HANDOFF_SKILL"/runtime/compose.yaml stop # keeps data
docker compose -f "$HANDOFF_SKILL"/runtime/compose.yaml start
docker compose -f "$HANDOFF_SKILL"/runtime/compose.yaml down # removes the container, keeps the volume
docker compose -f "$HANDOFF_SKILL"/runtime/compose.yaml up -d # same data againNever add -v to down unless you want to discard the derived graph:
down -v deletes the volume. Nothing canonical is lost that way — the graph can
be rebuilt below — but the Browser data is gone until you do.
rebuild replaces everything in the namespace spec-as-source/handoff/v1,
across all projects stored there, with exactly the sidecars you pass. Always
pass the complete desired set: whatever you leave out is removed.
python3 "$HANDOFF_SKILL"/scripts/import_handoff_graph.py validate .handoff # check the full set first
$PY "$HANDOFF_SKILL"/scripts/import_handoff_graph.py rebuild .handoffIt validates everything before connecting, then in one transaction takes the namespace lock, checks the boundary, deletes the namespace content and inserts the new set. It never clears the database or deletes the volume, and it leaves nodes and relationships outside the namespace untouched. If any relationship connects a namespace node to something outside the namespace (or an unowned relationship touches a namespace node), it aborts before deleting anything: inspect it with
MATCH (n:HandoffMemory {namespace: 'spec-as-source/handoff/v1'})-[x]-(o)
WHERE NOT (type(x) IN ['HAS_SESSION', 'HAS_HANDOFF', 'HAS_RECORD'] AND coalesce(x.namespace, '') = 'spec-as-source/handoff/v1' AND o:HandoffMemory AND coalesce(o.namespace, '') = 'spec-as-source/handoff/v1')
RETURN n.id, type(x), labels(o)and remove or move that relationship yourself. Any failure rolls back to the previous content.
The exporter picks the version itself: v2 when the handoff has frontmatter or a
sibling .meta.yaml, v1 otherwise. Regeneration keeps graph-project-id, the
.meta.yaml identity envelopes and every existing ID; historical Markdown and
metadata files are never rewritten by it.
Use this when sidecars are missing or you want to be sure the graph matches the Markdown:
Keep the Markdown handoffs and .handoff/graph-project-id untouched. That
file holds the project identity: if it is lost, E21 generates a new one and
every ID in the graph changes.
Re-export each handoff with the unchanged E21 exporter (it only rewrites its
own HANDOFF-NNN.graph.json):
for md in .handoff/HANDOFF-*.md; do python3 "$HANDOFF_SKILL"/scripts/graph_export.py "$md"; doneValidate the full set, then rebuild:
python3 "$HANDOFF_SKILL"/scripts/import_handoff_graph.py validate .handoff
$PY "$HANDOFF_SKILL"/scripts/import_handoff_graph.py rebuild .handoffThe importer never writes Markdown, sidecars or graph-project-id. Recovery
never depends on copying a Neo4j data volume.
validate still works offline.import/rebuild fail with exit code 3 and a short error class
(for example ServiceUnavailable); sources are not modified.up -d) and re-run the command, or rebuild from
scratch with the procedure above.These checks live in the spec-as-source repository, not in the skill; the paths below are relative to that repository's root.
bash tests/handoff-memory/test_contract.sh (part of
bash scripts/verify.sh; no Docker, no network).SMOKE_PYTHON=$PY bash tests/handoff-memory/smoke.sh. It
starts a separate Compose project with its own volume, random loopback ports
and a temporary password, checks import, idempotence, rejection,
provenance, the queries on this page, stop/start persistence, rebuild,
boundary and rollback, concurrency, then removes only its own project and
volume. It is never run by verify.sh.bash tests/handoff-memory/test_packaging.sh (part of
verify.sh) runs the offline commands from a copy of the skill outside the
repository; SMOKE_PYTHON=$PY bash tests/handoff-memory/smoke_skill_copy.sh
runs the documented export, import and queries from such a copy against its
own isolated Compose project. Neither touches the project handoff-memory..tessl-plugin
rules
skills
handoff
handoff-skill
openspec-apply-change
openspec-archive-change
openspec-explore
openspec-propose
openspec-sync-specs
plan-judge
plan-mode
prompt-engineer
prompt-loop
requirement-gathering
spec-as-source-setup
templates
openspec-schema
spec-as-source
templates
spec-ci-sync
spec-loop
spec-rebuild
spec-verify
spec-writer
work-review