Vendor-neutral context control + generated docs
Goal
- Build
workflow_control/as the vendor-neutral control SSOT. - Replace scattered
attach_*/compose_*guidance with one ContextAssembler. - Keep Claude rule files as generated projections until two-client parity passes.
- Make every LLM-visible workflow prompt a script-rendered catalog artifact over current plan/root state; no adapter, skill prose, or model may author or rewrite control text.
Self-hosting constraint
Preserve existing hard hooks during migration. Move selection authority into the engine; adapters and generated Claude projections remain consumers.
Implementation sketch
- Create
workflow_control/{prompts,rules,reminders,catalogs,schemas,clients}/; schemas include exact prompt provenance, TerminalReceipt, and ClientTransportDirective. - Migrate canonical units from CLAUDE.md/rules/skills with provenance; generate compatibility projections instead of copying policy.
- Consume the engine-core selector and ContextAssembler; this section owns canonical control content, projections, adapters, and generated docs.
- Render
payload.reminders[]through one generic Banner component; mandatory directives/effects remain separate. - Add root-anchor, active-invocation-path, capability-boundary, return-to-parent, root-owns-closure, open-descendant, and closure-obligation mappings.
- Inject active-execution isolation RuleUnit + exact bounded coding prompt from the same root/frame capability.
- Route every client prompt/tool/result through the engine authorization gateway; adapters cannot authorize, reselect, rewrite context, infer plan state, or choose a transport lifecycle action.
- Feed every provider event back to engine-core ExecutionPump. Adapters never own reconciliation or continuation; they expose delivery, control injection, generation cancellation, and typed event transport only.
- Render the single NextActionContract verbatim with exact action, target, evidence, result contract, return target, and prohibited alternatives; reject client substitution.
- Resolve mandatory reminders from state/event before every handoff and API response. Record delivered reminder IDs/digests on the dispatch receipt; missing required guidance blocks dispatch.
- Render the GuidanceEnvelope on every interaction: NOW exact task/completion, NEXT successor or closed result map, BANNED enforced alternatives. Never make clients infer any field from history.
- Support engine-pushed ReminderRefresh control events. Inject where supported; otherwise cancel/restart uncommitted generation at deadline or suspend committed work at an idempotent tool boundary, then refresh before activity.
- Restrict each adapter’s effective tools to the selected action. Failure-triage handoffs are read-only and accept only the closed FailureDisposition schema.
- Implement concrete Claude Code, Codex, and OpenCode adapters; all translate the same engine-issued ContextAssembly and authorization gateway. Where a client exposes goal/scheduling lifecycle APIs, the installed goal/loop performs only the exact
ContinueRuntimeAPI.read: active relayscurrent_task/next_upcoming_task, completed relayscompleted_report, andretry_lateremits no task, second call, or lifecycle mutation. The adapter separately snapshots lifecycle facts and delivers exact script-emitted setup/cleanup tool calls and receipts. - Capture each client’s final effective context and reject native rules/skills that conflict with or widen the authorized assembly.
- Enforce the spec-sec-11.4 hard ceiling of 30,000 estimated tokens for each ContextAssembly/raw model invocation. Required authority, result schema, NOW/NEXT/BANNED fields, RuleUnits, reminders, and evidence cannot be silently dropped; over-budget assembly must use references/progressive disclosure/task splitting or refuse with per-category usage.
- Generate
--docsand--explain-contextfrom component + prompt/rule/reminder registries. - Preserve the now-coherent hook redirect: both
block-route-edits.shandblock-status-md-read.shmust direct callers to the sanctionedread.py --summaryAPI and never offerstatus.md; add a regression pin so the contradiction cannot return. - Verify and preserve the existing
to_plan_state_dictread.py:__all__export through the WorkflowContext migration.
Spec references
- plans/workflow-engine-rework/spec/workflow-state-machines.md sec 1.6 (rails pillars 4+5, Plan-API docs SSOT), sec 10.2 (WorkflowContext, —docs). Research: hooks done; banners scattered;
--docs/WorkflowContext missing; status.md prose clash.
Test strategy / acceptance
Matrix: selector fact x context class; lifecycle/result/timer event; client injection support; plan/target; run; task handle; runnable/quiescent turn; goal/loop absent/owned/stale/foreign x ContinueRuntimeView active/completed/retry_later x TerminalReceipt match. Pins: parity; every goal/loop trigger performs exactly one continue-runtime read; active relays current/next unchanged, completed relays its report unchanged, and retry_later emits no task/second call/lifecycle mutation while staying active; every control prompt has script/catalog provenance; every when_condition resolves to a declared fact/gate plus valid args and every while_state resolves to a workflow/state/action selector; stale/missing/wrong-class selector references reject; unprompted timer refresh; safe suspend/resume fallback; one action; prompt/tool substitution rejects; adapters cannot treat client state as plan state. BUG-07-552 is the required regression fixture: the measured 83,522-token legacy invocation must be reduced below the 30,000-token hard ceiling without dropping mandatory context, and the absolute limit is gating rather than observation-only.
Gate: sc-0ff7a001 closes the off_task_access surface enumeration and requires one script-executed negative hook pin per hard-denied surface.
Adapter gate: sc-ef778b10 proves concrete Claude Code, Codex, and OpenCode delivery plus the full three-branch ContinueRuntimeView contract.
Work Items
- Author one generic Reminder Banner renderer + seed mapping catalog with schema validation that resolves every condition/gate argument and every workflow/state/action selector against the closed registries
- Verify and preserve the existing block-route-edits.sh / block-status-md-read.sh redirect to read.py —summary, with a regression pin that rejects any status.md fallback
- Verify and preserve the existing
to_plan_state_dictread.py__all__export through the WorkflowContext migration - Inventory and migrate existing rule, skill, next_action, hook, recurrence, and banned-action guidance into declarative reminder mappings, including the TPR complete-batch dispatch and recurrence cases
- Render structured payload.reminders through Banner presentation on every LLM handoff and API response, and generate reminder docs from the same mapping registry
- Resolve mandatory reminders from state/event and render structured payload.reminders through Banner presentation before every LLM handoff and API response; record delivered reminder IDs/digests on the dispatch receipt and block dispatch when required guidance is missing; generate reminder docs from the same mapping registry
- Create vendor-neutral rule units and contextual injection mappings from CLAUDE.md and .claude/rules without making either Claude surface canonical; preserve source provenance and generate compatibility projections
- Populate canonical prompt/rule/reminder catalogs consumed by the engine-core ContextAssembler; script-render and digest every LLM-visible control prompt from current ContextSnapshot; remove parallel assembly ownership and model/client-authored control text; fold BUG-07-552 into acceptance by measuring every emitted category, enforcing the 30,000-token hard ceiling before dispatch, preserving required units, returning an actionable over-budget refusal, and proving the formerly 83,522-token invocation passes only after progressive disclosure/reference/task-splitting correction
- Implement concrete Claude Code, Codex, and OpenCode thin adapters that translate ContextAssembly and exact ClientTransportDirective tool calls; goal/loop payloads only call ContinueRuntimeAPI.read, relay active current_task/next_upcoming_task or completed_report unchanged, and on retry_later emit no task/second call/lifecycle mutation while staying active, with no rule reselection, prompt rewrite, state inference, or lifecycle decision
- Implement concrete Claude Code, Codex, and OpenCode thin adapters that translate ContextAssembly and exact ClientTransportDirective tool calls, restricted to the selected action’s effective tools (failure-triage handoffs are read-only and accept only the closed FailureDisposition schema); goal/loop payloads only call ContinueRuntimeAPI.read, relay active current_task/next_upcoming_task or completed_report unchanged, and on retry_later emit no task/second call/lifecycle mutation while staying active, with no rule reselection, prompt rewrite, state inference, or lifecycle decision
- Add root/frame/return/closure Reminders that restate immutable root, active path, exact action, resume obligations, prohibited targets, and root closeout ownership
- Inject active-execution isolation from the same root/frame capability; route adapter tool/result requests through engine authorization and compare final effective client context
- Inject active-execution isolation from the same root/frame capability; route adapter tool/result requests through engine authorization, feed every provider event back to engine-core ExecutionPump so adapters expose only delivery, control injection, generation cancellation, and typed event transport (never reconciliation or continuation); compare final effective client context and reject native rules/skills that conflict with or widen the authorized assembly
- Implement proactive guidance rendering and transport controls for engine-pushed timer refresh or safe cancellation/resume; block activity unless NOW/NEXT/BANNED is complete, keep scheduling decisions in engine wiring, require goal/loop to read only ContinueRuntimeAPI and honor its active/current-next, completed/report, and retry-later/no-op branches exactly, and require script-generated client-tool arguments for setup/cleanup
- Generated
--docsderived from the registry + read.py surface (no hand-maintained file); renders per-component intent + spec_ref + changelog as the/improve-toolingpre-touch read - Generated
--docsand--explain-contextderived from the registry + read.py surface (no hand-maintained file); renders per-component intent + spec_ref + changelog as the/improve-toolingpre-touch read - Enumerate which off_task_access surfaces receive mechanical PreToolUse hook denial (the rails are a hook AND a banner, never a banner alone): skill-source reads of engine-migrated workflows alongside the existing plan.json denial; ship the hooks plus one negative pin per enumerated surface, and record which surfaces remain reminder+gateway-only with the progress-denial rationale
Fresh intel (regenerated)
This section is GREENFIELD (workflow_control/ does not exist on disk) but — unlike its sibling engine-core (pure greenfield) — it is anchored to THREE hard EXTERNAL invariants it must PRESERVE while collapsing a MEASURED, wide fragmentation the graph makes visible:
- The scatter it replaces is real and large: 39 modules hand-roll
attach_*/compose_*guidance, 90 runtime modules referencebanner/reminder, 361 modules referencenext_action.workflow_control/is the ONE ContextAssembler + Banner + reminder-catalog those parallel emitters converge onto ([JOIN]with the plan mission “make every LLM-visible workflow prompt a script-rendered catalog artifact”). - The three preserve-invariants are already TESTED — the section’s job is to keep them coherent through the migration, not re-invent them: the two hooks
block-route-edits.sh+block-status-md-read.sh(both already redirect toread.py --summary, both already pinned bytest_block_route_edits.py/test_block_status_md_read_hook.py); and theread.py__all__export (to_plan_state_dict+ 31 siblings, pinned bytest_read.py/test_exit_reasons_export_parity.py). - It CONSUMES engine-core’s selector + ContextAssembler + ContinueRuntimeAPI (sibling
engine-core--s-e585be74, also not-started) — it is a strict downstream of engine-core, NOT a peer.
Decisions this package forces before any code is written:
- Read
engine-core--s-e585be74first. This section’scontext.py/assembly.pyconsumers depend on engine-core’sWorkflowContext,SelectorEngine,ContextAssembly.guidance, andContinueRuntimeAPI.readcontracts. Buildingworkflow_control/against an unbuilt engine-core inverts the dependency. WorkflowContextWRAPSread.pyread-only. The__all__surface (load_plan/query_next_unblocked/query_success_criteria/to_plan_state_dict) is the SOLE scoped plan-state surface; NEVER reconstruct plan state from adapter/goal/loop/transcript. Any second plan-state reader isLEAK:scattered-knowledge.- Touch NOTHING on the live serve path (self-hosting). The plan executes THROUGH
route_walk.serve_next_v7/run_state_machine; the co-change tier proves theread.pyprojection historically co-edits the orchestrator emit/halt machinery — a careless projection edit mutates the run’s own substrate. - The generated Claude rule files are PROJECTIONS, never canonical. Migrating CLAUDE.md/
.claude/rulesintoworkflow_control/rules/must preserve source provenance and generate compatibility projections; making a Claude surface canonical is the DESIGN failure the section exists to prevent. - Every LLM-visible prompt is script-rendered + digested. No adapter, skill prose, or model authors control text;
SCHEMA:unvalidated-configguardsworkflow.schema.json+ the catalog/reminder registries; every reminder mapping resolves its condition/gate args + workflow/state/action selectors against the closed registries.
["serve_next_v7", "route_walk", "run_state_machine", "REGISTERED_HALT_STATES"]
DIG DEEPER
python -m scripts.plan_corpus.read --summary plans/workflow-engine-rework
sed -n '1,60p' plans/workflow-engine-rework/content/rails-components--s-b6c8160c.md
sed -n '1,66p' plans/workflow-engine-rework/content/engine-core--s-e585be74.md
(full dossier: rails-components—s-b6c8160c.intel.md)