Skip to content

Latest commit

 

History

History
292 lines (249 loc) · 77.9 KB

File metadata and controls

292 lines (249 loc) · 77.9 KB

Changelog

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.


[Unreleased]

Added

  • 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 / --accent design 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 existing namespaces.get / users.me payloads — no new upload/serving route; colors are #rrggbb hex on the same record. All three flow through the existing PATCH /api/namespaces/:handle (empty string clears), and the base64 logo is kept out of the audit snapshot.

Fixed

  • mediforce-fullstack no longer re-triages the same declined (fullstack:manual) issue on every tick: the "re-judge on edit" check compared issue.updated_at against the manual label event, but updated_at is bumped by any activity — including a human adding an unrelated label — which latched issues like #425 into an endless re-triage loop. fetch-candidates now treats an issue as edited only when updated_at outruns 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), and apply-verdicts re-stamps the label when re-declining an already-manual issue so a genuine edit re-judges exactly once instead of looping.
  • Runs stranded in running are 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 /run request), the run is left status: running with no live driver. The heartbeat's sweeps only looked at paused runs (resume waiting_for_timer, fail paused/null-reason orphans), so a stranded running run was invisible to every sweep and sat at its current step forever — e.g. a mediforce-fullstack run wedged at arm-timer for 2h+ while its PR's CI had long since gone green. The heartbeat now also sweeps running runs 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 — /run returns 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.

[2026-07-05]

Added

<<<<<<< fullstack/issue-818-task-assigned-notification

  • task_assigned notifications now dispatch when a human task is created (opt-in via a task_assigned entry in the workflow's notifications), 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's advanceStep (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 shared engine.dispatchTaskAssignedNotification; mirrors the existing agent_escalation dispatch #818. =======
  • mediforce-fullstack gains a TRIAGE_ONLY mode: when set, the pipeline triages the batch and persists the verdict labels (incl. auto-closing proven-obsolete issues) then stops before select, so a run can label the backlog without opening any PR — pairs with FULLSTACK_REASSIGN to re-label the whole backlog for review. The flag is echoed into fetch-candidates / apply-verdicts output as triageOnly (transition expressions can't read env) and routes both to done-empty. TRIAGE_BATCH_MAX is now a {{…}} env ref (tunable without re-registering) and its default drops from 25 to the safer 10 for 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 / review loop / allowedTools internet access / continueOnError; MCP & tool governance (agentId, mcpRestrictions, Agent Definitions, Tool Catalog, OAuth) and effective-MCP resolution; notifications; richer human steps (the file-upload/assignment-table/table-editor component registry, completion-kind union, verdict intent/requiresComment, param options/requiredForVerdicts); cowork outputSchema/outputSchemaRef + voice tuning; the full trigger set (incl. event flagged as enum-only/no-router) and triggerInput field types; workflow-envelope fields (visibility, roles, preamble, source provenance); and action-handler nuances (spawn 50-child cap, email rate limits, http non-2xx semantics).

  • design-workflow skill 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 distilled spawn+forEach fan-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 a git commit --amend unify path), and corrects the model-list guidance (mediforce model list needs 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-workflow reference package. A CI doc-lint test re-derives every cross-reference, the executor/type enum tables, the referenced mediforce CLI 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 summed inputTokens (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 OpenCode step_finish per-turn token counts.

Changed

  • 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.

Fixed

  • mediforce-fullstack agent steps lost their result contract: the prompts told the model to write result.json to ${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. triage classified an issue but apply-verdicts received 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: triage verdicts now reach apply-verdicts and 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 as assignedUserId, 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 stranded mediforce-fullstack approval gates whose FULLSTACK_REVIEWER_MAP used 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 for human | script | action executors only.

[2026-06-21]

Fixed

  • Context-saturation logging now covers the claude-code-agent plugin, not just OpenCode. Peak per-turn context occupancy is computed from the stream's per-turn assistant usage (summing input_tokens + cache-read + cache-creation, since caching hides most of the prompt from input_tokens) and folded into the result event's usage as peak_input_tokens, so logContextSaturation reports a real peak instead of nothing. Previously only the final turn's usage was 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-mcp server crashed on startup because @mediforce/platform-core was an undeclared (phantom) dependency and its workflow-examples loader had no subpath export, so list_workflow_examples was silently unavailable (the connection failure is swallowed). Declared the dependency, added the ./workflow-examples export, and import it via the package specifier.
  • Postgres workflow definitions now persist imported workflow source provenance, 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 test is now fully self-contained — Playwright globalSetup auto-starts the Firebase Auth emulator when absent and runs pending DB migrations, so E2E tests pass without manual pre-flight steps.

Changed

  • 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.

Added

  • CI deploy now targets both staging.mediforce.ai and cdisc.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 validate CLI command, both running the same parseWorkflowTemplate schema (cross-field rules included) that registration uses. The workflow-designer validate step 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_examples MCP tool — eliminates stale inline schema that caused script steps to generate with the old step.agent config instead of step.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.json that declares its own namespace imports cleanly (the target namespace wins, matching workflow 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 no index.json manifest, 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) + a task_attachments table behind a workspace-scoped repository, with headless upload/list/download/delete routes and a typed client; bytes live on the ~/.mediforce volume, metadata in Postgres, size guarded by MEDIFORCE_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_PROVIDER auto-detects or can be set explicitly; includes read-only admin status page, mediforce email status CLI 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 × action cross-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 normaliseModelId to platform-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/agents symlinks, with AGENTS.md documenting how Codex should translate Claude-specific skill tool syntax.

Removed

  • 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, and NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET are 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 + the agents.skill_file_names column, the skill-upload UI, and the Firebase Storage skill download from agent identity resolution; skillsDir is now the only skill mechanism (ADR-0003 PR1).
  • E2E GIF recording: the recording helper, ffmpeg conversion scripts, the docs/features/FEATURES.md gallery, and test:e2e:record/:gif scripts (3 README screenshots kept). UI journey tests stay as normal L4 e2e (page-error tracking kept in helpers/page-errors.ts).

Fixed

  • Cowork workflow designer can render diagrams again — nothing depended on @mediforce/mediforce-mcp, so pnpm never linked its mediforce-mcp bin onto PATH; the chat handler's spawn failed with ENOENT and the MCP tools (including render_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-mcp as a platform-ui dependency so the bin links onto the spawning process's PATH.
  • Cowork chat no longer silently drops large artifacts — the OpenRouter call left max_tokens at the client default of 4096, so building a multi-step workflow truncated the update_artifact tool-call JSON mid-stream, JSON.parse threw, 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 surfaces finishReason, 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_diagram no longer rejects in-progress workflows that route purely via step verdicts — its input schema required a top-level transitions array even though the renderer already handles its absence, so rendering a verdicts-only draft threw a Zod error; transitions now defaults to [].
  • Cowork MCP tools now work when env templates ({{SECRET}}) reference workspace secrets — connectMcp passes the resolved secrets to McpClientManager so 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 — fetchSkillsFromRepo no longer forces an SSH clone (which fails in the agent container, where there is no ssh binary) when no auth token is configured; SSH/deploy-key transport is used only for genuine git@ 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 --requirepass but the variable was missing, causing Redis to crash-loop on fresh deployments #777.
  • repoexternal_skills_repo migration applies on staging again — the previous 0026/0027 pair carried an out-of-order journal when timestamp (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 single 0026_external_skills_repo migration (column add + repo copy + 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/commit agents 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 running pnpm test:e2e, while interrupted MCP-binding journeys no longer dirty the next run.
  • datetime params 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 pauseReason are now surfaced as errors instead of silently stalling forever: the auto-runner verifies the waiting_for_timer write after it commits and escalates to failed if the write was lost; the heartbeat sweeps and escalates any surviving paused/null orphans; 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.yml now builds native linux/amd64 + linux/arm64 images via a matrix job on GitHub-hosted ARM64 runners; the Hetzner ARM64 staging server was silently falling back to a stale local be65eae4 build because the amd64-only CI images had no matching manifest for arm64/v8; deploy script pull errors are now visible instead of swallowed #740.
  • scripts/sync-model-rankings.py now 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: /deploy on a PR runs a real vercel build + vercel deploy --prebuilt via 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.

[2026-06-14]

Added

  • 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-contact endpoint 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

Changed

  • StepExecutor strategy pattern (ADR-0008) — agent and script steps now execute through separate StepExecutor strategies (AgentStepExecutor, ScriptStepExecutor) instead of a monolithic AgentRunner with isScript branches. Each strategy owns its full lifecycle (run, audit, advance/review, cost tracking). AgentPlugin interface renamed to StepExecutorPlugin to reflect that scripts and Databricks jobs are peers, not special-cased agents:
    • StepOutputEnvelope base schema in platform-core #688
    • PluginRunner extracted from AgentRunner #691
    • StepExecutor interface + AgentStepExecutor + ScriptStepExecutor #692
    • AgentPluginStepExecutorPlugin rename #694

[2026-06-07]

Added

  • databricks-job plugin — workflow steps can trigger an existing Databricks job (jobs/run-now + polling) and route transitions on the JSON the notebook exits with; secrets DATABRICKS_HOST/DATABRICKS_TOKEN, ${steps.*} param interpolation, single-task jobs in v1. Includes scripts/databricks-spike.py for manual verification against a real workspace.
  • Agent-produced files are no longer lost — everything an agent step leaves in /output is preserved as Output Files on the run branch (.mediforce/output/<stepId>/, ADR-0007) and is reviewable and downloadable:
    • API: GET /api/runs/<runId>/files lists a run's files; GET /api/runs/<runId>/files/<path> serves the bytes (binary-safe, RFC 6266 attachment); mediforce.runs.listOutputFiles / downloadOutputFile client 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> and mediforce run download <runId> [path] [-o dir].
  • OpenTelemetry tracing for agent runs (ADR-0007 layer 1)agent-runtime emits OTel spans: mediforce.agent.run (root, with full workflow correlation attributes: namespace, workflow name+version, stepId, agentRunId, processInstanceId, autonomy level) and openrouter.chat.completion (GenAI semantic conventions: request/response model, token usage). Content captured only when MEDIFORCE_OTEL_CAPTURE_CONTENT=true: step input / envelope result on agent-run spans (OpenInference input.value/output.value, rendered by Phoenix), prompt/completion text on LLM spans. Export is opt-in via OTEL_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=true to keep it); Phoenix added to docker-compose.yml as the dev trace viewer on :6006.
  • Dry run mode — workflow runs can be tagged as dry runs for testing. Full execution with a dryRun flag, 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-params task kind lets a single human step collect structured param values and a verdict together — previously required two separate steps #658. Includes ParamVerdictView component and datetime param type support.
  • Merged workflow designer — consolidated workflow-designer (v21), workflow-designer-2 (v3), and cowork-workflow-designer (v5) into a single cowork-based workflow-designer with 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_artifact call (wires up the previously unused validateOutputSchema), with results shown in the artifact panel and enforced as a gate on finalize.
    • New update_presentation built-in tool lets cowork agents push HTML previews rendered in a sandboxed iframe tab alongside the JSON tree explorer.
    • platform-mcp stdio server exposes render_workflow_diagram — a deterministic HTML renderer matching the platform's visual language (step type colors, executor badges, verdict pills).
    • POST /api/render/workflow-diagram REST 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 on cowork_sessions.

Added

  • 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 (fetches index.json, multi-select, progress bar), and POST /api/workflow-definitions/import + GET /api/workflow-definitions/manifest server-side handlers — imported definitions carry source.{repo,path,ref} provenance metadata #561.

Changed

  • Breaking (schema + data migration 0023): deterministic script-step config moved from step.agent to a typed step.script key; agent/autonomyLevel/cowork are now rejected on executor: 'script' steps and audit events record executorType: 'script' (forward-only). Stored workflow definitions are rewritten in-place by Postgres migration 0023_script_step_config — it must ship in the same release, because definitions are re-parsed on read.

Fixed

  • Script container runtime: "python" now invokes python3 — previously failed on golden image (and any image without a python symlink) with exec: "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_tokens from the CLI result event and dropped cache_read_input_tokens and cache_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 into inputTokens and cache-read tokens are tracked as cachedInputTokens, priced at the registry cacheRead rate (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.

Changed

  • 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_KEY for the CLI (must match PLATFORM_API_KEY), and building local script-executor images (mediforce-golden-image, mediforce-node) via scripts/rebuild-docker-images.sh before running workflows with script steps #670.
  • Deploys can no longer leave orphaned containers behind — deploy.sh and deploy-staging.sh pass --remove-orphans to docker 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 base64 script.mjs payloads) #682.

[2026-05-31]

Changed

  • pnpm dev / pnpm dev:queue now preflight infra via scripts/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 --wait so 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 surfacing ECONNREFUSED 127.0.0.1:5432 mid-request. Prerequisites + troubleshooting documented in GETTING-STARTED.md / dev-quickref.
  • Removed redundant dev:postgres script (use pnpm dev); dev:no-docker now sets DATABASE_URL so it boots post-#534; doc cleanup (ADR-0001 Accepted, STORAGE_BACKEND refs scrubbed, headless-migration docs marked Completed).

Fixed

  • Workspace-selection page no longer renders a blank picker when /api/users/me fails — useAllUserNamespaces surfaces isError/error and 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.deleted emits 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 list no longer 400 on null-envelope runs stored as '{}' instead of SQL NULL (read tolerates it, write stores NULL, migration 0015 backfills). #534
  • Soft-deleted workflows no longer leak into the catalog list (regression from the Firestore→REST cutover dropping the UI's !deleted filter). #534
  • PostgresAgentRunRepository.create() now upserts (onConflictDoUpdate) on a repeated run id — the runner writes the same id twice (running → terminal) and the plain insert threw agent_runs_pkey, a regression from Firestore .set(). #617
  • Staging deploy broke after #572: the migrate init-container stage derived FROM builder, so its arg-less build re-ran next build whose /workspaces/new prerender failed with auth/invalid-api-key (Firebase build args reach only the platform-ui service). Migrate stage now derives from deps + source, skipping the Next build entirely.
  • Flaky forced-password-change.journey.ts (#578) — updatePassword revokes the session's ID token, so the forced-reset flow now re-authenticates and clearMustChangePassword retries 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 shared stopRetryOn4xx util and a refetchInterval error gate — previously a 403/404 caused runaway API/Postgres load on every tick, burning budget (post-#591 cost regression).
  • useProcessNameMap now scopes its runs.list poll to the active workspace handle (namespace: handle) instead of fetching across every visible workspace every 5 s — a cross-workspace data-scope leak.
  • useAgentEvents polls incrementally via a new afterSequence cursor on processes.agentEvents instead of re-reading the entire agentEvents subcollection every 1.5 s — the Phase-4 cutover lost onSnapshot deltas and turned a chatty open run into ~20k Firestore reads/min.
  • Moved validateEnv (and its process.exit) into a node-only instrumentation-node.ts, dynamic-imported from the NEXT_RUNTIME === 'nodejs' branch, so Turbopack no longer parses process.exit for the Edge build — silences the process.exit ... not supported in the Edge Runtime warning that flooded the dev log on every compile.
  • StepParamSchema.type widened from a strict enum to z.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-string type still fails parsing — only the enum was the bug. Route adapter also console.errors handler ZodErrors so similar mismatches are debuggable from the server log.
  • CoworkSession schema now defaults agentchat and voiceConfignull so legacy chat-only sessions (predating the voice fields) survive the cutover's full-scan Zod reads instead of 400ing the whole cowork.list. findMostRecentActive drops its createdAt orderBy (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.

Changed

  • ADR-0001 final cutover — users/{uid} profile migrated to Postgres user_profiles (mustChangePassword only); invite + forced-password writes now route through PostgresUserProfileRepository. #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 dev now boots the full local stack (Postgres up + migrate + UI); dev:local removed, dev:no-docker stays docker-free, dev:queue opts into Redis/BullMQ.
  • Postgres repo row-mappers strip nullable columns once at the boundary via new compact()/parseRow() helpers in platform-core instead of per-field conditional spreads. #614
  • useProcessNameMap now reads a projected GET /api/runs/names (mediforce.runs.listNames, { id, definitionName } only) on a 30 s cadence instead of fetching up to 10k full ProcessInstance docs every 5 s — cuts the ~24 s/request payload bloat (#588).
  • Workspace-home run counts + 3-run preview now come from a server-side runSummary on workflows.list instead of polling up to 10k runs every 5 s and grouping client-side; includeCompletedRuns mirrors the "show completed" toggle.
  • Phase 4 done (PR-final #591) — zero firebase/firestore in platform-ui (api-boundaries.test.ts tripwire); PG PR2 (#534) unblocked.
  • ADR-0001 — server data layer migrated to Postgres; Firestore admin SDK deleted (14 repos, L2 parity + L3 API E2E). STORAGE_BACKEND flag removed (Postgres is the only backend). Cutover script + verifier + operator checklist in scripts/migrate-firestore-to-postgres/. #534.
  • Dev ergonomics + Vercel preview gating (#585) — pnpm dev honors PORT, E2E scripts honor E2E_PORT (defaults 9003 / 9007 unchanged), so two worktrees can run side-by-side without colliding. Root dev* scripts auto-run pnpm 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-preview on a PR — a workflow pushes an empty [preview] commit that the new vercel.json ignoreCommand 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 (migrate service, build target migrate) instead of an in-app boot step. Drops packages/platform-infra/scripts/migrate.mjs and the outputFileTracingIncludes workaround in packages/platform-ui/next.config.mjs (PR #570 hotfix for staging ERR_MODULE_NOT_FOUND).
  • Phase 4 PR5 — cowork session + chat moved to react-query: [handle]/cowork/[sessionId]/page.tsx no longer imports firebase/firestore; chat send is a useMutation with optimistic prepend, cancellation-race protection, and a 1 s polling cadence flip while in-flight. ChatCoworkSessionOutput gains additive session + turns fields so the UI can replace optimistic state without a follow-up GET. New mediforce 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/writing users/{uid}.mustChangePassword; GetMeOutput.user extended with mustChangePassword: boolean (default false); POST /api/users/me/clear-must-change-password + mediforce users clear-must-change-password CLI (audit user.password_change_acknowledged). Five settings-page mutations migrated: PATCH /api/namespaces/:handle (namespace.updated; bio is two-state — undefined leaves it untouched, any string overwrites incl. empty), DELETE /api/namespaces/:handle (namespace.deleted, cascade via NamespaceRepository.deleteNamespaceCascade), POST /api/namespaces/:handle/leave (namespace.member_left; owner blocked → precondition_failed), DELETE /api/namespaces/:handle/members/:uid (namespace.member_removed, atomic via removeMemberWithOrganizations), 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 of app/(app)/[handle]/page.tsx rewired through useUserMe / useNamespace / the new use-namespace-mutations hooks. 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. New GET /api/users/me (lazy personal-namespace bootstrap + user.personal_namespace_created audit), GET /api/namespaces/:handle, POST /api/namespaces (namespace.created audit, atomic via NamespaceRepository.createNamespaceWithOwner). auth-context.tsx, use-namespace, use-namespace-role, use-all-user-namespaces rewired (selectors over useUserMe()); use-user-namespace.ts deleted in favour of usePersonalNamespace. workspaces/new uses useMutation with list-affecting optimistic prepend; [handle]/settings drops the members onSnapshot. New HandleSchema write-boundary constant. New CLI: mediforce users me, mediforce namespace get|create.
  • Phase 4 PR2 — agent-runs + monitoring migrated off firebase/firestore. New headless endpoints GET /api/agent-runs (cursor-paginated, runId/stepId filters), GET /api/agent-runs/:agentRunId, GET /api/namespaces/:handle/monitoring/summary (all-time running / paused / completed / failed per status, mirroring the pre-PR2 useMonitoringData hook this endpoint replaced). useAgentRuns / useAgentRunsForStep / useAgentRun rewired to react-query (STANDARD LIVE 5 s, CRITICAL LIVE 1.5 s for detail); useMonitoringSummary runs NICE LIVE 30 s with focus-refetch. New mediforce agent-run CLI subcommand.
    • Agent-run workspace gating bypassed for parity with the pre-PR2 Firestore subscription; per-row gate restore tracked in #588.
    • FirestoreProcessInstanceRepository.listAll skips per-row parse so legacy docs no longer 400 the monitoring summary.
    • Monitoring summary perf: ListInstancesOptions.namespace pushes the workspace filter into Firestore (new (namespace, deleted, createdAt DESC) composite index), and a bulk HumanTaskRepository.getByInstanceIds collapses the per-run tasks N+1 into one chunked where('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.list gains caller-scope axis (no params = my workspace queue); runs.list adds a namespace filter and now returns the full ProcessInstance shape (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). New mediforce task CLI subcommand.
  • ADR-0005 Phase 2.5 — 18 endpoints (workflow-definition mutations, agents, secret read companions, processes archive/bulk) migrated to headless handlers; actions/processes.ts and actions/definitions.ts deleted. transferWorkflow now gates both source and target namespace, deleteWorkflow audit 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) behind GET/PUT /api/workflow-secrets/values; getWorkflowSecrets / saveWorkflowSecrets Server Actions deleted; UI uses mediforce.workflowSecrets.values() / .save(); new audit actions workflow_secret.values_revealed and workflow_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_KEY collapsed (closes #218).
  • ADR-0005 Phase 3.1 — cowork endpoints migrated to headless handlers; app/actions/cowork.ts and the dead /message SSE 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.ts renamed to proxy.ts (export middlewareproxy) per Next.js 16's middleware-to-proxy convention. Silences the ⚠ The "middleware" file convention is deprecated warning 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: kindverdict | params | upload | assignment | rows), processes/resume, processes/:id/steps/:stepId/retry, cron/heartbeat, and POST /api/processes now all run as (input, scope) => output handlers. Heavy lib/resolve-task.ts body (481 LOC) folded into a new WorkflowEngine.completeHumanTask so the handler stays cancel-run-shaped (workspace gate → engine call → audit + kick). New RunKicker abstraction (scope.system.runKicker) replaces eight inline fetch('/api/processes/:id/run') self-fetches with a single prod impl (createHttpSelfFetchRunKicker) + a test spy (noopRunKicker). POST /api/processes response 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/resolve route deleted (UI-unused per audit). ADR-0005 §7 closed audit-action set extended with instance.retried, cron.trigger.fired, process.resumed_after_task. New CLI: typed client methods on mediforce.tasks.complete, mediforce.runs.resume, mediforce.runs.retryStep, mediforce.cron.heartbeat. Internal processes/:id/advance + processes/:id/run stay inline (future BullMQ executor ADR migrates the auto-runner loop).

Added

  • ADR-0001 tracer-bullet (Firestore → Postgres): tool-catalog is the first repository with a Postgres backend behind STORAGE_BACKEND=postgres. New packages/platform-infra/src/postgres/ houses the Drizzle schema (tool_catalog_entries keyed on composite (workspace, id)), the PostgresToolCatalogRepository implementation, the forward-only SQL migration runner, and a shared ToolCatalogRepository contract that both the in-memory double and Postgres backend satisfy (InMemoryToolCatalogRepository now validates payloads via ToolCatalogEntrySchema for parity). docker-compose.yml ships a postgres:16 service; CI gains a postgres-tests job that exercises the Postgres branch via a services: 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 human workflow step can declare assignedTo (e.g. "${triggerPayload.userId}"); the auto-runner interpolates it against the run's sources and creates the task claimed and pinned to that assignedUserId instead of open to the whole role. An assignedTo that resolves to nothing fails the run rather than silently falling back to an open task; assignedTo on 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 wait action kind (#521) — pauses a run for a fixed duration or until a deadline, with an optional early-exit condition. The handler returns a __wait sentinel that the auto-runner turns into a paused/waiting_for_timer instance; the cron/heartbeat sweep and POST /api/processes/:id/resume-wait resume eligible runs. The wait step execution follows a running → paused → completed lifecycle — its resolved { resumeReason, waitedSeconds, resolvedAt } output is written once on resume, never the raw sentinel.

  • ADR-0001 tracer-bullet (Firestore → Postgres): tool-catalog is the first repository with a Postgres backend behind STORAGE_BACKEND=postgres. New packages/platform-infra/src/postgres/ houses the Drizzle schema (tool_catalog_entries keyed on composite (workspace, id)), the PostgresToolCatalogRepository implementation, the forward-only SQL migration runner, and a shared ToolCatalogRepository contract that both the in-memory double and Postgres backend satisfy (InMemoryToolCatalogRepository now validates payloads via ToolCatalogEntrySchema for parity). docker-compose.yml ships a postgres:16 service; CI gains a postgres-tests job that exercises the Postgres branch via a services: 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-table human-task UI for many-to-one item assignment with per-row assignee/priority/note. Items flow via task.options from the previous step; the assignee allowlist and labels live in task.ui.config.

  • Generic table-editor human-task UI (#469) — schema-driven columns (static | single-select | multi-select | text) declared in task.ui.config.columns, one form per task.options[i] row, pre-filled from option.suggestion[columnId], with allowEmpty:false single-selects gating submit. Emits a stable { rows: [{ itemId, values: { <columnId>: <value> } }] } map (completeTableEditorTaskresolve-task). assignment-table is now a thin back-compat shim that builds an equivalent column config internally, so existing workflows keep working with zero WD changes.

    • backlog-triage tags issues in-platform (#469): the tag-issues step now renders a table-editor (pick a category + priority per untagged issue) and a new apply-tags script POSTs the labels to GitHub — creating any label the repo lacks — looping tag-issues → apply-tags → fetch-backlog until every issue carries a category. Replaces the old "open each issue on GitHub, tag it, come back and re-check" round-trip; check-tags now 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.md instead of (or alongside) /output/presentation.html; the platform reads MD first and renders it via react-markdown + remark-gfm + rehype-sanitize inside Tailwind prose typography. 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-string presentation envelopes auto-coerce to {kind:'html', content} — no Firestore migration. Iframe sandbox stays for the cases that genuinely need JS / interactivity. apps/backlog-triage check-tags migrated 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 a CallerScope instead of raw repositories; 13 Authorized<Entity>Repository wrappers enforce workspace membership + WorkflowDefinition visibility on every read/write. Soft-delete filtering stays at the storage layer (lands with ADR-0001's WorkspaceScopedRepository). The 10 Phase 1 GET handlers from #450 lose their inline callerCanAccess guards (now in the wrapper). The static guard becomes no-raw-repo-imports.test.ts (structural — handlers cannot import raw repositories); auth-coverage.test.ts removed (#458, #463).

    • Code-review tightening on #463: AuthorizedWorkflowDefinitionRepository.getLatestVersion is now visibility-gated (no foreign-namespace private-WD enumeration); AuthorizedWorkflowRunRepository.update asserts namespace immutable and blocks non-system deleted patches (the getById gate alone would let a member smuggle a run across workspaces); getModel throws NotFoundError instead of bare Error so the route adapter maps it to 404.
  • @mediforce/platform-api/repositories subpath export: CallerScope, createCallerScope, AuthorizedRepository base, and per-entity wrappers.

  • @mediforce/platform-api/testing subpath export: createTestScope, userCaller — builds a real CallerScope from in-memory repos for handler tests downstream of platform-api.

Changed

  • CLI migrated to the citty library and a thin defineCommand wrapper in packages/cli/src/define-command.ts. Every subcommand (23 total) now declares its arg shape once; the wrapper handles parseArgs, --help, missing/extra positionals, resolveConfig + Mediforce client construction, --json mode, formatCliError envelope, and exit-code mapping. cli.ts dispatch is a small TREE map with auto-generated branch help (mediforce workflow --help); the long if-ladder is gone. Helpers: typed enumArg(['a','b'] as const) (narrows citty's EnumArgDef.options: string[] widening), CommandFn type, a TREE smoke test (packages/cli/src/__tests__/cli-tree-smoke.test.ts) that exercises --help on 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 -1726 LOC across packages/cli/. Adds citty dep, +25 tests, 183/183 green.
  • ADR-0005 Phase 2 PR2 (closes Phase 2): POST /api/processes/:instanceId/cancel migrated to the headless handler shape. New cancelRun handler reuses scope.runs.update() (state-machine in handler, matches PR1's deviation); response is entity-echoed { run: WorkflowRun } replacing the prior { instanceId, status }. Contract symbols use Run vocabulary per ADR-0001 (CancelRunInputSchema { runId }); URL path keeps the legacy processes/:instanceId segment until a coordinated URL rename phase, the adapter translates params.instanceIdrunId. mediforce.runs.cancel({ runId, reason? }) available on the typed client + new CLI command mediforce run cancel <runId> [--reason <text>]. Audit emission preserved bit-for-bit via scope.system.audit.append bridge per ADR-0005 §7 (action instance.cancelled matches workflow-engine's instance.* family and the legacy bulkCancelProcessRuns emit; a repo-wide instance.*/process.*run.* rename ships as its own ADR pass). UI callers (process-detail.tsx, agent-escalated-banner.tsx) moved off the cancelProcessRun Server Action. Dead code removed: cancelProcessRun action, UnclaimButton component + unclaimTask action (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/claim migration. HandlerError in @mediforce/platform-api now carries code: ApiErrorCode + details?: unknown; existing NotFoundError/ForbiddenError subclasses get the code stashed in their constructors, and PreconditionFailedError is added for the claim handler's state-machine throw. Other codes (unauthorized, validation, conflict, rate_limited) throw via the base HandlerError until a real throw site arrives. createRouteAdapter catches HandlerError once and emits the ADR-0005 §1 envelope { error: { code, message, details? } } across every route (Phase 1 + 1.5 reads retroactively typed too). Client-side ApiError gains code/details fields parsed from the envelope (existing status/message/body unchanged). New POST /api/tasks/:taskId/claim handler (pure (input, scope) => { task }), new loadOr404 helper, new mediforce.tasks.claim() client method. claimTask Server Action deleted; claim-button.tsx calls the typed client. Audit emission preserved bit-for-bit via scope.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 through createRouteAdapter + scoped handlers in @mediforce/platform-api/handlers, eliminating their inline callerCanAccess guards. GET /api/runs/:runId switches from 403 → 404 for foreign-workspace ids (anti-enumeration), and GET /api/workflow-secrets soft-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 on ui.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.options onto the next human task regardless of whether the next step declares selection, so non-selection components (like assignment-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.ts introduced in #450 — its AgentDefinition* / WorkflowDefinition* schemas duplicated agents.ts / workflows.ts shape-for-shape. SDK client mediforce.agentDefinitions.* and mediforce.workflowDefinitions.* namespaces dropped; mediforce.agents.* and mediforce.workflows.* are the only surface. Renamed handlers/definitions/handlers/workflows/ and handlers listWorkflowDefinitions / getWorkflowDefinitionlistWorkflows / 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 canonicalises Agent (no "Definition" suffix), adds a Workflow umbrella entry around the existing Workflow Definition, reframes Agent Run as a workflow-execution concept, and adds an Agent vs Agent Definition flagged-ambiguity note that tracks the pending AgentDefinitionSchema / AgentDefinitionRepository symbol rename. ADR-0001 PLAN now proposes the Postgres table as agents; ADR-0004 carries a post-merge naming note. CLI commands, UI labels, route handler logic, and wire shapes all unchanged.
  • CallerIdentity now carries readonly isSystemActor: boolean. The free function isSystemActor(caller) was replaced by reading caller.isSystemActor directly — 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 in platform-api/src/repositories/ become thin routers that pick the right method based on caller.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 — listAdapter and getByIdAdapter in @mediforce/platform-api/handlers. getTask, getProcess, getCoworkSession, listAgentDefinitions, and getAgentDefinition no 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 the mediforce-golden-image Docker container; Claude Code credentials are routed through OpenRouter via the workflow env block, since Docker steps don't inherit the host environment and must declare credentials explicitly (#492).

Removed

  • Duplicate Python migration script scripts/migrations/migrate_workflow_doc_ids.py — superseded by the TypeScript packages/platform-infra/scripts/migrate-workflow-namespacing.ts which is the canonical version (#424).

Added

  • packages/cli/src/__tests__/bin-shim.test.ts — spawn-based smoke test that runs the real bin/mediforce.cjs shim through tsx and asserts exit codes + stderr/stdout for --help, a missing positional, and an unknown command. Catches regressions (ESM resolution, tsx loader, bin shim entry resolution) that the in-process runCli suite cannot.

Fixed

  • Script-container failures now surface the error the script wrote to /output/result.json (its error/errors field) 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-designer register/validate steps now show the real cause (#546; follow-up to #538).
  • Workflow designers now default agent steps to the mediforce-golden-image Docker image. Generated claude-code-agent steps previously carried no agent.image, so a freshly designed workflow crashed on staging/production with "No Docker image configured" (imageless/local execution only works in dev with ALLOW_LOCAL_AGENTS=true). The generate-execution-proposals skills (workflow-designer, workflow-designer-2) and the cowork designer 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 injected RUN_ID/STEP_ID and (for workflow runs) MEDIFORCE_RUN_NAMESPACE — and input.json size) is written into the activity log the Step Log panel reads, so a fast no-output failure no longer sticks on "Initializing log…". Container exit code/signal is emitted as a status event on every run, and the exit code/killed by formatting (with the SIGTERM→timeout hint) is now a shared formatExitInfo helper reused by the agent and script container plugins (#538).
  • Workflow designer registers generated workflows in the run's own namespace instead of a hardcoded default org (#536). The script-container plugin now injects the run's namespace as MEDIFORCE_RUN_NAMESPACE (alongside RUN_ID/STEP_ID); all four designer register scripts read it. workflow-designer no longer falls back to default, the voice/cowork variants stop omitting the required ?namespace= query param (which previously 400'd), and workflow-designer-2 drops its {{NAMESPACE}} secret-template (which threw unless a per-namespace NAMESPACE secret was manually configured) in favour of the same auto-injected var.
  • backlog-triage tagging loop no longer whack-a-moles or silently swallows label failures. fetch-backlog paginates past PRs to a stable working set of the newest limit real issues — a single PR-heavy page (GitHub's issues endpoint counts PRs against per_page) or a merge mid-session used to shift which untagged issues showed up each loop. And apply-tags now surfaces label failures instead of swallowing them: a presentation.md summary 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. a GITHUB_TOKEN without label-write permission shows as a red run instead of an endless re-prompt; partial failures stay non-fatal.
  • check-gif-freshness.py now 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:mock now 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 Runs page no longer leaks runs from other namespaces the viewer is a member of — list is now filtered to the current handle, and useProcessInstances requires namespace so 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 strips defaultVersion for tenants that don't own that version, avoiding dangling defaults pointing at non-existent workflow definitions (#424).

[2026-05-17]

Added

  • 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 a CallerIdentity, so a missing gate is a code-level decision in review, not a silent regression). Wraps audit response as { events } and surfaces visibility-denied agent definitions as 404 (anti-enumeration). Supersedes #256, which predated the namespace access control on main and 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) and demo-uploader/ (Mediforce workflow with 8-verdict human review) (#407).

Changed

  • 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:mock boots the app with mocks in one command, every script is named for what it does (dev:testdev:mock, dev:localdev:no-docker, dev:ui:queuedev:queue), pnpm test runs the full pyramid instead of vitest only, and bootstrap-dev.py with its silent OPENROUTER_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 from e2e/ui/ (sparse multi-step user journeys only, not "is button visible"). Misleading legacy names cleaned up: src/test/*.test.tssrc/test/integration/, e2e/api/ (tier-2 real-LLM) → e2e/external/, e2e/journeys/e2e/ui/ (#413).

Fixed

  • Agent report iframe no longer blows up its host panel — height is capped and vh classes 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 9003 to 9007 for pnpm dev:mock, root API/UI E2E wrappers pass Playwright project flags correctly, and mediforce run start can target a namespace.
  • Staging step containers can finally see workspace files — the data dir is persisted at /var/lib/mediforce with 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).

Dependencies

  • 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).

[2026-05-10]

Added

  • 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 credits CLI — each tenant sees its own balance (#349).
  • Mediforce reviews its own PRs — pr-reviewer-mediforce workflow runs AGENTS.md-aware review on every PR (#338).

Changed

  • 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).