All notable changes to Mediforce.
Format: Keep a Changelog.
Weekly cuts via /add-changelog-entry + auto-cut every Monday 09:00 CET (.github/workflows/changelog-cut.yml).
Every non-trivial PR adds a bullet under ## [Unreleased]. Trivial edits (typos, single-line config, comment-only diffs) may be omitted. Renovate bumps batch under ### Dependencies.
- Workspace branding in Workspace settings (organizations): owners/admins can upload a logo image and set a main and auxiliary brand color. The logo replaces the Lucide icon wherever the workspace is shown (sidebar switcher, workspace header, workspace picker) and falls back to the icon when unset; the two colors override the
--primary/--accentdesign tokens app-wide (plus their--ring/--sidebar-*mirrors, with a contrast-picked foreground), applied in both light and dark. Logo is stored inline as a size-capped base64 data URL on the workspace record and travels with the existingnamespaces.get/users.mepayloads — no new upload/serving route; colors are#rrggbbhex on the same record. All three flow through the existingPATCH /api/namespaces/:handle(empty string clears), and the base64 logo is kept out of the audit snapshot.
mediforce-fullstackno longer re-triages the same declined (fullstack:manual) issue on every tick: the "re-judge on edit" check comparedissue.updated_atagainst the manual label event, butupdated_atis bumped by any activity — including a human adding an unrelated label — which latched issues like #425 into an endless re-triage loop.fetch-candidatesnow treats an issue as edited only whenupdated_atoutruns the newest recorded issue event (label/assignment changes create an event at the same instant, so they are ignored; body edits and comments are the only changes that don't, so they still re-judge), andapply-verdictsre-stamps the label when re-declining an already-manual issue so a genuine edit re-judges exactly once instead of looping.- Runs stranded in
runningare now recovered by the cron heartbeat. When a run's auto-runner request dies mid-step (deploy, crash, or request timeout — the whole step loop runs inside one long-lived/runrequest), the run is leftstatus: runningwith no live driver. The heartbeat's sweeps only looked atpausedruns (resumewaiting_for_timer, failpaused/null-reason orphans), so a strandedrunningrun was invisible to every sweep and sat at its current step forever — e.g. amediforce-fullstackrun wedged atarm-timerfor 2h+ while its PR's CI had long since gone green. The heartbeat now also sweepsrunningruns idle past their current step's own configured timeout plus a grace margin (falling back to a 45-minute default when the step can't be resolved), so a step that legitimately runs longer than the default is never mistaken for stranded, and re-kicks them; the re-kick is idempotent —/runreturns 409 while a live driver still holds the per-process lock, so only genuinely driverless runs advance #892. - Cancelling a run now cancels its still-actionable (
pending/claimed) human tasks too, so dead runs no longer leave zombie "Action needed" items lingering in the task queue; the cancel audit event records how many tasks were cancelled #639.
<<<<<<< fullstack/issue-818-task-assigned-notification
task_assignednotifications now dispatch when a human task is created (opt-in via atask_assignedentry in the workflow'snotifications), so role members are pushed an email instead of having to pull the "Human actions" inbox; a pre-assigned (assignedTo) or fallback-to-creator assignee is also notified even when outside the configured roles, so a claimed task never leaves the responsible person polling the inbox. Fires from both task-creation paths — the engine'sadvanceStep(next step is human) and the API auto-runner (an already-current human step, e.g. a workflow whose first step is human) — via one sharedengine.dispatchTaskAssignedNotification; mirrors the existingagent_escalationdispatch #818. =======mediforce-fullstackgains aTRIAGE_ONLYmode: when set, the pipeline triages the batch and persists the verdict labels (incl. auto-closing proven-obsolete issues) then stops beforeselect, so a run can label the backlog without opening any PR — pairs withFULLSTACK_REASSIGNto re-label the whole backlog for review. The flag is echoed intofetch-candidates/apply-verdictsoutput astriageOnly(transition expressions can't read env) and routes both todone-empty.TRIAGE_BATCH_MAXis now a{{…}}env ref (tunable without re-registering) and its default drops from25to the safer10for the grounded clone-and-verify triage pass.- Run Files panel now has a "Download all" button in the section header, so every Output File across all steps can be saved in one click instead of downloading each row individually.
- Output Files can now be previewed in-browser from the run Files tab and the step-level file list — a "View" action opens a modal that renders markdown, HTML (sandboxed iframe), CSV/TSV (table, first 100 rows), images, SVG (as an image, so embedded scripts can't execute), plain text/JSON/log, and PDF; unsupported types (rtf, xlsx, …) and text files over 5 MiB fall back to the existing download. Bytes come from the same authenticated, workspace-scoped file route — no new download surface.
main
-
Capability map (docs/workflow-capabilities.md) extended to cover previously-undocumented surfaces, each cross-referenced to the schema and the runtime that enforces it: agent autonomy L0–L4 /
confidenceThreshold/fallbackBehavior/reviewloop /allowedToolsinternet access /continueOnError; MCP & tool governance (agentId,mcpRestrictions, Agent Definitions, Tool Catalog, OAuth) and effective-MCP resolution; notifications; richer human steps (thefile-upload/assignment-table/table-editorcomponent registry, completion-kind union, verdictintent/requiresComment, paramoptions/requiredForVerdicts); coworkoutputSchema/outputSchemaRef+ voice tuning; the full trigger set (incl.eventflagged as enum-only/no-router) andtriggerInputfield types; workflow-envelope fields (visibility,roles,preamble,sourceprovenance); and action-handler nuances (spawn 50-child cap, email rate limits, http non-2xx semantics). -
design-workflowskill upgraded: new capability map (docs/workflow-capabilities.md) cross-referencing every executor, action kind, both expression languages, human-step UI, scripts, and models to the source files that define them (drift-guarded by the doc-lint test); a distilledspawn+forEachfan-out tutorial (docs/workflow-examples/11-fan-out-orchestration.wd.json) plus a capability index in the examples README; the skill now syntax-checks (Phase 3b) and behavior-tests (Phase 3c) every generated script, defers commit-SHA pinning to a Phase 4 post-commit step (with agit commit --amendunify path), and corrects the model-list guidance (mediforce model listneeds a deployment +MEDIFORCE_API_KEY; prefer short Claude aliases offline). -
Workflow authoring docs split into a process guide (docs/how-to-create-workflow.md) and a production checklist (docs/workflow-authoring-golden-rules.md), backed by the end-to-end
apps/golden-standard-workflowreference package. A CI doc-lint test re-derives every cross-reference, theexecutor/typeenum tables, the referencedmediforceCLI commands, and the ADR links from source, so the docs fail the build instead of silently drifting #784. -
Agent steps now record
tokenUsage.peakInputTokens— the largest single-turn prompt the model held during a run — and the executor logs a[saturation]line (peak / contextLength = N%) per step. Unlike the summedinputTokens(kept for cost), this is the real context-occupancy signal for sizing agent batches (e.g. how many issues to hand triage at once). Populated from OpenCodestep_finishper-turn token counts.
- Website "How it works" and README "How It Works" sections now describe the current CM0–CM4 Control Mode taxonomy (No agent, Assist, Cowork, Human review, Autonomous agent) instead of the retired L1–L4 autonomy-level labels, with icons and colors matching the workflow designer's step-type picker #882.
mediforce-fullstackagent steps lost their result contract: the prompts told the model to writeresult.jsonto${output_dir}, but the platform maps{output_dir}to the git worktree (/workspace), while the result contract must go to the separate Result Contract Directory (/output). The model wrote/workspace/result.json(committed to the run branch), the engine couldn't find the contract and fell back to raw text, so every JSON-returning step (triage,draft-plan,self-review,revise, …) silently produced no structured output — e.g.triageclassified an issue butapply-verdictsreceived zero verdicts and labelled nothing. All six agent prompts now write to the literal/output/result.json, matching the script steps and the platform's own contract-directory instruction. Verified on staging:triageverdicts now reachapply-verdictsand the issue is labelled.- Pre-assigned human tasks (
assignedTo) whose resolved value is an email are now resolved to the user's uid via the user directory before persisting asassignedUserId, so the task surfaces in the approver's queue (queues match on uid). Previously the email was stored verbatim, matched no user, and the task vanished from every queue — this strandedmediforce-fullstackapproval gates whoseFULLSTACK_REVIEWER_MAPused email values, as the README documented. An email that matches no user now hard-fails the run instead of silently stranding. - Run execution history now shows the correct control-mode badge for agent steps stored without an explicit
autonomyLevel— previously they fell through to "No agent" (CM0) even though an agent was running; they now display as "Autonomous agent" (CM4), which matches the design intent that CM0 is reserved forhuman | script | actionexecutors only.
- Context-saturation logging now covers the
claude-code-agentplugin, not just OpenCode. Peak per-turn context occupancy is computed from the stream's per-turnassistantusage (summinginput_tokens+ cache-read + cache-creation, since caching hides most of the prompt frominput_tokens) and folded into the result event's usage aspeak_input_tokens, sologContextSaturationreports a real peak instead of nothing. Previously only the final turn'susagewas surfaced, which is not the peak — Claude Code steps produced no[saturation]line, leaving batch-size tuning blind. - Workflow-designer cowork chat no longer hangs on its mandatory first step: the
mediforce-mcpserver crashed on startup because@mediforce/platform-corewas an undeclared (phantom) dependency and itsworkflow-examplesloader had no subpath export, solist_workflow_exampleswas silently unavailable (the connection failure is swallowed). Declared the dependency, added the./workflow-examplesexport, and import it via the package specifier. - Postgres workflow definitions now persist imported workflow
sourceprovenance, so get/list calls keep the advertised url/path/commit after GitHub imports. - A malformed or legacy-shaped
source(e.g. the pre-commit{ repo, path, ref }) no longer drops the entire workflow definition on read — the informational provenance is dropped best-effort and the workflow still loads and runs. pnpm testis now fully self-contained — PlaywrightglobalSetupauto-starts the Firebase Auth emulator when absent and runs pending DB migrations, so E2E tests pass without manual pre-flight steps.
- Autonomy levels overhaul across workflow designer and execution history — step-add popover is now a two-step wizard (pick step type, then pick executor: human / agent / script / cowork); step editor shows executor identity and L2/L3/L4 autonomy level instead of the "control mode" abstraction; execution history chips and the agent run table surface the raw autonomy level badge; executor identity in step history renders as
agent:<plugin>; branch icons in the decision diagram updated to chevrons; schema fields (executor,autonomyLevel) unchanged #783.
- CI deploy now targets both
staging.mediforce.aiandcdisc.mediforce.ai— the staging deploy workflow fans out over a server matrix (parallel, per-target cancellation,fail-fast: false). - Canonical workflow-definition schema validation endpoint (
POST /api/workflow-definitions/validate) +mediforce workflow validateCLI command, both running the sameparseWorkflowTemplateschema (cross-field rules included) that registration uses. The workflow-designervalidatestep now calls this endpoint instead of a hand-rolled partial JS reimplementation that drifted from the schema. - Runtime workflow-schema injection for cowork designers — chat and voice workflow-designer sessions now resolve the authorable WorkflowDefinition schema in-process from the running app before the session starts, without a schema-fetch container step or baked voice snapshot.
- Workflow designer now learns the correct schema from 11 CI-tested examples + 6 anti-patterns served via
list_workflow_examplesMCP tool — eliminates stale inline schema that caused script steps to generate with the oldstep.agentconfig instead ofstep.script#787. - Import workflows from git now records the resolved commit SHA as provenance (
source: { url, path, commit }) by resolving the requested ref to an immutable SHA and fetching the file at that SHA; a.wd.jsonthat declares its ownnamespaceimports cleanly (the target namespace wins, matchingworkflow register); the import dialog gains a branch/tag/commit field (defaults to main), an "Import by path" mode that also kicks in when a repo has noindex.jsonmanifest, and no longer pre-selects every browsed workflow. Scope (one-time copy, public GitHub only) documented in docs/how-to/import-from-git.md and ADR-0009. - Task-attachments backend —
BlobStore(filesystem impl) + atask_attachmentstable behind a workspace-scoped repository, with headless upload/list/download/delete routes and a typed client; bytes live on the~/.mediforcevolume, metadata in Postgres, size guarded byMEDIFORCE_ATTACHMENT_MAX_BYTES(ADR-0003 PR2) #765. - Docker image validation at workflow registration — server warns when a referenced image is not found on the platform, CLI deduplicates server vs local warnings, and the workflow editor shows an amber toast on save #734.
- SMTP email provider as alternative to Mailgun — organisations can now use their own SMTP infrastructure instead of Mailgun by setting
SMTP_*env vars;EMAIL_PROVIDERauto-detects or can be set explicitly; includes read-only admin status page,mediforce email statusCLI command, and bootstrap script prompts for initial setup #748. - Run view UX — execution history panel (renamed from "Step Status"), scrollable workflow diagram, active step info (started time, live elapsed timer, executor name/claimer for claimed human tasks)
- Task view — spreadsheet-style table replaces card grid; status column, clickable run links, bulk cancel via batch endpoint, hide-completed toggle. Absolute dates replace relative timestamps in the table.
process × actioncross-grouping (sub-grouping within a process group by action type) is not carried forward in the table layout; each grouping mode renders a flat table. - Process card — step-progress dots rendered against the run's actual definition version instead of the latest definition
- Pre-flight model validation: agent steps with unknown model IDs show an "Unknown model" warning (with Levenshtein-based "did you mean?" suggestions) in the start-run dialog before the run begins.
- Start-run dialog: selecting a different workflow version from the dropdown no longer skips pre-flight checks — the dialog now waits for the new version's definition and validation to load before allowing start.
- Extracted
normaliseModelIdtoplatform-core— was duplicated 5 times across 3 packages. - OpenRouter credits indicator and the start-run low-credit warning now use the effective spendable balance
min(key limit, account credits)(indicator shows a(key limit / credits)breakdown), instead of the key-limit ceiling alone which overstated available funds. - Codex now mirrors Claude's repo-local skill and agent discovery layout via
.codex/skills/*and.codex/agentssymlinks, withAGENTS.mddocumenting how Codex should translate Claude-specific skill tool syntax.
- Firebase Storage removed entirely — the task file-upload UI now uploads, lists, and downloads attachments through the headless attachments API instead of
firebase/storage, and the Storage SDK, emulator,storage.rules, andNEXT_PUBLIC_FIREBASE_STORAGE_BUCKETare gone; the per-byte upload progress bar becomes a per-file spinner (single non-streaming upload) and legacy demo attachments that were never migrated no longer render (ADR-0003 PR4). - Dead uploaded-skills feature — dropped
AgentDefinition.skillFileNames+ theagents.skill_file_namescolumn, the skill-upload UI, and the Firebase Storage skill download from agent identity resolution;skillsDiris now the only skill mechanism (ADR-0003 PR1). - E2E GIF recording: the recording helper, ffmpeg conversion scripts, the
docs/features/FEATURES.mdgallery, andtest:e2e:record/:gifscripts (3 README screenshots kept). UI journey tests stay as normal L4 e2e (page-error tracking kept inhelpers/page-errors.ts).
- Cowork workflow designer can render diagrams again — nothing depended on
@mediforce/mediforce-mcp, so pnpm never linked itsmediforce-mcpbin onto PATH; the chat handler's spawn failed with ENOENT and the MCP tools (includingrender_workflow_diagram) were silently absent, so the designer narrated "let me render the diagram" against a toolset that lacked the tool and the Artifact panel's Preview tab never appeared. Added@mediforce/mediforce-mcpas aplatform-uidependency so the bin links onto the spawning process's PATH. - Cowork chat no longer silently drops large artifacts — the OpenRouter call left
max_tokensat the client default of 4096, so building a multi-step workflow truncated theupdate_artifacttool-call JSON mid-stream,JSON.parsethrew, and the artifact panel stayed empty behind a misleading "Artifact update skipped (parse error)." message. The chat turn now requests 16384 output tokens, the client surfacesfinishReason, and a truncated turn reports an actionable "response truncated at the token limit — build it up across several updates" error to both the user and the model instead of a parse error. render_workflow_diagramno longer rejects in-progress workflows that route purely via stepverdicts— its input schema required a top-leveltransitionsarray even though the renderer already handles its absence, so rendering a verdicts-only draft threw a Zod error;transitionsnow defaults to[].- Cowork MCP tools now work when env templates (
{{SECRET}}) reference workspace secrets —connectMcppasses the resolved secrets toMcpClientManagerso template interpolation succeeds in both local dev and staging #787. - Binary file-download routes (task-attachment blob, run output files, agent output file) now send
X-Content-Type-Options: nosniff— defense-in-depth against MIME-sniffing a user-supplied content-type into an executable type (ADR-0003). - Skill-loading steps no longer intermittently fail with "Skill file not found" from another workflow's cache — the container plugin singleton kept the skills-cache dir in a mutable field that leaked across runs; the path is now derived on demand from the workflow's
externalSkillsRepo. - External skills repo now clones public repos over anonymous HTTPS —
fetchSkillsFromRepono longer forces an SSH clone (which fails in the agent container, where there is nosshbinary) when no auth token is configured; SSH/deploy-key transport is used only for genuinegit@refs. - Deploy-key cache now recovers when the cached path is replaced by a directory (EISDIR); auto-runner stuck-loop errors now surface the underlying step error in
mediforce run get#776. - Bootstrap now generates
REDIS_PASSWORD— staging overlay requires--requirepassbut the variable was missing, causing Redis to crash-loop on fresh deployments #777. repo→external_skills_repomigration applies on staging again — the previous0026/0027pair carried an out-of-order journalwhentimestamp (2025-06-19) that drizzle-kit silently skips on any DB already past it, leaving the new column uncreated and every workflow listing empty. Merged both into a single0026_external_skills_repomigration (column add +repocopy + invalid-value repair) with a correct timestamp.- Image-less build-mode workflow agents now resolve their derived Docker tag before choosing execution mode, so
dockerfile/repo/commitagents run in containers instead of falling back to local host execution. - Workflow editor agent image fields keep custom Docker tag entry available even when discovered local Docker images are listed.
- E2E test setup deletes only the workspace handles it owns (
test,tenant-a,tenant-b, and journey-specific handles) and resets seeded agent definitions — personal namespaces and their registered workflows are no longer wiped when runningpnpm test:e2e, while interrupted MCP-binding journeys no longer dirty the next run. datetimeparams now store UTC ISO strings — scheduled wait steps fire at the user's intended local time instead of being off by the browser-to-UTC offset; the datetime field shows the detected browser timezone (e.g. "Europe/Warsaw (UTC+2)") so users can see exactly what time the server will act on.- Runs stuck at a wait step with no recorded
pauseReasonare now surfaced as errors instead of silently stalling forever: the auto-runner verifies thewaiting_for_timerwrite after it commits and escalates tofailedif the write was lost; the heartbeat sweeps and escalates any survivingpaused/nullorphans; the run view shows an actionable message ("Resume this run to restart from the current step") instead of "Workflow stopped unexpectedly". - Staging deploys now pick up each push to main —
build-images.ymlnow builds nativelinux/amd64 + linux/arm64images via a matrix job on GitHub-hosted ARM64 runners; the Hetzner ARM64 staging server was silently falling back to a stale localbe65eae4build because the amd64-only CI images had no matching manifest forarm64/v8; deploy script pull errors are now visible instead of swallowed #740. scripts/sync-model-rankings.pynow uses OpenRouter's frontend rankings JSON endpoint instead of a brittle private server-action scrape, restoring local model ranking sync.- On-demand preview deploys now work:
/deployon a PR runs a realvercel build+vercel deploy --prebuiltvia the Vercel CLI, replacing the empty-commit[preview]-marker hack that silently no-op'd. - Execution history panel now shows all iterations of looped steps — steps that completed before the current loop revisit (e.g. a timer-wait that ran between two visits to the same decision gate) were previously hidden or incorrectly marked pending due to the steps API returning only the latest execution per step and using a positional definition-order algorithm for status derivation.
- The "Before you start" pre-flight dialog now shows actionable resolution paths per warning instead of a static hint: missing-image warnings link to "Configure build source" (workflow editor), "Build manually" (Docker setup tutorial), and "Contact admin" (mailto: to the namespace owner); missing-secret warnings deep-link directly to the Secrets panel for the affected key. Backed by a new
GET /api/namespaces/:handle/admin-contactendpoint that resolves the earliest owner's email #312. - Task view — spreadsheet-style table replaces card grid; status column, clickable run links, bulk cancel via batch endpoint, hide-completed toggle
- Process card — step-progress dots rendered against the run's actual definition version instead of the latest definition
- StepExecutor strategy pattern (ADR-0008) — agent and script steps now execute through separate
StepExecutorstrategies (AgentStepExecutor,ScriptStepExecutor) instead of a monolithicAgentRunnerwithisScriptbranches. Each strategy owns its full lifecycle (run, audit, advance/review, cost tracking).AgentPlugininterface renamed toStepExecutorPluginto reflect that scripts and Databricks jobs are peers, not special-cased agents:
databricks-jobplugin — workflow steps can trigger an existing Databricks job (jobs/run-now+ polling) and route transitions on the JSON the notebook exits with; secretsDATABRICKS_HOST/DATABRICKS_TOKEN,${steps.*}param interpolation, single-task jobs in v1. Includesscripts/databricks-spike.pyfor manual verification against a real workspace.- Agent-produced files are no longer lost — everything an agent step leaves in
/outputis preserved as Output Files on the run branch (.mediforce/output/<stepId>/, ADR-0007) and is reviewable and downloadable:- API:
GET /api/runs/<runId>/fileslists a run's files;GET /api/runs/<runId>/files/<path>serves the bytes (binary-safe, RFC 6266 attachment);mediforce.runs.listOutputFiles/downloadOutputFileclient methods. - UI: "Files" card on the run detail page (grouped by step, hidden when empty) + per-step paperclip chips expanding to that step's files.
- CLI:
mediforce run files <runId>andmediforce run download <runId> [path] [-o dir].
- API:
- OpenTelemetry tracing for agent runs (ADR-0007 layer 1) —
agent-runtimeemits OTel spans:mediforce.agent.run(root, with full workflow correlation attributes: namespace, workflow name+version, stepId, agentRunId, processInstanceId, autonomy level) andopenrouter.chat.completion(GenAI semantic conventions: request/response model, token usage). Content captured only whenMEDIFORCE_OTEL_CAPTURE_CONTENT=true: step input / envelope result on agent-run spans (OpenInferenceinput.value/output.value, rendered by Phoenix), prompt/completion text on LLM spans. Export is opt-in viaOTEL_EXPORTER_OTLP_ENDPOINT(registered in platform-ui instrumentation); only@mediforce/*spans are exported (Next.js auto-instrumentation noise filtered out,MEDIFORCE_OTEL_EXPORT_ALL_SPANS=trueto keep it); Phoenix added todocker-compose.ymlas the dev trace viewer on:6006. - Dry run mode — workflow runs can be tagged as dry runs for testing. Full execution with a
dryRunflag, spawned children inherit it. CLI:run start --dry-run,run watch,run logs,task complete. UI: violet badge on run list/detail, All/Production/Dry Runs filter, "Dry Run" button in start-run dialog. - Dry run feedback loop in Workflow Designer — the cowork agent can now test workflow definitions mid-conversation via 6 new MCP tools (
dry_run_workflow,get_run_status,list_run_tasks,complete_task,get_run_logs,list_models). Agent starts a dry run, polls progress, pauses on human steps with a proposed payload, analyzes results, and iterates. Namespace auto-injected from run context #684. - ADR-0007: LLM evaluation & observability — four-layer model (traces → scores → eval datasets → eval runs), hybrid system of record (traces external via OpenTelemetry GenAI conventions, scores/datasets/eval runs as platform entities), Agent Run as the unit of evaluation, per-deployment content-capture switch. Glossary gains Trace / Score / Eval Dataset / Eval Run.
verdict-with-paramstask kind lets a single human step collect structured param values and a verdict together — previously required two separate steps #658. IncludesParamVerdictViewcomponent anddatetimeparam type support.- Merged workflow designer — consolidated
workflow-designer(v21),workflow-designer-2(v3), andcowork-workflow-designer(v5) into a single cowork-basedworkflow-designerwith create/edit mode support, live validation, HTML diagram previews, and a rich system prompt covering the full WorkflowDefinition schema.- Cowork sessions now validate artifacts live on every
update_artifactcall (wires up the previously unusedvalidateOutputSchema), with results shown in the artifact panel and enforced as a gate on finalize. - New
update_presentationbuilt-in tool lets cowork agents push HTML previews rendered in a sandboxed iframe tab alongside the JSON tree explorer. platform-mcpstdio server exposesrender_workflow_diagram— a deterministic HTML renderer matching the platform's visual language (step type colors, executor badges, verdict pills).POST /api/render/workflow-diagramREST endpoint (same renderer).- Artifact panel: collapsible JSON tree explorer replaces raw
JSON.stringify; tabbed Data/Preview view. - Built-in tool calls (
update_artifact,update_presentation) now persist as live tool turns visible in the cowork chat UI. - Postgres migration 0018:
validation_result(jsonb) +presentation(text) columns oncowork_sessions.
- Cowork sessions now validate artifacts live on every
- Workflows can now be imported from a public GitHub repo as a one-time copy:
mediforce workflow import --repo <url> --path <wd.json-path> --namespace <ns>CLI command, a manifest-browser dialog in the workspace toolbar (fetchesindex.json, multi-select, progress bar), andPOST /api/workflow-definitions/import+GET /api/workflow-definitions/manifestserver-side handlers — imported definitions carrysource.{repo,path,ref}provenance metadata #561.
- Breaking (schema + data migration 0023): deterministic script-step config moved from
step.agentto a typedstep.scriptkey;agent/autonomyLevel/coworkare now rejected onexecutor: 'script'steps and audit events recordexecutorType: 'script'(forward-only). Stored workflow definitions are rewritten in-place by Postgres migration0023_script_step_config— it must ship in the same release, because definitions are re-parsed on read.
- Script container
runtime: "python"now invokespython3— previously failed on golden image (and any image without apythonsymlink) withexec: "python": not found#675. - Queued (BullMQ) Docker execution no longer corrupts binary files (PDF, XLSX, ZIP) and now preserves nested output directories — file payloads cross Redis as base64 keyed by POSIX relative path, matching local-mode behaviour.
- Run cost under-reported for cached agent runs — the container-agent token extractor read only
input_tokens/output_tokensfrom the CLI result event and droppedcache_read_input_tokensandcache_creation_input_tokens, so prompt-cached runs (where cache reads dominate input) showed costs many times lower than OpenRouter actually charged. Cache-creation tokens now fold intoinputTokensand cache-read tokens are tracked ascachedInputTokens, priced at the registrycacheReadrate (falling back to the input rate). (#654) - Completed param-only tasks (e.g. data-entry steps with no verdicts) no longer show a "Waiting for step output... Verdict buttons will be enabled..." box — that message only made sense for review steps, and the panel now renders nothing when the previous step produced no output to show #672.
- Human steps in the run view are now first-class: a waiting step shows the full task UI (verdict buttons, params, claim/access banner); a completed human step shows the read-only task body alongside execution input/output — the separate task detail page is gone. Task inbox renamed "New actions" → "Human actions"; all inbox links point directly to the step view.
/tasks/{id}deep-links (emails, bookmarks) redirect transparently to the owning step. - GETTING-STARTED now covers two first-run blockers that were previously only discoverable by hitting the error: setting
MEDIFORCE_API_KEYfor the CLI (must matchPLATFORM_API_KEY), and building localscript-executor images (mediforce-golden-image,mediforce-node) viascripts/rebuild-docker-images.shbefore running workflows withscriptsteps #670. - Deploys can no longer leave orphaned containers behind —
deploy.shanddeploy-staging.shpass--remove-orphanstodocker compose up, so containers from since-removed compose services die on deploy instead of silently consuming shared queues with weeks-old code (root cause of the staging incident where a stale agent worker wrote base64script.mjspayloads) #682.
pnpm dev/pnpm dev:queuenow preflight infra viascripts/dev-infra.py: it checks Docker is installed, the daemon is running, and the Compose v2 plugin exists, then brings up Postgres (and Redis) with--waitso the DB is healthy before migrations run. Missing prerequisites now stop with an actionable message (e.g.sudo apt install docker-compose-v2) instead of silently booting the app against a non-existent database and surfacingECONNREFUSED 127.0.0.1:5432mid-request. Prerequisites + troubleshooting documented in GETTING-STARTED.md / dev-quickref.- Removed redundant
dev:postgresscript (usepnpm dev);dev:no-dockernow setsDATABASE_URLso it boots post-#534; doc cleanup (ADR-0001 Accepted,STORAGE_BACKENDrefs scrubbed, headless-migration docs marked Completed).
- Workspace-selection page no longer renders a blank picker when
/api/users/mefails —useAllUserNamespacessurfacesisError/errorand the page shows an explicit error (with a Try-again) for an errored or empty bundle, redirects straight through for a single workspace, and only shows the picker for two or more. Previously a 500 was swallowed into an empty namespace list and rendered as "Choose a workspace" with no cards. - Forced password-change, workspace bio/profile edits, agent MCP-binding writes, and namespace member/delete mutations no longer 500 — their audit events now carry an FK-valid
workspace(the acting/affected namespace) instead of throwing against the Postgres NOT NULL column;namespace.deletedemits before the cascade and is anchored to the owner's surviving personal namespace. #534 - Cutover script maps agent-run
duration_ms+ token counts from the correct envelope keys (duration_ms,tokenUsage.{inputTokens,outputTokens}), so migrated runs keep their duration/token data. #534 - Run History /
mediforce agent-run listno longer 400 on null-envelope runs stored as'{}'instead of SQL NULL (read tolerates it, write stores NULL, migration0015backfills). #534 - Soft-deleted workflows no longer leak into the catalog list (regression from the Firestore→REST cutover dropping the UI's
!deletedfilter). #534 PostgresAgentRunRepository.create()now upserts (onConflictDoUpdate) on a repeated run id — the runner writes the same id twice (running → terminal) and the plain insert threwagent_runs_pkey, a regression from Firestore.set(). #617- Staging deploy broke after #572: the
migrateinit-container stage derivedFROM builder, so its arg-less build re-rannext buildwhose/workspaces/newprerender failed withauth/invalid-api-key(Firebase build args reach only theplatform-uiservice). Migrate stage now derives fromdeps+ source, skipping the Next build entirely. - Flaky
forced-password-change.journey.ts(#578) —updatePasswordrevokes the session's ID token, so the forced-reset flow now re-authenticates andclearMustChangePasswordretries on a revoked-token 401 (forcing a token refresh) before clearing the gate, instead of letting the stale-token 401 fail the gate closed and bounce the user back to/change-password. - Polling hooks now stop on 4xx (
useProcessInstances, the task queues, and the cowork-session list/by-instance hooks) via a sharedstopRetryOn4xxutil and arefetchIntervalerror gate — previously a 403/404 caused runaway API/Postgres load on every tick, burning budget (post-#591 cost regression). useProcessNameMapnow scopes itsruns.listpoll to the active workspace handle (namespace: handle) instead of fetching across every visible workspace every 5 s — a cross-workspace data-scope leak.useAgentEventspolls incrementally via a newafterSequencecursor onprocesses.agentEventsinstead of re-reading the entireagentEventssubcollection every 1.5 s — the Phase-4 cutover lostonSnapshotdeltas and turned a chatty open run into ~20k Firestore reads/min.- Moved
validateEnv(and itsprocess.exit) into a node-onlyinstrumentation-node.ts, dynamic-imported from theNEXT_RUNTIME === 'nodejs'branch, so Turbopack no longer parsesprocess.exitfor the Edge build — silences theprocess.exit ... not supported in the Edge Runtimewarning that flooded the dev log on every compile. StepParamSchema.typewidened from a strict enum toz.string().min(1).default('string')so human-task GETs no longer 400 when the param uses a widget hint the UI already renders (e.g.textarea,multiselect). Non-stringtypestill fails parsing — only the enum was the bug. Route adapter alsoconsole.errors handler ZodErrors so similar mismatches are debuggable from the server log.CoworkSessionschema now defaultsagent→chatandvoiceConfig→nullso legacy chat-only sessions (predating the voice fields) survive the cutover's full-scan Zod reads instead of 400ing the wholecowork.list.findMostRecentActivedrops itscreatedAtorderBy (picks the latest in memory) so the by-instance lookup no longer needs an undeployed composite index — fixes the 500 that hid the run page's "Open cowork" button.
- ADR-0001 final cutover —
users/{uid}profile migrated to Postgresuser_profiles(mustChangePasswordonly); invite + forced-password writes now route throughPostgresUserProfileRepository. #534 - Firestore fully removed — only Firebase Auth + Storage remain. Dropped the Firestore repos, admin/client handles, rules/indexes, and the emulator from every config. #534
- Plain
pnpm devnow boots the full local stack (Postgres up + migrate + UI);dev:localremoved,dev:no-dockerstays docker-free,dev:queueopts into Redis/BullMQ. - Postgres repo row-mappers strip nullable columns once at the boundary via new
compact()/parseRow()helpers inplatform-coreinstead of per-field conditional spreads. #614 useProcessNameMapnow reads a projectedGET /api/runs/names(mediforce.runs.listNames,{ id, definitionName }only) on a 30 s cadence instead of fetching up to 10k fullProcessInstancedocs every 5 s — cuts the ~24 s/request payload bloat (#588).- Workspace-home run counts + 3-run preview now come from a server-side
runSummaryonworkflows.listinstead of polling up to 10k runs every 5 s and grouping client-side;includeCompletedRunsmirrors the "show completed" toggle. - Phase 4 done (PR-final #591) — zero
firebase/firestoreinplatform-ui(api-boundaries.test.tstripwire); PG PR2 (#534) unblocked. - ADR-0001 — server data layer migrated to Postgres; Firestore admin SDK deleted (14 repos, L2 parity + L3 API E2E).
STORAGE_BACKENDflag removed (Postgres is the only backend). Cutover script + verifier + operator checklist inscripts/migrate-firestore-to-postgres/. #534. - Dev ergonomics + Vercel preview gating (#585) —
pnpm devhonorsPORT, E2E scripts honorE2E_PORT(defaults9003/9007unchanged), so two worktrees can run side-by-side without colliding. Rootdev*scripts auto-runpnpm install --prefer-offline --silent(~2s no-op when up to date) so fresh worktrees boot without remembering to install. Vercel preview builds now skip on every non-main push; trigger one on demand by posting/deploy-previewon a PR — a workflow pushes an empty[preview]commit that the newvercel.jsonignoreCommand allow-lists. Sidebar SHA badge picks up the branch name on dev (cherry-picked from #583). - ADR-0001: Drizzle migrations run as a compose init container (
migrateservice, build targetmigrate) instead of an in-app boot step. Dropspackages/platform-infra/scripts/migrate.mjsand theoutputFileTracingIncludesworkaround inpackages/platform-ui/next.config.mjs(PR #570 hotfix for stagingERR_MODULE_NOT_FOUND). - Phase 4 PR5 — cowork session + chat moved to react-query:
[handle]/cowork/[sessionId]/page.tsxno longer importsfirebase/firestore; chat send is auseMutationwith optimistic prepend, cancellation-race protection, and a 1 s polling cadence flip while in-flight.ChatCoworkSessionOutputgains additivesession+turnsfields so the UI can replace optimistic state without a follow-up GET. Newmediforce cowork {get, get-by-instance, chat}CLI subcommands. - Phase 4 PR4.5 — firestore residual sweep so PR-final is delete-only. New
UserProfileRepository(scope.userProfiles) reading/writingusers/{uid}.mustChangePassword;GetMeOutput.userextended withmustChangePassword: boolean(defaultfalse);POST /api/users/me/clear-must-change-password+mediforce users clear-must-change-passwordCLI (audituser.password_change_acknowledged). Five settings-page mutations migrated:PATCH /api/namespaces/:handle(namespace.updated;biois two-state —undefinedleaves it untouched, any string overwrites incl. empty),DELETE /api/namespaces/:handle(namespace.deleted, cascade viaNamespaceRepository.deleteNamespaceCascade),POST /api/namespaces/:handle/leave(namespace.member_left; owner blocked →precondition_failed),DELETE /api/namespaces/:handle/members/:uid(namespace.member_removed, atomic viaremoveMemberWithOrganizations),PATCH /api/namespaces/:handle/members/:uid(namespace.member_role_changed, owner-only).auth-context.tsx,app/(app)/[handle]/settings/page.tsx, and the namespace/user sections ofapp/(app)/[handle]/page.tsxrewired throughuseUserMe/useNamespace/ the newuse-namespace-mutationshooks. New CLI:mediforce namespace update|delete|leave|remove-member|set-member-role. Personal-namespace pages now treat the org-list panel + member-avatar profile links as owner-only (was: visible to any signed-in viewer) — deliberate privacy tightening. - Phase 4 PR4 — namespaces + identity bundle migrated off
firebase/firestore. NewGET /api/users/me(lazy personal-namespace bootstrap +user.personal_namespace_createdaudit),GET /api/namespaces/:handle,POST /api/namespaces(namespace.createdaudit, atomic viaNamespaceRepository.createNamespaceWithOwner).auth-context.tsx,use-namespace,use-namespace-role,use-all-user-namespacesrewired (selectors overuseUserMe());use-user-namespace.tsdeleted in favour ofusePersonalNamespace.workspaces/newusesuseMutationwith list-affecting optimistic prepend;[handle]/settingsdrops the membersonSnapshot. NewHandleSchemawrite-boundary constant. New CLI:mediforce users me,mediforce namespace get|create. - Phase 4 PR2 — agent-runs + monitoring migrated off
firebase/firestore. New headless endpointsGET /api/agent-runs(cursor-paginated,runId/stepIdfilters),GET /api/agent-runs/:agentRunId,GET /api/namespaces/:handle/monitoring/summary(all-timerunning/paused/completed/failedper status, mirroring the pre-PR2useMonitoringDatahook this endpoint replaced).useAgentRuns/useAgentRunsForStep/useAgentRunrewired to react-query (STANDARD LIVE 5 s, CRITICAL LIVE 1.5 s for detail);useMonitoringSummaryruns NICE LIVE 30 s with focus-refetch. Newmediforce agent-runCLI subcommand.- Agent-run workspace gating bypassed for parity with the pre-PR2 Firestore subscription; per-row gate restore tracked in #588.
FirestoreProcessInstanceRepository.listAllskips per-row parse so legacy docs no longer 400 the monitoring summary.- Monitoring summary perf:
ListInstancesOptions.namespacepushes the workspace filter into Firestore (new(namespace, deleted, createdAt DESC)composite index), and a bulkHumanTaskRepository.getByInstanceIdscollapses the per-run tasks N+1 into one chunkedwhere('processInstanceId','in', …)fan-out. ~11 s → ~1.7 s on a 113-run workspace in dev.
- Phase 4 PR3 — runs / processes / audit domains migrated to react-query (ADR-0006);
mediforce.tasks.listgains caller-scope axis (no params = my workspace queue);runs.listadds anamespacefilter and now returns the fullProcessInstanceshape (PRD §9 read-path convergence). - Phase 4 PR1 — react-query foundation (ADR-0006) + tasks domain migrated off
firebase/firestore(task detail page, claim button optimistic, role-scoped queues). Newmediforce taskCLI subcommand. - ADR-0005 Phase 2.5 — 18 endpoints (workflow-definition mutations, agents, secret read companions, processes archive/bulk) migrated to headless handlers;
actions/processes.tsandactions/definitions.tsdeleted.transferWorkflownow gates both source and target namespace,deleteWorkflowaudit actor fixed, and archive / setDefault / register / copy / setVisibility now emit audit events. - ADR-0005 Phase 2.5 — workflow-secret value reveal + atomic bulk save migrated to headless handlers (
getWorkflowSecretsFull,saveWorkflowSecrets) behindGET/PUT /api/workflow-secrets/values;getWorkflowSecrets/saveWorkflowSecretsServer Actions deleted; UI usesmediforce.workflowSecrets.values()/.save(); new audit actionsworkflow_secret.values_revealedandworkflow_secret.bulk_saved. - ADR-0005 Phase 2.6 — six admin/users endpoints (oauth-providers, tool-catalog, docker-images, users/members, users/invite, users/resend-invite) migrated to headless handlers; role-gate plumbing +
PLATFORM_*_API_KEYcollapsed (closes #218). - ADR-0005 Phase 3.1 — cowork endpoints migrated to headless handlers;
app/actions/cowork.tsand the dead/messageSSE route deleted; streaming overhaul deferred to #516. - Decompose
tasks/complete-task.ts(#528) — Phase 3 god-function split into helpers; main handler reads as short recipe. packages/platform-ui/src/middleware.tsrenamed toproxy.ts(exportmiddleware→proxy) per Next.js 16's middleware-to-proxy convention. Silences the⚠ The "middleware" file convention is deprecatedwarning that fired on every dev compile. Matcher and auth behaviour unchanged; integration tests renamed in lockstep.- ADR-0005 Phase 3 — kick-driven mutations migrated to headless handlers.
tasks/complete(5-variant discriminated union body:kind∈verdict | params | upload | assignment | rows),processes/resume,processes/:id/steps/:stepId/retry,cron/heartbeat, andPOST /api/processesnow all run as(input, scope) => outputhandlers. Heavylib/resolve-task.tsbody (481 LOC) folded into a newWorkflowEngine.completeHumanTaskso the handler stays cancel-run-shaped (workspace gate → engine call → audit + kick). NewRunKickerabstraction (scope.system.runKicker) replaces eight inlinefetch('/api/processes/:id/run')self-fetches with a single prod impl (createHttpSelfFetchRunKicker) + a test spy (noopRunKicker).POST /api/processesresponse shape breaks from{ instanceId, status }to entity-echo{ run: WorkflowRun }per ADR-0005 §5; UI (start-run-button.tsx) + CLI (mediforce run start) + E2E suites updated in the same PR. Five Server Actions deleted (completeTask× 4 verdict/params/upload/assignment variants +completeTableEditorTask;startWorkflowRun,resumeProcessRun,retryFailedStep);POST /api/tasks/:taskId/resolveroute deleted (UI-unused per audit). ADR-0005 §7 closed audit-action set extended withinstance.retried,cron.trigger.fired,process.resumed_after_task. New CLI: typed client methods onmediforce.tasks.complete,mediforce.runs.resume,mediforce.runs.retryStep,mediforce.cron.heartbeat. Internalprocesses/:id/advance+processes/:id/runstay inline (future BullMQ executor ADR migrates the auto-runner loop).
-
ADR-0001 tracer-bullet (Firestore → Postgres):
tool-catalogis the first repository with a Postgres backend behindSTORAGE_BACKEND=postgres. Newpackages/platform-infra/src/postgres/houses the Drizzle schema (tool_catalog_entrieskeyed on composite(workspace, id)), thePostgresToolCatalogRepositoryimplementation, the forward-only SQL migration runner, and a sharedToolCatalogRepositorycontract that both the in-memory double and Postgres backend satisfy (InMemoryToolCatalogRepositorynow validates payloads viaToolCatalogEntrySchemafor parity).docker-compose.ymlships apostgres:16service; CI gains apostgres-testsjob that exercises the Postgres branch via aservices:Postgres container. Firestore remains the default backend; the flag scopes to tool-catalog until each follow-up PR migrates its own repository. -
Pre-assigned human tasks (#521) — a
humanworkflow step can declareassignedTo(e.g."${triggerPayload.userId}"); the auto-runner interpolates it against the run's sources and creates the taskclaimedand pinned to thatassignedUserIdinstead of open to the whole role. AnassignedTothat resolves to nothing fails the run rather than silently falling back to an open task;assignedToon a non-human step is rejected at WD validation. The personal task queue (useMyTasks) now scopes assigned tasks to their owner — unassigned tasks stay role-wide. -
Workflow
waitaction kind (#521) — pauses a run for a fixeddurationor until adeadline, with an optional early-exitcondition. The handler returns a__waitsentinel that the auto-runner turns into apaused/waiting_for_timerinstance; thecron/heartbeatsweep andPOST /api/processes/:id/resume-waitresume eligible runs. The wait step execution follows arunning → paused → completedlifecycle — its resolved{ resumeReason, waitedSeconds, resolvedAt }output is written once on resume, never the raw sentinel. -
ADR-0001 tracer-bullet (Firestore → Postgres):
tool-catalogis the first repository with a Postgres backend behindSTORAGE_BACKEND=postgres. Newpackages/platform-infra/src/postgres/houses the Drizzle schema (tool_catalog_entrieskeyed on composite(workspace, id)), thePostgresToolCatalogRepositoryimplementation, the forward-only SQL migration runner, and a sharedToolCatalogRepositorycontract that both the in-memory double and Postgres backend satisfy (InMemoryToolCatalogRepositorynow validates payloads viaToolCatalogEntrySchemafor parity).docker-compose.ymlships apostgres:16service; CI gains apostgres-testsjob that exercises the Postgres branch via aservices:Postgres container. Firestore remains the default backend; the flag scopes to tool-catalog until each follow-up PR migrates its own repository. -
Backlog triage workflow (
apps/backlog-triage/) — fetches open GitHub issues, an LLM suggests assignments (human or AI workflow) with priority and rationale, a triager reviews and edits in a table, then dispatch PATCHes GitHub and POSTs the Mediforce process-start API for agent delegations. -
assignment-tablehuman-task UI for many-to-one item assignment with per-row assignee/priority/note. Items flow viatask.optionsfrom the previous step; the assignee allowlist and labels live intask.ui.config. -
Generic
table-editorhuman-task UI (#469) — schema-driven columns (static|single-select|multi-select|text) declared intask.ui.config.columns, one form pertask.options[i]row, pre-filled fromoption.suggestion[columnId], withallowEmpty:falsesingle-selects gating submit. Emits a stable{ rows: [{ itemId, values: { <columnId>: <value> } }] }map (completeTableEditorTask→resolve-task).assignment-tableis now a thin back-compat shim that builds an equivalent column config internally, so existing workflows keep working with zero WD changes.backlog-triagetags issues in-platform (#469): thetag-issuesstep now renders atable-editor(pick a category + priority per untagged issue) and a newapply-tagsscript POSTs the labels to GitHub — creating any label the repo lacks — loopingtag-issues → apply-tags → fetch-backloguntil every issue carries a category. Replaces the old "open each issue on GitHub, tag it, come back and re-check" round-trip;check-tagsnow hands the untagged issues straight to the table instead of writing a markdown checklist.
-
Markdown presentation support for script/agent steps (#468). Scripts and agents can now write
/output/presentation.mdinstead of (or alongside)/output/presentation.html; the platform reads MD first and renders it viareact-markdown+remark-gfm+rehype-sanitizeinside Tailwindprosetypography. Cuts inline-script preview boilerplate from ~20 lines of hand-rolled HTML and inline styles to a 1-line bullet template, and unifies typography across workflows. Legacy raw-stringpresentationenvelopes auto-coerce to{kind:'html', content}— no Firestore migration. Iframe sandbox stays for the cases that genuinely need JS / interactivity.apps/backlog-triagecheck-tagsmigrated as the reference example. -
ADR-0004 + PLAN-0004 implementation (Phase 1.7 of the headless-API migration): scoped data-access authorization wrapper layer in
packages/platform-api/src/repositories/. Every API handler now accepts aCallerScopeinstead of raw repositories; 13Authorized<Entity>Repositorywrappers enforce workspace membership + WorkflowDefinition visibility on every read/write. Soft-delete filtering stays at the storage layer (lands with ADR-0001'sWorkspaceScopedRepository). The 10 Phase 1 GET handlers from #450 lose their inlinecallerCanAccessguards (now in the wrapper). The static guard becomesno-raw-repo-imports.test.ts(structural — handlers cannot import raw repositories);auth-coverage.test.tsremoved (#458, #463).- Code-review tightening on #463:
AuthorizedWorkflowDefinitionRepository.getLatestVersionis now visibility-gated (no foreign-namespace private-WD enumeration);AuthorizedWorkflowRunRepository.updateassertsnamespaceimmutable and blocks non-systemdeletedpatches (thegetByIdgate alone would let a member smuggle a run across workspaces);getModelthrowsNotFoundErrorinstead of bareErrorso the route adapter maps it to 404.
- Code-review tightening on #463:
-
@mediforce/platform-api/repositoriessubpath export:CallerScope,createCallerScope,AuthorizedRepositorybase, and per-entity wrappers. -
@mediforce/platform-api/testingsubpath export:createTestScope,userCaller— builds a realCallerScopefrom in-memory repos for handler tests downstream ofplatform-api.
- CLI migrated to the
cittylibrary and a thindefineCommandwrapper inpackages/cli/src/define-command.ts. Every subcommand (23 total) now declares its arg shape once; the wrapper handlesparseArgs,--help, missing/extra positionals,resolveConfig+Mediforceclient construction,--jsonmode,formatCliErrorenvelope, and exit-code mapping.cli.tsdispatch is a smallTREEmap with auto-generated branch help (mediforce workflow --help); the long if-ladder is gone. Helpers: typedenumArg(['a','b'] as const)(narrows citty'sEnumArgDef.options: string[]widening),CommandFntype, a TREE smoke test (packages/cli/src/__tests__/cli-tree-smoke.test.ts) that exercises--helpon every leaf. Test phrasings updated to citty's native output (USAGE …,Missing required positional argument: RUNID,Missing required argument: --workflow) — strictly better than the historic hand-rolled strings. Net-1726LOC acrosspackages/cli/. Addscittydep, +25 tests, 183/183 green. - ADR-0005 Phase 2 PR2 (closes Phase 2):
POST /api/processes/:instanceId/cancelmigrated to the headless handler shape. NewcancelRunhandler reusesscope.runs.update()(state-machine in handler, matches PR1's deviation); response is entity-echoed{ run: WorkflowRun }replacing the prior{ instanceId, status }. Contract symbols useRunvocabulary per ADR-0001 (CancelRunInputSchema { runId }); URL path keeps the legacyprocesses/:instanceIdsegment until a coordinated URL rename phase, the adapter translatesparams.instanceId→runId.mediforce.runs.cancel({ runId, reason? })available on the typed client + new CLI commandmediforce run cancel <runId> [--reason <text>]. Audit emission preserved bit-for-bit viascope.system.audit.appendbridge per ADR-0005 §7 (actioninstance.cancelledmatchesworkflow-engine'sinstance.*family and the legacybulkCancelProcessRunsemit; a repo-wideinstance.*/process.*→run.*rename ships as its own ADR pass). UI callers (process-detail.tsx,agent-escalated-banner.tsx) moved off thecancelProcessRunServer Action. Dead code removed:cancelProcessRunaction,UnclaimButtoncomponent +unclaimTaskaction (zero callers in source). Complete/resume/resolve reclassified to Phase 3 because all three depend on the still-undecided orchestration-kick mechanism (#499). - ADR-0005 Phase 2 PR1: typed error envelope +
tasks/claimmigration.HandlerErrorin@mediforce/platform-apinow carriescode: ApiErrorCode+details?: unknown; existingNotFoundError/ForbiddenErrorsubclasses get the code stashed in their constructors, andPreconditionFailedErroris added for the claim handler's state-machine throw. Other codes (unauthorized, validation, conflict, rate_limited) throw via the baseHandlerErroruntil a real throw site arrives.createRouteAdaptercatchesHandlerErroronce and emits the ADR-0005 §1 envelope{ error: { code, message, details? } }across every route (Phase 1 + 1.5 reads retroactively typed too). Client-sideApiErrorgainscode/detailsfields parsed from the envelope (existingstatus/message/bodyunchanged). NewPOST /api/tasks/:taskId/claimhandler (pure(input, scope) => { task }), newloadOr404helper, newmediforce.tasks.claim()client method.claimTaskServer Action deleted;claim-button.tsxcalls the typed client. Audit emission preserved bit-for-bit viascope.system.audit.append({...})(handler-resident bridge per ADR-0005 §7). - Phase 1.5 of the headless-API migration: seven hybrid endpoints (
GET /api/runs,GET /api/runs/:runId,GET|PUT|DELETE /api/workflow-secrets,GET /api/system/docker-info,GET /api/system/openrouter-credits) now compose throughcreateRouteAdapter+ scoped handlers in@mediforce/platform-api/handlers, eliminating their inlinecallerCanAccessguards.GET /api/runs/:runIdswitches from 403 → 404 for foreign-workspace ids (anti-enumeration), andGET /api/workflow-secretssoft-fails to{keys: []}instead of 403 for non-members. Docker-info stays@public-handler(every authenticated user fetches it). #480. - Human-task body rendering now routes through a small registry (
task-body-registry.tsx) keyed onui.component, with file-upload/selection/params/verdict extracted into focused view files. Adding a new task body kind is a one-line registry entry. - Workflow engine copies
prevOutput.optionsonto the next human task regardless of whether the next step declaresselection, so non-selection components (likeassignment-table) consume the items array. Selection-style steps keep the min-count validation; existing apps unaffected. - Contract dedup + API naming alignment with CONTEXT.md. Deleted the redundant
packages/platform-api/src/contract/definitions.tsintroduced in #450 — itsAgentDefinition*/WorkflowDefinition*schemas duplicatedagents.ts/workflows.tsshape-for-shape. SDK clientmediforce.agentDefinitions.*andmediforce.workflowDefinitions.*namespaces dropped;mediforce.agents.*andmediforce.workflows.*are the only surface. Renamedhandlers/definitions/→handlers/workflows/and handlerslistWorkflowDefinitions/getWorkflowDefinition→listWorkflows/getWorkflow. Unified the URL surface:/api/agent-definitions/*(CRUD +/:id/mcp-servers/*) moved to/api/agents/*and now lives next to the existing/api/agents/:id/oauth/*subtree. CONTEXT.md canonicalisesAgent(no "Definition" suffix), adds aWorkflowumbrella entry around the existingWorkflow Definition, reframesAgent Runas a workflow-execution concept, and adds anAgent vs Agent Definitionflagged-ambiguity note that tracks the pendingAgentDefinitionSchema/AgentDefinitionRepositorysymbol rename. ADR-0001 PLAN now proposes the Postgres table asagents; ADR-0004 carries a post-merge naming note. CLI commands, UI labels, route handler logic, and wire shapes all unchanged. CallerIdentitynow carriesreadonly isSystemActor: boolean. The free functionisSystemActor(caller)was replaced by readingcaller.isSystemActordirectly — one less import, future-proof against the planned'apiKey'discriminator rename (#448).- Storage-layer authorization: raw repository interfaces split into system-actor and namespace-scoped variants (
listAll/listInNamespaces,getById/getByIdInNamespaces, etc.). Wrappers inplatform-api/src/repositories/become thin routers that pick the right method based oncaller.isSystemActor. Filter logic moves from JavaScript wrappers to the storage layer, ready for ADR-0001's Postgres swap to push it down to SQL. - Trivial GET handlers deleted in favour of two generic route helpers —
listAdapterandgetByIdAdapterin@mediforce/platform-api/handlers.getTask,getProcess,getCoworkSession,listAgentDefinitions, andgetAgentDefinitionno longer have handler files; the route wires the helper directly with the contract schema. Handlers now exist only where there is genuine logic (cross-entity load, role/ownership/state, shape transform, or@public-handler). ADR-0004 §Decision-10 codifies the rule. - workflow-designer & workflow-designer-2 agentic steps (
generate-steps,generate-execution-proposals) now run in themediforce-golden-imageDocker container; Claude Code credentials are routed through OpenRouter via the workflowenvblock, since Docker steps don't inherit the host environment and must declare credentials explicitly (#492).
- Duplicate Python migration script
scripts/migrations/migrate_workflow_doc_ids.py— superseded by the TypeScriptpackages/platform-infra/scripts/migrate-workflow-namespacing.tswhich is the canonical version (#424).
packages/cli/src/__tests__/bin-shim.test.ts— spawn-based smoke test that runs the realbin/mediforce.cjsshim throughtsxand asserts exit codes + stderr/stdout for--help, a missing positional, and an unknown command. Catches regressions (ESM resolution,tsxloader, bin shim entry resolution) that the in-processrunClisuite cannot.
- Script-container failures now surface the error the script wrote to
/output/result.json(itserror/errorsfield) when the container exits non-zero with no stdout/stderr — instead of only invocation metadata (no stdout/stderr captured — image=…). The reported reason is also written to the activity log the Step Log panel reads, so e.g. the workflow-designerregister/validatesteps now show the real cause (#546; follow-up to #538). - Workflow designers now default agent steps to the
mediforce-golden-imageDocker image. Generatedclaude-code-agentsteps previously carried noagent.image, so a freshly designed workflow crashed on staging/production with "No Docker image configured" (imageless/local execution only works in dev withALLOW_LOCAL_AGENTS=true). Thegenerate-execution-proposalsskills (workflow-designer,workflow-designer-2) and thecoworkdesigner prompt now set the default image, omitting it only when the user explicitly asks for local execution. - Script-container step failures now surface a usable reason instead of
Script container failed (exit code 1): no output. When the container produces no stdout/stderr, the diagnostic (image, command, env var names — including the injectedRUN_ID/STEP_IDand (for workflow runs)MEDIFORCE_RUN_NAMESPACE— andinput.jsonsize) is written into the activity log the Step Log panel reads, so a fast no-output failure no longer sticks on "Initializing log…". Container exitcode/signalis emitted as a status event on every run, and theexit code/killed byformatting (with the SIGTERM→timeout hint) is now a sharedformatExitInfohelper reused by the agent and script container plugins (#538). - Workflow designer registers generated workflows in the run's own namespace instead of a hardcoded
defaultorg (#536). Thescript-containerplugin now injects the run's namespace asMEDIFORCE_RUN_NAMESPACE(alongsideRUN_ID/STEP_ID); all four designer register scripts read it.workflow-designerno longer falls back todefault, the voice/cowork variants stop omitting the required?namespace=query param (which previously 400'd), andworkflow-designer-2drops its{{NAMESPACE}}secret-template (which threw unless a per-namespaceNAMESPACEsecret was manually configured) in favour of the same auto-injected var. backlog-triagetagging loop no longer whack-a-moles or silently swallows label failures.fetch-backlogpaginates past PRs to a stable working set of the newestlimitreal issues — a single PR-heavy page (GitHub's issues endpoint counts PRs againstper_page) or a merge mid-session used to shift which untagged issues showed up each loop. Andapply-tagsnow surfaces label failures instead of swallowing them: apresentation.mdsummary on partial failures, and on total failure (every row failed) it fails the step (exit 1) with the first error in the run log — so e.g. aGITHUB_TOKENwithout label-write permission shows as a red run instead of an endless re-prompt; partial failures stay non-fatal.check-gif-freshness.pynow evaluates the net PR diff vs base instead of summing per-commit diffs. Add-then-revert within the same PR no longer trips the GIF freshness check (regression surfaced on #481).- PR #172 rebase polish: presentation-only agent outputs now render in review surfaces, report iframes escape stringify fallback data and block network exfiltration with CSP, and local script execution no longer inherits the full server environment.
pnpm dev:mocknow starts local Firebase emulators, seeds demo data, and then boots Next, so the browser no longer falls into offline Firestore/Auth mode on a fresh mock-dev run.- Workspace
Runspage no longer leaks runs from other namespaces the viewer is a member of — list is now filtered to the currenthandle, anduseProcessInstancesrequiresnamespaceso TypeScript catches future regressions compile-time (#424, follow-ups tracked in #447 and #448). - Instance namespace backfill now resolves namespace per instance definition version instead of global latest-by-name, preventing cross-tenant run reassignment during migration (#424).
- Workflow definition storage now scopes definition and metadata keys by namespace, so tenants can register the same workflow name independently (#424).
- Migration script splits the legacy global
workflowMeta/<name>into one doc per owning namespace and stripsdefaultVersionfor tenants that don't own that version, avoiding dangling defaults pointing at non-existent workflow definitions (#424).
- Migration script splits the legacy global
- Phase 1 of the headless-API migration is done — 10 GET endpoints across tasks, processes, definitions, cowork and plugins now sit behind framework-free handlers in
@mediforce/platform-api, with namespace policy enforced inside the handler (every handler signature requires aCallerIdentity, so a missing gate is a code-level decision in review, not a silent regression). Wrapsauditresponse as{ events }and surfaces visibility-denied agent definitions as 404 (anti-enumeration). Supersedes #256, which predated the namespace access control onmainand would have regressed it. - Workflows can now be moved between namespaces — copy + redirect lands you in the target tenant (#359, #370).
- Workflow discovery by Docker image:
GET /api/workflows/by-image— needed to scale step containers without grepping definitions (#377). - Human review steps are no longer binary — each step declares its own verdicts and target transitions (e.g. approve / request changes / escalate), wired end-to-end from schema to UI (#396).
- SDTM rule migration workflow — Vedha's legacy rule definitions ported to CDISC SDTM via a dedicated Mediforce app (#355).
- Validation reports are now reproducible — landing-zone data-validator renders from a study-owned template instead of regenerating HTML each run, with an injection-demo variant for the propose-rules story (#402, #408).
- Two ways to seed the Landing Zone demo on the same Hetzner SFTP host —
demo-console/(FastAPI SPA, operator-facing) anddemo-uploader/(Mediforce workflow with 8-verdict human review) (#407).
- Cowork is now per-workspace billed — the OpenRouter key is read from workspace secrets instead of a global env var; chat textarea auto-grows to fit input (#378, #381, #382).
- Dev setup is no longer a footgun —
pnpm dev:mockboots the app with mocks in one command, every script is named for what it does (dev:test→dev:mock,dev:local→dev:no-docker,dev:ui:queue→dev:queue),pnpm testruns the full pyramid instead of vitest only, andbootstrap-dev.pywith its silentOPENROUTER_API_KEY=fake-…trap is gone in favour of a commented.env.example#421. - Agent output is now consistent across surfaces — step detail and human-task panel share one
AgentOutputDisplay, so L2 auto-runner steps (e.g.interpret-validation) finally show their HTML report without needing an L3 review (#409). - Tests now have a 5-level pyramid with API E2E as the explicit foundation —
e2e/api/is a dedicated Playwright project running real Next + emulators over HTTP (no browser), separated frome2e/ui/(sparse multi-step user journeys only, not "is button visible"). Misleading legacy names cleaned up:src/test/*.test.ts→src/test/integration/,e2e/api/(tier-2 real-LLM) →e2e/external/,e2e/journeys/→e2e/ui/(#413).
- Agent report iframe no longer blows up its host panel — height is capped and
vhclasses inside the report are neutralised (#392). - Mock dev workflows now run seeded agent steps through the mock Claude runtime even when their demo plugin ids are not registered, the model ranking sync helper falls back from
9003to9007forpnpm dev:mock, root API/UI E2E wrappers pass Playwright project flags correctly, andmediforce run startcan target a namespace. - Staging step containers can finally see workspace files — the data dir is persisted at
/var/lib/mediforcewith an identical host bind so docker.sock-spawned containers share the same path (#405). - CLI network failures now explain the unreachable API host, reason, and recovery hints instead of printing raw
TypeError: fetch failed(#397).- Follow-up: local I/O failures (for example
EACCES) are no longer mislabeled as API network errors and now keep their original actionable message (#397). - Follow-up: dual-stack fetch failures now classify
AggregateError.errors[]causes, and staging/remote URLs no longer suggest starting a local dev server (#397).
- Follow-up: local I/O failures (for example
- tailwindcss v4.2.4 (#374), yaml v2.8.4 (#371), pnpm v10.33.2 (#372), fast-xml-parser v5.7.2 (#375), jsdom v29.1.1 (#379), lucide-react v1.14.0 (#380).
- Workflows now have per-namespace visibility and access control — no more leaking definitions across tenants (#346).
- Agents have public/private visibility — fixes the gap where staging couldn't see agents owned by other namespaces (#353).
- Per-step cost in dollars now surfaces in run + step lists, so you can see which step burned the budget (#348).
- Per-namespace OpenRouter credit tracking +
mediforce creditsCLI — each tenant sees its own balance (#349). - Mediforce reviews its own PRs —
pr-reviewer-mediforceworkflow runs AGENTS.md-aware review on every PR (#338).
- Public profile pages are safe for unauth viewers — member-only UI and secrets are hidden, avatar falls back to the linked user photo (#350, #354).
- Public pages no longer need a Firestore client — they call the REST API directly, which unblocks SSR and avoids leaking client config (#358).
- AGENTS.md tightened around dogfooding, RED→GREEN, and self code review — fewer ways for agents to skip the safeguards (#352, #356).