Portable MCP server for agent handoff state and review tracking.
Project description
Workstate Handoff MCP
Portable MCP server for agent handoff state, review findings, exports, close checks, and lane-scoped worker/orchestrator coordination.
Scope
This package owns handoff state and related workflow primitives:
- task state, decisions, blockers, and next actions
- review findings and review runs
- generated
CURRENT_TASK.jsonsnapshots - lane registration, lane activity, worker reports, and lane messages
- artifact indexing and derived metrics snapshots
It does not include unrelated repo-intel or UI helpers.
Runtime State
By default, runtime state lives under the workspace you point the CLI at:
- SQLite DB:
.task-state/handoff.db - exports:
.task-state/exports/ - generated markdown:
CURRENT_TASK.json
Override these with CLI flags or WORKSTATE_HANDOFF_* environment variables. Legacy pre-rebrand names are still read for one release.
Tuning compaction thresholds
The compaction advisory (get_handoff_state(sections="identity").compaction_advisory)
compares observed token and char totals against thresholds. The single source
of truth for the canonical defaults is the compaction: block of
docs/agentic/contracts/harness-protocol.yaml (threshold_tokens,
threshold_chars); see that file for the current values and the rationale
comment above them. Per-deployment overrides resolve with precedence
env > overlay > contract, per knob, every call (no module-level cache):
-
Environment variables:
WORKSTATE_HANDOFF_COMPACTION_THRESHOLD_TOKENS=<int>WORKSTATE_HANDOFF_COMPACTION_THRESHOLD_CHARS=<int>
-
.workstate-overlay.jsonat the workspace root, sibling ofsurfaces(tokensandcharsmust be non-negative integers; the values below are illustrative overrides, not required values):{ "surfaces": {"contracts": {"local_root": "docs/agentic"}}, "compaction": {"thresholds": {"tokens": 90000, "chars": 360000}} }
Either knob may be omitted; missing knobs fall through to env or contract.
-
Contract default in
harness-protocol.yaml(single source of truth).
The advisory envelope reports the effective value at thresholds.{tokens,chars}
and the resolving layer at thresholds_source.{tokens,chars} ("env" /
"overlay" / "contract"). Invalid override values (non-int, negative) append
a compaction_threshold_override_invalid: <source>=<key>=<value> warning to
warnings and fall through to the next layer.
When the advisory says compaction is recommended, recording is still an
explicit action: call compaction(operation="record", ...), run
make compact-now, or let the compact-session Stop hook write the row. A
successful record keeps compaction_id=<id> as the first operator-visible
line and then prints receipt values in stable order:
compaction_id=C-WORKSTATE-99-0001
tokens_saved_estimate=123
input_chars=4200
summary_chars=800
prose_residual_chars=200
load_session(include_context_refresh=True) can also return a soft
same-session context_refresh packet for the latest compaction. Pass the
returned dedupe_key back as last_injected_compaction_id to avoid injecting
the same packet repeatedly. This refresh packet helps attention and grounding;
it does not replace or disable the host harness's own context compaction.
Disabling Workstate compaction
Workstate unifies the runtime disable surface so the advisory short-circuit
(compute_compaction_advisory) and the compact-session Stop hook are
silenced together by a single resolver. Host-harness compaction (Claude
Code's own /compact, Codex's internal summarization, etc.) is not affected
— this disables Workstate's tracker only. For the operator-facing comparison
between Workstate's compaction and the host harness's built-in compaction (and
the note on what the compression-ratio benchmark actually measures), see
docs/explainers/compaction-vs-default-harness-compaction.md.
Precedence (highest first):
WORKSTATE_HANDOFF_COMPACTION_DISABLEDenv var (truthy). Legacy disable aliases are still honored for one release.- Task-scoped row in
compaction_settings(when atask_refis provided). - Workspace-default row in
compaction_settings. - Otherwise: enabled.
Write the DB rows via any of these equivalent surfaces:
# Operator make wrappers (flagless, no YAML editing).
make compaction-disable # workspace default
make compaction-disable TASK=WORKSTATE-99 # task-scoped
make compaction-enable [TASK=...]
make compaction-status [TASK=...] # prints the resolved receipt
# CLI form (identical effect).
mcp-workstate-handoff compaction --operation disable [--task-ref WORKSTATE-99]
mcp-workstate-handoff compaction --operation enable [--task-ref WORKSTATE-99]
mcp-workstate-handoff compaction --operation status [--task-ref WORKSTATE-99]
# MCP op form (callable from any harness; payload is a discriminated union).
compaction(operation="disable", task_ref="WORKSTATE-99")
compaction(operation="enable", task_ref="WORKSTATE-99")
compaction(operation="status", task_ref="WORKSTATE-99")
Each call returns a CompactionStatusReceipt:
{"disabled": true, "source": "db", "env_override": false,
"db_row": {"scope_kind": "workspace", "task_ref": null,
"enabled": false, "updated_at": "...", "updated_by": "..."}}
When disabled=true, the advisory envelope carries
disabled=true, disabled_source=<env|db> (threshold/floor logic skipped), the
Stop hook logs compaction skipped: disabled (source=<env|db>), and the
dashboard's Needs Attention rail prints
compaction: disabled via <source>.
Installation
From PyPI (recommended)
pip install mcp-workstate-handoff
# or, as an isolated tool:
uv tool install mcp-workstate-handoff
# or, ad-hoc without installing (pin a release for reproducibility):
uvx mcp-workstate-handoff@0.11.1 --workspace-root /path/to/workspace serve-stdio
From the monorepo source tree (development)
From this package root inside workstate:
cd packages/mcp-workstate-handoff
python -m pip install -e ".[dev]"
Development
All package-local development commands are intended to run from the package root:
make lint-handoff
make fix-lint-handoff
make format-handoff
make mypy-handoff
make test-handoff
make check-handoff
Prefer the package-local Make targets when running checks from editors or agent sessions. Workspace-level interpreter discovery can point at a minimal environment that lacks this package's dev extras such as pytest, while make test-handoff and make check-handoff keep execution anchored to the package root and honor PYTHON=/path/to/python3 overrides when you need to pin a specific interpreter.
The Makefile automatically adds a sibling ../workstate-codex-bridge/src to PYTHONPATH when that checkout exists. In a fully extracted repo, that fallback is unnecessary once dependencies are installed normally.
Direct commands also work:
PYTHONPATH=src python -m ruff check src tests
PYTHONPATH=src python -m mypy src
PYTHONPATH=src python -m pytest tests -q
Python API
The package can be used directly as a library — this is the primary fallback when MCP tool calls are unavailable.
Always import from the package root (workstate_handoff_mcp), never from submodules like .config, .decisions, or .core. Submodules are internal and may change.
from pathlib import Path
from workstate_handoff_mcp import (
RuntimeConfig,
configure_runtime,
get_handoff_state,
search_handoff,
validate_decision_id,
record_event,
review_findings,
list_review_findings,
set_handoff_state,
update_task_status,
get_verified_tests,
render_handoff,
record_file_touch,
get_touched_files,
)
# Configure runtime before any read/write call
configure_runtime(RuntimeConfig.for_repo(Path("/path/to/workspace")))
# Read state
state = get_handoff_state(sections="identity")
# Search decisions
results = search_handoff(queries=["slice_complete"], record_types=["decision"], limit=5)
# Preflight a slice-complete decision id before writing
preflight = validate_decision_id(
decision="codex_slice_complete_plan0004_contract-pinning-and-docs",
decision_kind="slice_complete",
)
# List open findings
findings = list_review_findings(status="open")
Without installation (monorepo source tree):
PYTHONPATH=packages/mcp-workstate-handoff/src python3 -c "
from pathlib import Path
from workstate_handoff_mcp import RuntimeConfig, configure_runtime, get_handoff_state
configure_runtime(RuntimeConfig.for_repo(Path('.')))
state = get_handoff_state(sections='identity')
print(state['data']['active']['task_ref'])
"
Compaction Advisory
get_handoff_state(sections="identity") exposes the canonical compaction
advisory at data.compaction_advisory, with a
mirrored boolean at data.compaction_recommended (key configurable via
compaction.advisory_field in docs/agentic/contracts/harness-protocol.yaml).
The same envelope is mirrored by load_session, and the workspace-summary
CURRENT_TASK.json carries it at active.compaction_advisory (no data
wrapper — CURRENT_TASK.json is the projection file itself, not an MCP
tool envelope).
state = get_handoff_state(sections="identity")
advisory = state["data"]["compaction_advisory"]
if advisory["recommended"]:
# Schedule a compaction; thresholds + observed totals + transcript path
# are documented under the advisory envelope.
...
Cold-start consumers MUST read advisory state from this surface rather than recomputing token/character totals locally — the evaluator owns contract loading, transcript discovery, and harness detection.
CLI Usage
Run the MCP server over stdio:
mcp-workstate-handoff --workspace-root /path/to/workspace serve-stdio
Run the MCP server over HTTP:
mcp-workstate-handoff --workspace-root /path/to/workspace serve-http
FastMCP's current HTTP defaults are:
- host:
127.0.0.1 - port:
8000 - path:
/mcp
Useful CLI checks:
mcp-workstate-handoff --workspace-root /path/to/workspace doctor
mcp-workstate-handoff --workspace-root /path/to/workspace state
mcp-workstate-handoff --workspace-root /path/to/workspace validate --kind decision-id --decision codex_slice_complete_plan0004_contract-pinning-and-docs --decision-kind slice_complete
mcp-workstate-handoff --workspace-root /path/to/workspace review-findings --operation list
mcp-workstate-handoff --workspace-root /path/to/workspace handoff-close-check
Slice-complete Decision IDs
Do not hard-code the slice-complete regex in client prompts or scripts. Treat get_handoff_state(sections="identity").data.limits.write.slice_complete_decision_id as the authoritative registry, and use validate_decision_id(decision=..., decision_kind="slice_complete") for side-effect-free preflight when composing ids outside close_slice(author_tag=..., work_ref=..., slug=...).
Valid: cdx_slice_complete_plan0004_contract_pinning_and_docs
Invalid: cdx_slice_complete_plan0004_contract-pinning-and-docs
Review Intake
Preferred path when workstate-orchestrator-mcp is available:
get_latest_slice_review_packetget_review_findings_summaryorreview_findings --operation listas needed
Handoff-only fallback:
load_sessionsearch_handoff(queries=["slice_complete"], record_types=["decision"], limit=1)get_verified_tests(task_ref=..., commit_sha=...)review_findings(review={"operation":"list","status":"open"})
Use the MCP read surfaces above before inspecting .task-state/handoff.db directly. Drop to raw SQLite or filesystem inspection only when the required MCP tool is unavailable or when you are debugging MCP transport or serialization failures.
Source-tree execution without installation:
PYTHONPATH=src python -m workstate_handoff_mcp --workspace-root /path/to/workspace serve-stdio
Token-Efficient Usage
The v2 envelope is already compact, but callers still save the most tokens by shaping read responses deliberately:
- Prefer named
read_profile=over hand-rolledsections=/top_n_*in routine paths. Profiles:identity,hot_summary,review_packet,open_items,full_debug. Profiles bundle a stable shape and surfacedata.read_shape.applied_profilefor verification. - Pair
read_profile=withresponse_budget_bytes=in production retry loops. The Layer-2 budget planner reduces detail level, halvestop_n_*, and drops optional sections before heavy rows materialize, so a budgeted call returns within a single round trip. The default policy isauto_summarywhen a budget is set; passbudget_policy="fail"to receivedata.read_budget.retry_withinstead of an over-budget payload. - Use
get_handoff_state(read_profile="identity")(or the legacysections="identity") for routine task checks instead of a full state fetch. - Use
detail="summary"on read surfaces such asget_handoff_state,load_session,review_findings,search_handoff, andartifactswhen truncated text is acceptable. - Use
top_n_*,limit=, andfields=to cap read size instead of trimming large payloads client-side.load_sessionacceptstop_n_touched_files(default 20, max 200) to bound the additivetouched_fileslist. - Read from the canonical
datablock —result["data"]["active"]etc. The legacy top-level mirror was removed in 0.3.0 and never returns.
Package-local guidance and examples live in docs/guides/token-efficient-usage.md.
Wire format note (≥0.3.0)
Starting in mcp-workstate-handoff 0.3.0, MCP tool responses are native JSON
objects on the wire, not JSON strings inside structured_content.result.
Every handler is annotated -> dict and returns a real dict via
_envelope(); FastMCP serialises it once. If you previously did
json.loads(handoff_tool(...)) to parse a JSON string return value, drop
the json.loads — the call returns a dict directly. If you previously
read result.content[0].text from the MCP wire payload and parsed it,
read result.structured_content directly instead.
The envelope field set is unchanged (ok, schema_version, tool,
scope, data, mutation, artifacts, warnings, task_ref) and
schema_version stays at 2. Only the wire-encoding moved from
JSON-string-inside-JSON to a native nested object. See
CHANGELOG.md for the full migration notice.
Client Adapter Shape
Installed console-script adapter (resolves whatever version is currently installed):
{
"name": "altcontext-mcp",
"command": "mcp-workstate-handoff",
"args": ["--workspace-root", "/path/to/workspace", "serve-stdio"]
}
Pinned via uvx (recommended for consumers — reproducible across machines without a global install):
{
"name": "altcontext-mcp",
"command": "uvx",
"args": [
"mcp-workstate-handoff@0.11.1",
"--workspace-root",
"/path/to/workspace",
"serve-stdio"
]
}
Source checkout adapter:
{
"name": "altcontext-mcp",
"command": "python3",
"args": [
"-m",
"workstate_handoff_mcp",
"--workspace-root",
"/path/to/workspace",
"serve-stdio"
],
"cwd": "/path/to/mcp-workstate-handoff",
"env": {
"PYTHONPATH": "/path/to/mcp-workstate-handoff/src"
}
}
Version pinning
Consumer configs should pin the server version they were validated against so that
client-adapter contract drift is visible at config-review time rather than at
runtime. The installed console-script adapter resolves whatever release is
currently installed; the uvx and source-checkout variants above pin a known
release.
To verify the version of the server actually being launched:
mcp-workstate-handoff --version
# mcp-workstate-handoff 0.11.0
The same value is exposed as a top-level field in run_doctor output so
adapters can introspect it via the MCP surface:
{
"ok": true,
"version": "0.11.0",
"workspace_root": "/path/to/workspace",
"...": "..."
}
Tool Surface
mcp-workstate-handoff now exposes a single unified MCP surface.
Legacy --tool-profile all|core|extended inputs are still accepted for compatibility, but they all expose the same tool set:
mcp-workstate-handoff --workspace-root /path/to/workspace --tool-profile core serve-stdio
Unified surface
| CLI name | MCP tool |
|---|---|
state |
get_handoff_state |
set |
set_handoff_state (pass --status-only for status-only updates that replace the legacy update_task_status) |
validate |
validate (--kind decision_id|write; replaces the legacy validate-decision-id and validate-write subcommands) |
event |
record_event |
next-actions |
next_actions |
review-findings |
review_findings |
review-runs |
review_runs |
integrity-check |
integrity_check (--kind working_tree|post_merge|close; replaces the legacy working-tree-integrity-check, post-merge-integrity-check, and handoff-close-check subcommands) |
render-handoff |
render_handoff (kind=current_task/dashboard) |
| (no CLI) | load_session |
| (no CLI) | close_slice |
audit-decisions |
audit_decision_ids |
export |
export_handoff_state |
import |
import_handoff_state |
archive |
archive (--operation archive|gc|get; replaces the legacy archive-task-state, tasks-gc, and get-archived-task subcommands) |
artifacts |
artifacts (typed --operation selects record, search, get, or purge for indexed artifact rows) |
touched-files |
touched_files (--operation record|list; replaces the legacy record-file-touch and get-touched-files subcommands) |
compaction |
compaction (--operation record|get|get_latest; replaces the legacy compact-session, get-compaction, and get-latest-compaction subcommands) |
handoff-search |
search_handoff |
handoff-rows |
list_handoff_rows |
get-verified-tests |
get_verified_tests |
record_event uses a typed event payload with event_kind="decision" | "test_result" | "blocker" so each variant keeps its own required fields.
next_actions uses operation="list" | "add" | "update" | "complete" | "skip".
review_findings uses operation="record" | "batch_record" | "update" | "list".
review_runs uses operation="record" | "list" | "coverage".
Branch/worktree drift is surfaced through warnings on write responses. If you
set WORKSTATE_HANDOFF_ENFORCE_BRANCH=1, branch mismatches against the active
task's target_branch fail before mutation on enforceable branches. Direct
Python callers see BranchMismatchError; MCP clients receive the standard v2
error envelope with data.expected_branch and data.actual_branch.
CLI-only extras (not part of MCP registry): artifact-list, artifact-terms.
Surface classes:
action: mutates canonical state; do not blind-retryquery: read-only inspection of canonical state; usually safe to retrygenerator: derived output such as close checks, markdown renders, and metrics summaries
Troubleshooting
The fastest first check is still:
mcp-workstate-handoff --workspace-root /path/to/workspace doctor
If startup succeeds but calls fail, check the workspace paths first: --workspace-root, --state-dir, --current-task-path, and --exports-dir must all point at the same workspace state.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_workstate_handoff-0.11.5.tar.gz.
File metadata
- Download URL: mcp_workstate_handoff-0.11.5.tar.gz
- Upload date:
- Size: 479.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6b9f556e47a8d5d2040ca7cfcdc3411229ea9f0f75430231492b4c468f997d0
|
|
| MD5 |
5fba4c4ee92ac6377310fcb3c4154ab6
|
|
| BLAKE2b-256 |
83e7087d99ceeb8781c17336fb93d4932b0bac31a2cbfbca1fa622925477f535
|
File details
Details for the file mcp_workstate_handoff-0.11.5-py3-none-any.whl.
File metadata
- Download URL: mcp_workstate_handoff-0.11.5-py3-none-any.whl
- Upload date:
- Size: 256.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6713c3f482f85cfb86ed2a988bdaf3fa38d96a33f897d1dfeda0d88e7eea4055
|
|
| MD5 |
44991b09bb6472359a656c330f2600c9
|
|
| BLAKE2b-256 |
dfe9558033a45e5b5f11ecbf674c4d623b84d2fb470c0a70117c9d2f46ed0cc6
|