Release environments, development, deployment agents, and lots of improvements #5

Merged
niels merged 155 commits from develop into main 2026-10-13 10:32:25 +00:00
Owner
No description provided.
niels added 148 commits 2026-10-13 08:35:03 +00:00
Twin 3.2.0 (sc-2657) adds `tracks` to grant versions and derives the version
min/max amounts from them. Mirror its save rules in the proposal pre-check
(duplicate track names, min above max, base above max rate, a version amount
the tracks contradict, fork inheritance of tracks) so a create cannot fail
halfway, and show a track's funding rates as one compact line per activity
in the Changes view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Twin 3.2.0 (sc-2688) runs a save.body check in validate-payload. A create that
makes its knowledge base (or provider) in the same apply leaves that id out of
the preflight, so the body check failed with "knowledgeBaseVersionId should
not be null" and blocked every such proposal. Drop only the messages about the
fields the proposal creates first; other body errors still block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A list inside a record (a track's funding rates and their uplifts) rendered
as one run-on line. Each rate now shows its activity, range and provision on
one line, one line per uplift below it, and the note last; other nested
record lists get the same list layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An agent definition lists its labels in metadata.session_labels (key, label,
tone, description, source). A run sets the `agent` ones by writing
output/session-labels.json ({"add": [...], "remove": [...]}), applied after a
completed execution and named in the workspace contract. A `twin_changes`
label is derived from the session's Twin Changes proposals, or from those of
the sessions it dispatched: "Apply to Twin" while one waits, "Applied to Twin"
once applied, "Twin apply failed" when it needs attention. The labels show
as badges in the session topbar, the agent session list and home activity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each declared session label gets `show_on_home` (default true). An admin
switches it per label in a new Session labels section on the agent settings
page (saved through the agent PATCH, so it is versioned with the definition);
the home activity list hides the labels switched off. The agent session list
gets a filter row with one toggle chip per label (with counts) next to the
search, showing sessions with any selected label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenSteward must not carry Twin-specific code. Undo today's additions:
- Delete the copy of Twin's grant version rules (twin-grant-version-rules.js):
  the proposal preflight now sends the exact write body, with baseVersionId
  on a fork, and the upstream runs its own save rules on it.
- The derived session label source is `change_proposals` (was
  `twin_changes`); every text comes from the definition, with `label` as the
  only fallback.
- The Changes view lays out nested lists generically; the track funding-rate
  format is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An api_wrapper approve rule refuses the agent's direct call (409
change_proposal_required). The agent now lists such calls in its proposal
under `requests` ({method, path, body, label, reason}); the platform checks
each against the tool's rules (it must match an approve rule, else it is an
issue), shows them on the Changes page and in the confirmation, and makes
them last, in order, once a person confirms. The calls' meaning stays in the
agent definition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request may carry the agent's own action word ("archived"); the Changes
page header counts requests per action next to created and changed records
("− 4 archived"), and the requested calls section moves to the top, above
the records the proposal creates. The empty "deleted" count is hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A proposal tool may set `change_proposals.max_cases`: a revision with more
cases is refused with a 400 before any upstream call, in managed and
standalone sessions, so a person confirms a bounded batch. The proposal body
limit rises from 8 MB to 32 MB (PROPOSAL_MAX_BYTES) for documents that carry
captured source texts. Both live in a generic shared/proposal-limits.js.
Ported from the uncommitted feature/proposal-max-cases worktree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Databases created before an engine was supported (twin production) never got
rows for Grok Build, Antigravity, Muse, DeepSeek and GLM, because only the
seed inserted them and production runs migrations without the seed. The
migration step now adds missing engine rows (existing rows keep their
settings) and disables the retired Gemini and Cursor drivers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- change_proposals.kind "requests": a proposal of approve-only calls with a
  summary and review items, compiled without a Twin inventory and shown on a
  generic Changes page.
- verification.after_apply starts the agent's own check execution with the
  recorded call results after every successful apply.
- api_wrapper static_headers: fixed upstream headers (for example Origin)
  on proxied and proposal calls; reserved headers are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only calls a requests-proposal tool marks approve-only are diverted to the
proposal; allowed non-GET calls such as a read-only check pass through.
Twin Changes keeps routing every write except its validation calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A proposal is usually submitted, and in autonomous mode applied, while the
submitting step is still running. The check now retries while the session
reports an unresolved turn instead of failing at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discard (session topbar, POST /sessions/:id/discard with an optional reason)
tags the session with the platform's own Discarded chip and a notice, stops
live runs, archives open questions, sets proposal revisions that wait for
confirmation to `discarded` (kept readable, never applied, no new revisions)
and rejects pending dispatch proposals. Then the agent's discard step, named
in agent metadata `session_discard.execution_configuration_id`, runs in the
session with the reason so the agent releases its state and a later run finds
the same work again. The step is never a session button and starts only
through a discard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keys the platform issues to a running session stay refused, so an agent
cannot discard its own work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mark is written under the proposal lock, so a step that is still
running cannot submit a revision or dispatch between the discard and the
stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session shows the state of its latest revision that was not discarded,
so an applied revision keeps its label after a newer one is discarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An admin ticks labels under Label tabs on the home page (Admin mode); each
ticked label gets a tab listing every session that shows it now, across
agents. Stored as an instance setting (home_label_tabs); nothing is ticked
by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
normalizeApiWrapperConfig always added an empty static_headers object, which
is part of a change proposal's tool fingerprint: every Twin Changes revision
prepared before the deploy would have refused confirmation. The key is now
present only when a tool sets static headers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	api/src/modules/change-proposals/change-proposal.service.js
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A proposal tool with change_proposals.kind "generic" names its tab and page
texts (label, view) and the agent submits items to review and the calls to
make. Nothing in the platform knows what the upstream is.

- Items: context, resources with a field table of current and proposed
  values (nested lists and records as nested rows with their own marks),
  files, notes and a preview; header counts from resource actions.
- Calls: run in order, reading earlier results ({{call.field}} or $result),
  with expect for what a good response looks like and captured uploads.
- Checks: read when prepared and again before applying (snapshot,
  none_match, some_match with pagination, exists).
- Wrapper rules carry approver_header instead of a fixed route list; the key
  is stored only when true, so existing tool fingerprints are unchanged.
- The proxy holds back only approve-only calls; standalone prepares and
  stores generic revisions the same way.
- The wisvids requests kind is replaced by this shape.

A transitional converter turns stored Twin revisions into the generic shape.
Parity check: all 185 stored Twin revisions available (20 local, 165 from
production) render byte-identical HTML on the generic page in every case,
mode and unchanged-fields setting (1383 views), plus a committed synthetic
fixture with retirements, new providers, relations and nested rates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parity check now also renders the documents after the platform's own
validation, the path an agent's submission takes; all 185 stored Twin
revisions still match the Twin page exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dry run by default; --apply rewrites document, operations and checks into
the generic shape and keeps status, run results and verification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The start panel lists one row per engine and model with the steps that use
it, and when the extra steps run (by hand, or when a session is discarded).
Engine readiness reports for each step whether it runs automatically,
manually or on discard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 25-topic New EU grants proposal was 25.8 MB because every Knowledge Base
carries the same regulation and annexes and each file text appeared in its
upload call, the item resource and the item preview, and again in the stored
operations and run requests (34.5 MB per row, 8.2 MB of it file text, 1.8 MB
unique). Generic proposals now accept contents: { id: { content | content_base64 } }
and content_ref on uploads and files. The stored operations and run requests
keep the reference; the apply step puts the content in place for the request
only, and the Changes page reads it from document.contents. Inline content
still works. Unreferenced contents are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Confirm, verify and recheck on a change proposal accept a personal API key
(osk_) next to the signed-in page; the request acts as the key owner. The
shared admin key, wrapper tokens and keys the platform issues to a running
session stay refused, so an agent never approves its own changes.

The approver snapshot keeps the key (id, name, prefix) and the client the
caller names in X-OpenSteward-Client, the session timeline says who
confirmed and with which key, and every confirm, verify and recheck writes
an admin_audit_events row. The Changes page shows who confirmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Twin keeps only a display name for the approver of a write, so a
confirmation made with a personal API key sends "<person> via API key
"<name>" (<client>)" in X-Approver, capped at Twin's 200 characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A generic call's expect could not name an earlier result: {{create.id}} stayed literal, so a
correct update was marked uncertain, and reconcile only knew a few fixed routes, which left the
proposal stuck with no way to submit a new revision. The runner now resolves references in
expect (without URL-encoding), and reconcile checks an interrupted PATCH or PUT that has an
expect by reading the same path back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Autonomous mode applied clean Twin Changes proposals and skipped dispatch
approval in every session of an autonomous agent, including runs a person
started by hand. It now acts only in a session its schedule started at the
planned time, or one an autonomous run dispatched without approval. A
manual start, a rerun, a schedule's Run now and an approved dispatch keep
every approval step; the session records why the proposal waits.

Scheduled sessions record their trigger (schedule_trigger), and a client
starting a session can no longer claim a schedule or dispatch origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The proposal row reads as applied before its change_proposal_applied
session event is inserted, so reading the event once right after
waitForApply races the apply. It passed while files ran one by one and
failed under parallel load. Poll for the event (bounded) and keep the
assertion on its message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
session-workflow.test.js held 153 tests and about 70% of the API suite's
test time in one file, so no amount of file-level parallelism could make
the suite faster than that file. Split it, unchanged and in order, into
eight files by topic:

- session-workflow-planner: turns, planner approval and feedback
- session-workflow-artifacts: preview wrappers, trace and package artifacts
- session-workflow-agent-config: agents, tools and credentials
- session-workflow-executions: manual executions, evaluations, documents,
  startup recovery
- session-workflow-tool-runtime: linked tools, runtime secrets, local
  collector
- session-workflow-review-and-replay: review limits, deployment drain,
  trace finalization and turn replay
- session-workflow-dispatch: worker dispatch, stale recovery, worker
  reports and engine checks
- session-workflow-environments: session environments and stopping runs

The shared before/after setup (the workflow test agent, the controlled
codex credential, the API server) and the helpers move to
test/support/session-workflow-harness.js; every file registers the same
hooks with registerSessionWorkflowTestHooks(). Test bodies and assertions
are moved verbatim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Several workflow tests kept a worker turn or session start in flight with
a fixed messageDelayMs / sessionCreateDelayMs and then acted inside that
window. The delay applied to every turn of the test, so it cost seconds
each run, and the window was still a guess: a slow host could outlast it
(these are among the tests that failed when CI was CPU-starved).

The mock worker now takes messageGate and sessionCreateGate hooks, and
createGate() gives tests a promise to release. The tests hold exactly the
response they act on and release it at the point the old window assumed:

- manual executions mark the session as running: hold the generator turn,
  wait until the worker has it, inspect the running session, release
  (6.7s -> 0.8s locally)
- session message turn requests serialize starts: hold the accepted turn
  until every pending-replay check has run (2.3s -> 1.1s)
- stopping a running generator: hold only the generator session start
  until the stop request has returned (3.8s -> 0.5s)
- stopping a generator mid-turn: hold only the generator turn until the
  stop has returned, then let the worker reject it (4.9s -> 0.6s)

Each gate also opens on its own after 30s so a failing test cannot leave
a worker response hanging.

The idle-environment deployment drain test waited the full 5s drain
timeout because other executions in the file stay active; the drain stops
idle environments in its first pass, before any readiness wait, so a 1s
timeout checks the same thing (5.5s -> 1.3s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restoring a runtime preview polls for the runner container every 2s and
for preview grants every 4s, up to six times after the restore command
ends. When the command fails nothing waits on the grant loop, which keeps
syncing in the background for ~24s; in the preview restore test that kept
the test process alive long after its tests had finished.

OPENSTEWARD_SESSION_PREVIEW_RESTORE_POLL_MS now replaces both intervals
when set. Each wait reads it once when it starts, and nothing outside the
tests sets it, so production keeps 2s/4s. The preview restore test sets it
to 25ms for its duration and polls the restore status every 50ms with the
same overall budget (environments file: ~33s -> ~8s wall locally).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API suite ran its files one at a time against one database
(--test-concurrency=1) because every file shares that database's state.
Files spend most of their time waiting on the database, sockets and
timers, so the runner now gives parallel files separate databases:

- run-test-files.mjs copies the migrated and seeded run database into one
  slot database per parallel file (`<run>_s<n>`, CREATE DATABASE ...
  TEMPLATE) and starts `node --test --test-concurrency=<n>` over
  test/**/*.test.js. The explicit glob also stops node from starting every
  helper under test/support as a test file of its own (6 empty processes,
  previously counted as 6 extra "tests").
- claim-test-database-slot.mjs is preloaded into every test file process.
  Before the test code imports the database client it claims a free slot
  with a Postgres advisory lock (on a connection held for the life of the
  process, so the slot frees itself however the process ends) and points
  DATABASE_URL at it. Processes a test forks keep their parent's slot.
- Concurrency defaults to min(4, CPUs - 1) (at least 2 on multi-core
  hosts), further capped by the server's free connections at ~24 per file
  (the heaviest files peak at ~21 across their query and lock pools), so
  the CI sidecar's default max_connections=100 gives 4.
  OPENSTEWARD_API_TEST_CONCURRENCY overrides it; 1 runs the files one
  after the other.
- run-api-tests-isolated.mjs drops the slot databases with the run
  database.

Files that share a slot run one after the other on the same database, as
every file used to on the single run database. A fresh copy per file was
tried first: creating and dropping ~100 databases forces a Postgres
checkpoint per drop and cost more than the parallelism gained.

The CI contract is unchanged: DATABASE_URL=... npm --prefix api test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profiling a workflow test showed the test process idle most of the time,
waiting on database round trips, and each workflow turn commits many small
transactions. On the run database and its slot copies the runner now sets
synchronous_commit = off (ALTER DATABASE ... SET), so a commit returns
before its WAL is flushed to disk.

That only gives up durability across a Postgres crash, which a database
dropped at the end of the run never needs; visibility, locking and
isolation are unchanged, and nothing outside these test databases is
touched. Locally the workflow tests got ~35% faster and the suite went
from ~63s to ~50s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pattern matched only the top-level folder, so api/node_modules and
worker-api/node_modules were sent with every build and copied into the app
image by COPY . . . No image needs them: the API and worker images install
their own dependencies, and the app only builds the frontend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/v1/worker/agent-sessions/purge takes up to 1000 agent session
ids and deletes their worker_agent_sessions rows; events and traces
cascade. It works on the Postgres and SQLite trace stores, reports ids
that are already gone as not found (idempotent), and leaves sessions the
worker still runs in memory. Worker machine auth applies as for the other
/api/v1/worker routes; the route stays closed in standalone mode.

The control plane calls it after an admin session cleanup deleted the
executions that ran in those agent sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/v1/admin/session-cleanup (and /api/admin/...) lists, and with
an explicit "dry_run": false deletes, sessions whose last activity (the
session row, its executions and its messages) is before the cutoff,
oldest first, plus finished standalone executions. older_than_days must
be at least 30, and every query also clamps the cutoff to 30 days before
the database clock, so data from the last 30 days is always kept.

Sessions with a live execution, an active runner or preview grant, or a
change proposal being applied are reported as skipped. Each session is
re-checked under a row lock and deleted in its own transaction with its
executions (traces cascade); session-context documents are archived as
DELETE /sessions/:id does. After the commit the session folder in the
runtime directory is removed (path-contained) and the worker purges the
stored agent sessions of the deleted executions; failures there are
reported in errors without undoing the delete.

An advisory lock allows one cleanup at a time (409 otherwise) and each
run writes one sessions.cleanup admin audit event. OpenAPI documents the
operation as cleanupSessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A how-to with curl examples for the dry run and the real run, what is
always kept and why sessions are skipped, plus an Administration entry in
the API surface reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
API test files run one after another on a shared slot database. The
cleanup fixtures left idle session runners behind, which the idle
environment reaper test in a later file then also stopped. The file now
deletes its agents, sessions, executions, member user and audit rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /sessions/:sessionId/export returns one JSON bundle (kind
opensteward.session.export, schema_version 1) with the session, its
messages, events, checklist items, evaluations, feedback requests,
artifacts with every version and stored file (base64 + sha256), and a
read-only record of executions and change proposals. Secrets, credential
references, share tokens, worker ids/URLs and host paths are redacted with
the agent definition key policy; redacted paths are listed. Traces and
per-execution snapshots are opt-in. Missing or corrupt stored files are
reported in warnings instead of failing the export.

POST /agents/:agentId/sessions/import creates a new, read-only, completed
session owned by the caller with new ids. Artifacts map to the target
agent's definitions by id, then by name; unmatched ones are skipped with a
warning. Files are re-verified and written under the normal runtime layout.
Executions are kept in a session_imported event instead of being
recreated. The route parses its own body (200 MB by default,
OPENSTEWARD_SESSION_IMPORT_MAX_BYTES) and work routes answer 409 on
imported sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds exportSession and importSession with the SessionExportBundle and
SessionImportResult schemas, and a test asserting the operation ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	api/src/openapi.js
#	docs/how-to/README.md
runSessionCleanup posts to the admin cleanup endpoint, importSession
uploads an export bundle file unparsed (with upload progress), and
buildSessionExportUrl builds the download link with the optional
traces and execution snapshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/admin/session-cleanup previews the cleanup with a dry run (sessions
with agent, last activity and runs, standalone runs, and kept items
with their reason) and only enables the delete for a preview made with
the same settings. Deleting asks for confirmation and shows the deleted
counts, the worker purge and any errors; a running cleanup (409) and
invalid settings are explained. The admin home links to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session page gets an Export action that downloads the bundle, with
options to include traces and execution snapshots. Sessions marked
metadata.read_only (imported exports) show an "Imported (read-only)"
badge and notice, and hide the composer, the manual run buttons, the
environment bar and the discard action, which the API refuses with 409.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Import session" on the agent page uploads a chosen export file to the
agent's import route, lists the warnings when there are any, and opens
the new read-only session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cleanup endpoint accepts optional session_ids and
standalone_execution_ids (UUIDs, at most `limit` each). With either
list the run only considers those ids and still applies the cutoff,
the 30-day floor, the agent filter and the skip rules; listed ids that
no longer qualify are reported as skipped with no_longer_eligible, and
a category without a list gets no candidates. Without the lists the
behaviour is unchanged. The audit row records both lists.

The admin page's Delete sends the previewed ids with the same
parameters, and the confirmation says exactly those sessions and runs
are deleted, each re-checked first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Admin-only endpoints under /api/v1/admin/backup:
- GET /admin/backup streams a gzip-compressed NDJSON backup of every API
  table (read in one REPEATABLE READ snapshot, parents first) plus the
  runtime data folders (agents/, sessions/, git/), with per-file sha256
  and an end record checksumming the whole stream. Credentials and the
  engine logins stored with them are included, still encrypted.
- GET /admin/backup/status reports whether the instance is empty, its
  migration and a fingerprint of the credential key.
- POST /admin/backup/restore streams a backup into an empty instance at
  the same migration: one transaction under table locks, staged and
  verified files moved into place before the commit, rollback and file
  restore on any failure, and a warning when the credential key differs.

Backups and restores write admin audit events without contents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An admin-only /backup page shows whether the instance is empty and
restorable, its database migration and credential key fingerprint. It
downloads a backup after the admin acknowledges that the file contains
credentials and engine logins, and offers a restore upload (with a
confirmation step, upload progress and the result with warnings) only
when the instance is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A how-to covering what a backup contains and leaves out, its format,
the security of a file that holds every credential and engine login,
the empty-instance rule, the restore steps with curl and the UI, and
what to do afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The status endpoint no longer briefly takes the backup and restore
locks to see whether they are held, so asking cannot make a backup
that starts at the same moment fail. Adds a test that only one backup
and one restore run at a time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	src/app/AppRoutes.jsx
#	src/app/shell-and-home.jsx
Some deployments run the worker API on the same Postgres database, so the
catalog picked up worker_agent_sessions, events and traces (run logs that can
be hundreds of megabytes) and counted them against an empty instance. They
and the worker's migration table are now excluded, with a test that creates
them in the test database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An imported session is a finished record: the API answers 409 to plan
approval, runs, continue, improvement proposals, questions and tool setup.
Messages in a read-only session now keep only the actions that open
something (artifacts, panes, links) and drop the question, tool setup and
sign-in links; the pending improvement proposals panel is hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
openapi.js had grown to 9901 lines, past its 9300-line budget. The agent,
agent tool and execution component schemas move to openapi-agents.js,
openapi-agent-tools.js and openapi-executions.js and are spread back in
place, so the generated document is unchanged (checked byte for byte).
openapi.js is now 6898 lines; its budget drops to 7500.

The size-budget script still listed api/test/session-workflow.test.js,
which was split into topic files; that entry is gone. Five other files had
grown past budgets that were never enforced since July; their budgets are
reset to the current sizes so the no-growth check passes and guards again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workspace panes of an imported session no longer get the handlers that
change a session, so plan approve and needs-work, the evaluation run
button, file upload and remove, and the suggest-improvement forms stay
hidden. Questions are listed as text with their suggested answers, and
tool setup requests show their title, status and summary without the
setup form. Files, artifacts and the plan stay viewable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A restore now runs the startup recovery itself after the commit, so runs
that were in progress at backup time are settled without a restart. Other
writes get an immediate 503 instead of waiting on the restore's table
locks (also across API processes, through pg_locks). A restore started
from a browser session signs its caller in again as the restored account
with the same sign-in identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The read-only guard listed the routes that start work; anything else
(questions, checklist items, change proposals, uploads) still went
through. It now refuses every write below an imported session except
renaming or deleting it, sharing an artifact and unlocking protected
content, and also guards the question and checklist routes that address
a row by its own id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A backup joined each fetch of 250 rows into one string. On Twin the
execution rows average 2 MB (up to 35 MB) and proposals reach 52 MB, so
that string passed the JavaScript string limit ("Invalid string length")
and the download stopped after it had started. Rows are now written in
blocks of about 8 MB, and each table starts with a small fetch that is
then sized to stay near 32 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backup file is built while it downloads, so the browser cannot show
its size or the time left. The backup now tracks how far it is, estimated
from the table sizes Postgres reports and the size of the runtime files,
and GET /admin/backup/progress reports it with the time left at the pace
so far and the compressed bytes sent. The backup page polls it and shows
a progress bar, the table being written and how the backup ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs, the environments agents leave running (prototypes, dev servers) and
the containers agents start all share the runner Docker daemon's memory.
The worker now measures it (docker stats, limits from the deployment) and
attributes each container to a run or a session environment. The home page
shows memory and CPU for runs and the sessions using them, with a Stop
button for environments; a session's Environment bar shows its memory.

A new run is refused with 409 run_memory_full while less than
OPENSTEWARD_RUN_MIN_FREE_MEMORY_BYTES (1 GiB) is free; a scheduled run is
recorded as skipped_memory. Messages to running runs are not affected, and
an unknown measurement never blocks work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel moves from the home page to the agent page above New session, and
Start session is disabled with the reason while memory is full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each session on the resources panel shows whether its agent is working or
waiting and for how long, and how long its environment has been up and when
it stops by itself (its lease). A Stop all button stops every listed
environment, and a How to free memory section opens by itself while new runs
are paused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prototypes, development servers and session environments are listed in one
place (top menu, /environments): status, who started it, uptime, when it
stops by itself or how long a paused one keeps its data, memory, preview
links and Stop, with a Mine filter; admins see all. GET /api/v1/environments
assembles them from the runner's containers, session runners and preview
grants, as step 1 of the Environments design.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It failed twice in CI (develop runs 4kgml and jvkrr) with the evaluation
session still awaiting plan approval when the 5 s completion grace ran out.
The grace only bounds how late the evaluator may answer after the 1 s session
timeout; 20 s keeps that meaning with room for the planner and generator
turns under load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stopping a session's environment only matched containers labelled as the
session's own children; docker compose containers carry only their project
label, so they kept running while the API reported the environment stopped.
The worker now also stops the session's compose project (its own project
name and the session's leases), and removes the containers when asked to
clean up a session it no longer remembers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every environment an agent leaves running (the compose project of its
session) now has a row in environments, with its status, lifetime, services
and history (environment_events). A sync every 30 s keeps the rows in step
with the runner and:

- starts environments again once after a release restarted the runner, so
  previews come back by themselves; ones stopped on purpose stay paused;
- pauses temporary environments after an hour without preview use or agent
  work;
- marks environments gone from the runner as removed.

People start, pause, keep longer (1-90 days, or for good for admins),
remove and read the logs of the environments of sessions they may open
(/api/v1/environments/:id/...). The worker lists environments including
stopped ones, acts on them by project, and keeps a lease until a hold time
that renewals no longer cut short. A release's drain stops idle runners'
agents but no longer removes their environments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An agent definition can declare environments (name, compose file, preview
ports, prepare command, timeouts, expected memory). Runs of such agents get
a session-scoped environment token (ose_, valid only while the run is
active) and the environment tool:

  node /workspace/run/opensteward-environment.mjs start|status|logs|stop <name>

start runs docker compose up --build --wait in the session's project and
registers the environment: OpenSteward records it under its declared name,
grants the declared ports it really publishes as previews and returns the
links. Its idle timeout and expected memory come from the declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 4: starting an environment needs its declared memory free besides the
minimum for runs, and each of its containers is capped at 1.5 times the
declared memory (docker update), so one runaway prototype cannot take the
runner. Declared previews are renewed while the environment runs.

Step 5: a session whose environment runs or is kept longer is no longer
removed by session cleanup, so its previews stay. People share an
environment's preview with a link that needs no sign-in (oes_ token, hash
stored, 1 h to 90 days, revocable, dead once the environment is removed):
POST/GET /api/v1/environments/:id/shares, DELETE .../shares/:shareId, opened
at /api/v1/environment-shares/:token/runtime-preview/:port/. The
Environments page gets a Share button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stack (api, back office, training and their database) is one environment
with one network. Declared ports can name their compose service, so apps on
the same port number are told apart; each becomes a page with its own label,
preview and share link. Each environment has its own page (/environments/:id)
with its pages, services and their logs, actions and history, and a session
whose agent left an environment running gets an Environment tab showing it.
GET /api/v1/environments takes ?session_id=, and GET /environments/:id
returns the full environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session page passes its own tab list, so the Environment tab never
appeared; it is now added to that list for sessions with an environment.
The empty files panel stretched its rows to the panel height, which blew
Add files up to a tall box; its rows now keep their natural height. The
environment bar keeps its four columns with the memory figure in the
Last changed cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session with an environment gets a tab per page (API, Back office,
Training) next to the Environment tab, with the app in a frame, Reload and
Open in new tab, and Start when the environment is paused.

Agent replies link their preview paths (/api/v1/sessions/.../runtime-preview/...)
instead of showing the markdown. In a preview, the app's own calls to
site-root paths such as /api/players reach the app instead of the OpenSteward
API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While paused the containers publish no ports, so the overview fell back to
the session's old previews, named Port N and pointing at stale host ports.
Declared apps now stay pages while down (without a link). Start records the
new host ports and opens their previews at once instead of at the next
sync, and previews are renewed as soon as the ports change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Page tabs are keyed by service and container port (api-3000, api-8080), so
two ports of one service no longer share a tab. The guide shows how.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A declared page can name a path (/admin, /docs), so one port serves several
pages; each page has a key (service, port and path) for its session tab, its
guide and its share link, and its link includes the path.

Guides: an environment and each of its pages can name a markdown file the
agent writes (what it is, how to use it). The environment tool sends them at
start, and `guides <name>` publishes them again; they show on the Environment
tab and above each app. Links by service address (http://api:3000/docs) open
that app's preview. Pages and the environment page get Copy link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a start on new host ports, the old ports kept their grant and the
service addresses (api:3000) kept mapping to them, so an app's links could
point at another app's old port. An environment's grants now replace its
earlier ones. On the Environment tab the guide box no longer stretches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In an app's tab the guide is a slim bar (remembered open or closed per
page) instead of taking 40% of the height over the app. The Environment tab
lists the pages before the stack's guide. Bare app addresses in a guide
become links named after the page, code stays as written, and an address
as a link's text no longer nests. The empty files panel has padding and Add
files no longer wraps.

When an environment's grants are replaced, the worker's grants for its own
containers on ports it no longer uses go too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On wide screens (4K) the environment tab, the environment page and the
Environments list stretched across the window, with each row's actions far
from its name. They now stop at 72rem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agents move between instances through agent definition import and export;
the self-contained standalone export is no longer supported. This removes
the export route and its OpenAPI schema, the standalone worker mode (runner,
UI routes, basic auth, secret store, api-wrapper proxy, sqlite session
documents and tool actions, data-retention reaper), the standalone React
client and session adapter, the standalone deployment fields in the agent
editor, the standalone view-model paths, their tests and docs, and the
build scripts. Managed behavior is unchanged: codex runs keep their auth in
the run's secrets, the worker always requires its machine token, and the
sqlite trace store keeps its file. The repo instructions now ask for
complete changes instead of managed/standalone parity.

"Standalone executions" in session cleanup (executions without a session)
is a different concept and stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session cleanup called executions that never belonged to a session (or
outlived a deleted one) standalone executions, which read like the removed
standalone export. They are detached executions now, in the API fields
(include_detached_executions, detached_execution_ids, detached_executions),
the cleanup page and the docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DELETE /sessions/:id cancelled a session's active executions and then left
all of them behind without a session (the foreign key sets session_id to
null). It now deletes them in the same transaction as the session and purges
their traces from the worker, as session cleanup already does. The worker
purge moves into its own module so both paths share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An apostrophe ended the single-quoted string, so the API could not load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A source_control agent tool serves a repository over git's smart HTTP
protocol. Git authenticates with the run token through a credential helper
set in GIT_CONFIG_* env vars, so neither the run token nor the git host
token lands in a checkout. OpenSteward judges every operation against the
tool's rules (action x branch glob x allow/approve/deny, with the API
wrapper's semantics), streams allowed fetches to the git host with the
stored token, answers refusals as text git prints after "remote:", and logs
each operation in source_control_calls.

Pushes are refused for now; standalone runs never receive the token env
var of a source_control tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every run of a session with a source_control tool mounts one Docker volume
at /workspace/repos. Before the first turn of a run the worker's
opensteward-repositories.mjs clones each repository once (blob:none, the
tool's source branch, the agent as commit author) or fetches it again, and
writes where each checkout stands to /workspace/run/repositories-status.md.
A tool's checkout folder is its `directory` setting or the repository name.

A new volume is refused while the runner's Docker disk is below
OPENSTEWARD_WORKER_MIN_FREE_DISK_BYTES (5 GiB). The volume is not part of
the session's compose project, so environments come and go without it; an
API sweep removes the volumes of sessions that no longer exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agents with a source_control tool show a base branch select per repository
on the start form, listing the branches the git host advertises (read with
the tool's token, default branch first). The choice is stored on the
session as metadata.source_control_branches and reaches each run as the
tool's source_branch, so the session's first clone checks it out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session page read every agent message and, when it held a setup word,
a source word and a route word, guessed a missing data source and showed a
"<provider> source setup" card. Ordinary technical messages matched: a plan
saying "session TODOs already exist as an API" became a "Session TODOs
Already Exist As source setup" card. Setup cards now come only from explicit
setup requests (and the Gmail connection check), and the source setup schema
config goes with the guess.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An environment declaration may name a repository: its compose_file and
guides are then read from /workspace/repos/<repository>, the session's
checkout, and the compose file can mount that checkout through
OPENSTEWARD_REPOSITORIES_VOLUME so code changes reload instead of
rebuilding. The environment tool reports how long the start took.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The environment tool's start --background starts an environment in a
detached process with its own log and returns at once. Planners that hand
over without plan approval start the agent's environments that way before
planning, so images, the database and dependencies are ready when the
generator needs them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vite's own CORS layer answered preflight requests before proxying them, so
a sandboxed runtime preview (origin null) never got the API's
Access-Control-Allow-Origin and its fetch calls failed with a network
error. The API decides CORS in production already; the dev server now
leaves it to the API too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tool manual is the agent's reusable notes, so the package check no longer
treats files it mentions as deliverables the run promised. And when a run
has no reviewer and no repair round left, an unsaved package file no longer
ends the session as "Review retry limit reached": the agent's answer
stands and OpenSteward adds a note below it naming what was not saved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenSteward reads a push's branch updates (create, push, delete) before
forwarding it and judges each against the tool's rules. An allowed push
streams to the git host with the stored token and is logged per branch; a
refused one is answered with git's own report, so the agent's git push
prints "OpenSteward refused <ref>: <reason>" and which branches it may
push to. One refused update refuses the whole push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After every turn the worker runs the repository script's new push step:
the working branch is pushed through the source_control tool when it has
new commits (never the source branch), and the repositories' state -
branch, commits ahead and behind, pushed or not - goes back with the turn
result. The API saves it on the session, and GET /sessions/:id/source-control
compares it with the git host: whether the branch is there and whether the
source branch has commits the branch does not have yet.

The session topbar shows the working branch, linked to the git host, and
a "Merge develop" button when develop moved on; it asks the agent to merge,
resolve conflicts and rerun the tests. Agents now name branches after the
feature (agent/due-dates), not agent/<topic> or a generic name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Environments now carry Docker's health check result per container. An app
whose container runs but fails its health check - a crashed dev server
waiting for a file change - shows "Not responding" instead of "Up" in
its session tab, with a Restart button, rather than a preview error page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It empties the environments table; run directly with node --test it used
the local development database and removed every environment record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session page asked only for the state the last turn saved, so a
session showed no branch until the agent ran again. The worker now reads
the session's checkouts straight from its repository volume (a throwaway
container mounts it read-only and asks git), and the API compares that
with the git host. The saved state is the fallback when the volume cannot
be read. An unpushed branch is shown without a link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A generic proposal may carry next_run: the text a person reads, how many
remain and the request of the next run. The Changes page shows it under the
summary; an admin gets "<apply label> and run next" on a clean draft, which
confirms the revision and starts a new session of the agent, and "Run next"
once applied. The rerun route takes the next run's request as user_input.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When autonomous mode applied a revision that carries next_run, the platform
starts the agent's next run at once with the request the proposal names, as
a run autonomous mode acts in again, so a backlog clears run after run until a
proposal leaves nothing. At most OPENSTEWARD_NEXT_RUN_CHAIN_MAX runs (ten)
follow each other; past that the session says so and a person starts the next.
A next run a person starts from the page keeps waiting for a person. Both read
as "Next run" on the session.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pull requests go through the same tool and rules as git: JSON calls with
the run token, judged as pr_read/pr_create/pr_comment/pr_merge against the
base branch, sent to Forgejo or GitHub with the tool's token and logged per
session. A merge under an approve rule asks the session first and is bound
to the pull request's head commit, so a later push needs a new approval.
Approve rules can name approvers (emails or role:<role>); anyone else
answering the question is refused. The session topbar links the session's
pull requests next to its branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The source_control tool gets its own panel: git host, repository URL and
default base branch, the rules as rows (action, branch, allow / ask
approval / deny), who may approve picked from the instance's users or
every admin, developer and deployment presets, and a tester that runs the
shared rule engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A developer agent's repository tool names its deployment agent; the
deployment agent's tool for the same repository lists deploy targets.
"Request deploy" in the developer session opens the pull request and
starts (or wakes) the deployment session with the branch checked out,
linking both. The deployment agent's /workspace/run/deploy-review.json
comes back with the turn; its session shows the risk and "Send to
developer" posts the ticked findings into the developer session. Both
settings are in the repository tool editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button reads "Review for deploy" and its panel lists the steps: a
pull request is opened, the deployment agent reviews it, the person
approves the merge or sends findings back, and only the merge deploys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session topbar named only OpenSteward. It now shows the agent's name
above the session title, linking to /run/agents/:id; the OS mark still
goes to the OpenSteward home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deployment session shows "Merge into <target>" next to the review:
the person merges, judged by the deployment tool's pr_merge rule and its
approvers, only at the reviewed commit, and the agent then gets a message
to check the deploy. The deployment preset puts merging in its own rule
group so the agent's run cannot merge or ask about it.

Also stop failing a resumed turn when it re-reports an unchanged artifact
under the run's file path: the version row keeps the definition's path,
so only different bytes at the same file path are a replay conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A repository with deploy targets shows "Branch to deploy" and "Deploy
into" on the start form, with the message optional. Starting opens (or
reuses) the pull request and starts the review session with the branch
checked out, so promotions such as develop into main start here too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rechecking an interrupted create compared every sent key with the record read
back, so write-only input (Twin's grant check dates, deprecated budget dates),
derived figures and reformatted timestamps kept it uncertain. Keys the read
leaves out or adds are no longer compared, and timestamps compare as instants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Start review in a developer session now goes to the new (or reused)
deployment session, where the review runs; its back link returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A branch that was merged already (or holds only what the target has)
opened an empty pull request. Both ways of asking for a review now compare
the branch with the target on the git host first and refuse with the
reason, and the developer session's button says so when the checkout is
not ahead of its source branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deployment session showed the developer's branch controls (including a
"Merge <branch>" that would ask the reviewer to merge the feature in), a
pull request chip, a back link and the review separately. It now shows
one group: the branch into its target with the pull request, the review's
verdict (or that it is running, or merged), Send to developer and Merge.
The developer session is linked from the Send to developer panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
By severity (high, medium, low), then changes before risks before
questions: in the stored review, the Send to developer panel and the
message the developer gets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Send to developer the deployment session's header says how many
findings went back and that it waits for fixes; the button becomes Send
again. A new review request on the same session clears the old review,
so the header shows the review running until the new one is in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Developer preset already refused them because only agent/** was
allowed; explicit deny rules keep the deployed branches closed when a
broader push rule is added later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An environment can be a reference { name, from, environment } to the
environment another agent declares. It resolves when the run asks for it,
with the repository mapped to this agent's own checkout of the same
upstream, so a deployment agent runs the developer's stack from the
branch under review without a copy that drifts.

A deployment session with a running environment gets Open app on its
deploy review, to try the branch before merging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
niels merged commit f735fe5827 into main 2026-10-13 10:32:25 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
opensteward/OpenSteward!5
No description provided.