next_activity moves; get_activity reads. The orchestrator advances the session and gets back only the new activity's id; the worker then loads the full definition. Later transitions come from the transitions field of the current activity, validated server-side against the definition.
An active checkpoint locks the session. Between yield_checkpoint and respond_checkpoint, navigation and content reads (next_activity, get_activity, get_technique, get_resource, get_trace) are refused; only the checkpoint tools and get_workflow_status proceed. respond_checkpoint with a user choice is additionally rejected if it arrives less than three seconds after the yield — a response that fast did not involve a person.
Replays are safe. Yielding a checkpoint that was already resolved returns the cached response instead of pausing again, so a worker resumed with stale context cannot double-ask.
+
get_activity requires context_tokens. The worker declares its own context window in tokens on every activity fetch. The server derives an eager step-technique bundling budget from it (availability headroom × a token→char factor, both server config) and inlines ungated step-bound techniques that fit. It is per-agent and per-call — never stored on the session, never defaulted. Omitting context_tokens is a validation error. See eager bundling and Resolution.
Errors: the call failed
@@ -107,7 +108,7 @@
Warnings: the call succeeded, and left evidence
Missing / unexpected steps in manifest
The reported step_manifest doesn't match the activity's declared steps — work was skipped, invented, or renamed.
Step order mismatch, empty step output
Steps reported out of declared order, or a step claimed complete with nothing to show.
-
Technique not fetched
A manifested technique step has no technique_fetched event — the step was summarized from memory, not executed from its definition.
+
Technique not fetched
A manifested technique step has no technique_fetched or technique_bundled event — the step was summarized from memory, not executed from its definition.
Workflow version drift
The definition changed on disk since start_session; the run continues under the new definition.
Parent chain depth
More than five levels of nested dispatch — usually a sign the dispatch topology has gone wrong.
@@ -138,7 +139,7 @@
Reference delivery
hash already in the ledger?deliveredContent, per agent
- { unchanged: true,
+ { delivery: "unchanged",content_hash }full content delivered
@@ -157,7 +158,16 @@
Reference delivery
Reference delivery in persistent mode. The ledger is keyed per agent and per channel (technique bundles, rules, individual techniques), so an orchestrator and a worker on the same session have independent delivery state. An agent that has lost its context can force re-delivery with get_activity { bundle: "full" } or get_technique { full: true }.
-
The marker is a contract, not a hint: unchanged: true asserts the content is byte-identical to what this agent already received, which the server can promise because payload composition is deterministic. If your context no longer contains the earlier copy, ask for full delivery rather than proceeding from a summary.
+
The marker is a contract, not a hint: delivery: "unchanged" asserts the content is byte-identical to what this agent already received, which the server can promise because payload composition is deterministic. If your context no longer contains the earlier copy, ask for full delivery rather than proceeding from a summary.
+
+
Eager step-technique bundling
+
Every get_activity call automatically inlines small, ungated step-bound techniques under a step_techniques map — there is no per-activity opt-in. The worker supplies context_tokens; the server computes a cumulative per-activity character budget:
+
context_tokens × headroomFraction × charsPerToken — defaults 0.80 and 4, overridable via BUNDLE_HEADROOM_FRACTION and BUNDLE_CHARS_PER_TOKEN. Ungated technique steps are inlined in document order until the next would overflow the budget; gated steps (when/condition on the step or an enclosing loop) and oversized techniques stay lazy via get_technique { step_id }. A per-activity bundleTechniques.maxChars caps any single technique; maxChars: 0 opts the activity out entirely. Resources referenced by inlined techniques are never inlined — the worker still calls get_resource on demand.
+
Bundled entries share the technique:<resolvedId> ledger key with get_technique, so in persistent context mode a bundled delivery collapses a later step-bound refetch to an unchanged marker. Each bundled step is recorded as a technique_bundled event and counts toward manifest fidelity checks alongside technique_fetched.
+
+
Enforcement notes and provenance
+
The server is a state ledger and payload composer, not an executor. Some constructs are agent-interpreted — step ordering, condition evaluation, actions[] verbs, protocol execution — and the server never checks them. Because a payload-only reader cannot see this classification, get_activity surfaces an enforcement_notes block (also in _meta) when the current activity contains agent-interpreted constructs: an actions note when a step is kind:action or carries an actions[] list; an auto_advance note when a checkpoint declares autoAdvanceMs. An activity without these constructs carries no block.
+
A step-bound get_technique fetch annotates the binding seam: each of the technique's own inputs carries a source: line stating where its value comes from (step-binding value, workflow variable, prior step output, declared default, or UNRESOLVED); remapped outputs carry a destination: line. Contract-inherited inputs are annotated only when the resolution says something the block note does not. Classification is purely static — same corpus and step always produce byte-identical annotations, which is what makes reference delivery safe.
Dispatch in practice
A typical work package starts before any planning folder exists: the meta session created by start_session for workflow selection is transient, living in the system temp directory. The first dispatch_child that commits to real work does three things at once — promotes the transient session into .engineering/artifacts/planning/<slug>/ (the slug from your request, or a dated fallback), creates the child session embedded in the parent's file, and returns the child's own session_index for the worker. From there:
For contributors. Guards live in scripts/ as npm run check:*; tests run under Vitest in tests/.
The guard suite
-
Most guards audit the workflow corpus (YAML and markdown). A summary by category:
-
-
-
-
Guard
What it enforces
-
-
-
validate-workflow-yaml, validate-activities
Every workflow manifest and activity file parses and satisfies the schemas — a dry run of the server's own loaders.
-
check-all-refs
Every technique reference resolves to a real technique; no misspellings, no dangling paths.
-
check-binding-fidelity
Step bindings are coherent end to end: arguments conform to declared inputs, reads resolve to a producer, outputs are consumed, and inputs have a source. Only new findings fail — known ones are listed in binding-fidelity-baseline.json.
-
check-identifier-qualification
Technique input and output ids are qualified noun phrases (completion_summary, never a bare summary).
-
check-technique-template
Technique markdown follows the normative structure of the technique protocol.
-
check-resource-anchors
Every #section anchor a technique uses to reference a resource exists in that resource.
Namespace collisions between activities and techniques, inputs that circularly reference themselves, and unreachable review-lens paths.
-
check-site-links, check-svg-layout
This site: every internal link and anchor resolves, every GitHub link points at a real file, and no SVG diagram text collides with boxes, arrows, or sibling text.
-
-
-
+
Guards are TypeScript scripts under scripts/ that walk the workflows worktree or this site's HTML and report violations. Each convention named in a specification or architecture page has a matching check — the spec defines the rule; the guard enforces it mechanically. Run them with npm run check:<name>; a clean tree always yields the same result.
+
+
+
+ Guards mirror the specification model: corpus checks group around loadability, technique contracts, static wiring, and workflow-specific safety; site checks keep links and inline diagrams honest. Per-guard behaviour lives in the scripts and in the spec pages that name them — for example Workflows, State, Resolution, and Fidelity.
+
+
+
+
+ Authors run guards locally while editing the workflows branch; Vitest mirrors key corpus checks so regressions fail npm test; the deploy-docs workflow runs the site pipeline on documentation PRs.
+
+
+
Most guards are hard-zero — any finding fails the run. A few compare against a committed baseline of reviewed, pre-existing findings and fail only when the corpus grows beyond that snapshot; re-snapshot with --update-baseline after an intentional change. The full command list is in package.json.
The test architecture
The Vitest suite (thirty-plus files) is organized around what could break:
Unit coverage of the machinery — schema validation and generation, every loader, and the session store's sharp edges: HMAC sealing, index derivation, collision handling, legacy-folder migration.
-
Fidelity behaviour — the runtime validators, binding provenance, fetch observability, reference-not-repeat delivery, and trace-token signing, each tested as an observable contract rather than an implementation detail.
+
Fidelity behaviour — the runtime validators, binding provenance, fetch observability (including technique_bundled coverage), reference-not-repeat delivery, and trace-token signing, each tested as an observable contract rather than an implementation detail.
End-to-end execution — mcp-server.test.ts drives real tool calls; a deterministic walker (tests/e2e/walker.ts) enumerates every reachable path through every workflow definition and executes each one against the live server, so an unreachable activity or an impossible transition in content fails the server's test suite.
Documentation — site.test.ts runs the two site guards and the drift check described below, making stale documentation a red build.
Agent smoke runs — scripts/smoke/ drives a real agent through a workflow for the behaviour no deterministic walk can check: whether the instructions actually steer a model.
Generated, then guarded
-
Two artifacts are machine-derived from the source, and both are guarded against drift rather than trusted to stay fresh. The documentation site itself follows the same pattern — hand-authored pages with generated API regions — and check-site-links / check-svg-layout keep links and diagrams honest:
+
Two artifacts are machine-derived from the source, and both are guarded against drift rather than trusted to stay fresh. The documentation site itself follows the same pattern — hand-authored pages with generated API regions — and check:site / check:svg keep links and diagrams honest:
Module map
-
Grouped by what a request touches. Each row links to the primary source file.
+
Grouped by what a request touches. Tool handlers sit on shared machinery; machinery reads definitions and reads or writes session state on disk.
+
+
+
+ Every tool is a thin handler over the same machinery. Loaders and validators are stateless; only the session store owns mutable run state. The tables below link each group to its source files.
+
+
+
Primary source files by group:
Startup
@@ -152,7 +185,48 @@
Persistence and delivery
The layer every call passes through
-
There is no central router — each tool is a closure registered at startup — but session-bound handlers follow the same skeleton so logging, seal checks, and persistence are uniform. Every handler is wrapped in withAuditLog. Tools that take a session_index call loadSessionForTool before touching state and saveSessionForTool after any mutation. Bootstrap entry points — discover, list_workflows, health_check, and start_session — get audit logging only. The worked example is request lifecycle.
+
There is no central router — each tool is a closure registered at startup — but session-bound handlers follow the same skeleton so logging, seal checks, and persistence are uniform. Bootstrap entry points (discover, list_workflows, health_check, start_session) get audit logging only. The worked example is request lifecycle.
+
+
+
+ The nesting is literal — wrappers in logging.ts and session/resolver.ts — so a failed seal check never reaches handler logic.
+
+
+
Stage by stage:
withAuditLog (logging.ts) — outermost wrapper on every registered handler. It times the call, emits a structured audit event to stderr, and — when the parameters include a session_index — appends a trace event to the session's in-process TraceStore buffer. This runs on success and on thrown errors alike, so a failed seal check or validation still leaves an audit and trace record.
loadSessionForTool (session/resolver.ts) — resolves the six-character session_index to a planning folder (how the index maps to disk), verifies the HMAC seal on session.json against .session-token, and parses the file into typed SessionFile state. Child sessions dispatched via dispatch_child live inside the parent's session.json; the loader navigates to the embedded sub-state the index addresses. If the seal does not match or the file fails schema validation, the call stops here — the handler never runs on state the server cannot vouch for.
@@ -178,11 +252,40 @@
Error discipline
How these surface to a calling agent — and how they differ from non-blocking validation warnings — is covered in Protocol.
Where things live on disk
-
-
Workflow definitions are read from the configured workflow directory — in this repository, the workflows/ worktree of the orphan workflows branch (why an orphan branch).
-
Session state is written under <workspace>/.engineering/artifacts/planning/<slug>/ — the session store page covers the folder's contents.
-
The server key lives at ~/.workflow-server/secret, per user, outside any repository.
-
+
Three locations, three lifetimes — definitions are read-only content, session state belongs to the workspace, and the server key is per-user infrastructure.
+
+
+
+ Definitions and session artifacts are version-controlled in their respective trees; the server key is created lazily at ~/.workflow-server/secret (mode 0600). Folder contents are detailed in session store and getting started.
+
Agents wake in reverse order using the host's resume mechanism, with effects passed down as plain instructions. Before the worker can touch regular tools again it must call resume_checkpoint — a hard gate that verifies the checkpoint really was resolved and hands back the recorded variable updates. In inline (single-agent) execution the wake is a no-op persona switch; the server-side gates are identical.
+
Shared checkpoint fragments
+
A checkpoint reused at several sites is declared once as a checkpoint fragment under fragments.checkpoints in the owning workflow's workflow.yaml. Each use site is a kind:checkpoint step carrying only its site-local id and ref: [workflow::]name — plus a condition when the fragment declares none. The loader materializes the fragment body into the step before delivery, so yield, present, respond, and every downstream consumer see an ordinary full checkpoint. The check:fragments guard rejects an inline body that duplicates a declared fragment.
+
Why this design
Pause and presentation are separate — the worker records the pause with yield_checkpoint and steps aside; the user-facing agent later presents and responds. Nothing advances past an unresolved checkpoint, wherever it was raised.
Example: bare names resolve workflow-first with a meta fallback; nested segments address sub-techniques; a leading workflow segment reaches shared layers explicitly.
+
Borrowed activities and source-workflow scoping
+
A workflow may reuse an activity file authored in another workflow — a borrowed cross-workflow activity. Bare technique refs inside that activity (including activity-group shorthand like activity-id::op) resolve against the source workflow the activity file was authored in, not the borrowing session's workflow. Explicit workflow:: prefixes still reach named workflows directly. The same scoping applies to checkpoint fragment refs and to binding-provenance producer scans on step-bound get_technique fetches. Non-borrowed activities keep source scope equal to the session workflow, so behaviour is unchanged outside the borrowed-activity case.
+
+
Workflow fragments
+
Beyond technique refs, a workflow can declare reusable content once under fragments in workflow.yaml and import it by reference:
+
+
fragments.rules — shared rule texts; rules slots accept either a rule string or { ref: "[workflow::]name" }
+
fragments.checkpoints — shared checkpoint bodies (message, options, effects); a kind:checkpoint step imports one via ref, contributing its own site-local id
+
+
Resolution follows the same scoping as technique refs: workflow::name resolves only in that workflow's fragments; a bare name tries the declaring workflow first, then meta. Fragment bodies are plain content — a fragment cannot itself contain a reference. The loader materializes refs at load and delivery time, so agents always receive full content, never a ref to resolve. The check:fragments guard rejects inline copies that duplicate a declared fragment.
+
Delivery at workflow and activity granularity
Agents never chain resolution calls at runtime. The server pre-resolves reference lists into bundles when you call:
get_activity — worker bundle: activity refs, inherited techniques.activity from the workflow, plus core worker techniques (yield/resume, finalize, conduct rules)
-
Each bundle groups resolved content into three buckets. When a single technique is delivered, ancestor Initial / Final protocol blocks wrap the technique body recursively and the server renumbers the combined sequence. In persistent-context mode, reference-not-repeat delivery replaces already-delivered content with unchanged hash markers; fetch recording warns when a manifested step was never loaded.
+
Each bundle groups resolved content into three buckets. When a single technique is delivered, ancestor Initial / Final protocol blocks wrap the technique body recursively and the server renumbers the combined sequence. In persistent-context mode, reference-not-repeat delivery replaces already-delivered content with { delivery: "unchanged", content_hash } markers; fetch recording warns when a manifested step was never loaded.
+
+
Eager step-technique bundling
+
The lazy per-step model above is complemented by automatic eager bundling on every get_activity call. The worker passes context_tokens — its context window size in tokens — and the server derives a cumulative per-activity character budget:
+
context_tokens × headroomFraction × charsPerToken (defaults 0.80 × 4; env-overridable). Ungated step-bound techniques are inlined in document order under step_techniques until the budget would overflow; gated steps and per-activity bundleTechniques.maxChars oversize caps stay lazy. maxChars: 0 opts the activity out entirely. Each inlined entry is byte-identical to a step-bound get_technique fetch, including provenance annotations; resources referenced inside inlined techniques are never inlined. Bundled steps are recorded as technique_bundled events and satisfy manifest fidelity checks. Protocol details are in Protocol.
When a session starts, the server reads this manifest. The first get_workflow call returns:
@@ -218,9 +219,15 @@
Activities
Only one activity is current at a time. Two roles split the work:
Orchestrator — calls next_activity to move between phases
-
Worker — calls get_activity to load the step list (and may receive bundled techniques up front)
+
Worker — calls get_activity with context_tokens to load the step list and receive eagerly bundled step techniques that fit the derived budget
+
Borrowed activities
+
An activity file can be listed in more than one workflow — the borrowing workflow runs the phase, but the activity file's technique refs resolve against the workflow it was authored in. This mirrors fragment scoping: a borrowed activity's bare refs, checkpoint fragment refs, and binding-provenance scans all use the source workflow, not the session workflow. See Resolution — borrowed activities.
+
+
Shared fragments
+
Rule texts and checkpoint bodies reused at several sites are declared once under fragments in workflow.yaml and imported by { ref: "[workflow::]name" }. Rules slots and kind:checkpoint steps carry the ref; the server materializes the body before delivery, so agents always see full content. A ref-form checkpoint step carries only its site-local id and, when the fragment declares none, a condition — body fields like message and options are forbidden locally. Details are in Resolution — workflow fragments and Checkpoints — shared checkpoints.