- name
- ops-workspace
- description
- An isolated place to build and test one change — worktree, database, ports, dependencies — and the teardown that removes everything it created. Prepared and torn down by `ops-change`, never by a loop, so how isolation is provisioned stays a product fact. The framework default uses a plain git worktree; a repo needing a seeded database or a free port ships its own. Called by name with (action, context-json). NOT for direct use — never select it from a description match.
# ops-workspace
One change, one clean place to build it.
**Visibility: supporting primitive.** Only **`ops-change`** calls it. A framework loop must
not — the loop does not know whether this repo needs a worktree, a container, a seeded
database, or nothing at all, and the moment a loop decides that, the engine has learned a
product fact.
## Invocation
```
ops-workspace <action> '<context-json>'
```
| Action | Context | Returns |
|---|---|---|
| `prepare` | `{ branch }` | `{ ok, path, reused, fidelity, notes }` |
| `teardown` | `{ path }` | `{ ok }` |
An absent context is `{}`. **Reject any other action.**
## Action: `prepare`
Create an isolated workspace for that branch and leave it **ready to build** — not merely
checked out. Whatever this repo needs before its build command works (dependencies restored, a
database seeded, a port claimed, credentials in place) happens here, because the alternative is
`ops-change · implement` discovering it halfway through.
**Idempotent.** An existing workspace for that branch **MUST** be reused and returned with
`reused: true`, never duplicated. Two worktrees on one branch is a corruption, not a
convenience, and a re-fired routine will ask twice.
**Framework default:** a git worktree off the branch, and nothing else. That is enough for a
repo whose build needs only a checkout, and honestly insufficient for one that needs a
database — which is why those repos override.
**In a cloud routine there is often nothing to do.** The session already has its own isolated
checkout and no sibling work to collide with. Return that checkout's path with
`reused: true` rather than nesting a worktree inside it. A workspace capability that insists on
creating something is worse than one that recognises it already has what it needs.
### Find out what the environment already provides, before provisioning anything
A cloud environment is **built once and reused**, and its build step may already have installed
the SDK, cached a container image, or started a service. A `prepare` that probes blindly or
reinstalls wastes minutes of every run and can end up with two versions of the same thing.
So **read first, provision second**. In a cloud environment built by the engine's stub, that
record is **`$HOME/env-manifest.md`**, written by `cloud-env-setup.sh` on every build: the .NET
SDK it installed, whether the SQL Server image is cached, and where the session script lives.
Read that and treat it as authoritative. If there is no manifest, say so and fall back to the
repo's own documented setup rather than guessing. Report the difference in `notes`: what was
already there, and what you had to create.
**To bring Umbraco up**, a repo's `prepare` calls the script the manifest names:
```
bash $HOME/.umbraco-ops/run-umbraco.sh --provider <sqlite|sqlserver> --boot "<this repo's start command>"
```
`--boot` is the one thing only the repo knows — how its site starts — and it must honour
`ASPNETCORE_URLS` (for `dotnet run`, pass `--no-launch-profile`). For `sqlserver` the script
starts the container and hands the site `ConnectionStrings__umbracoDbDSN` through its
environment; for `sqlite` the site keeps its own default. Choose the provider by `fidelity`
below: `sqlserver` where the manifest says the image is cached, because that is what CI tests
against; `sqlite` is `reduced`.
"Available but not running" is not "unavailable". A cached container image with a stopped
daemon means **startable** — start it. Reinstalling because a service was not already running
is the most expensive way to get this wrong.
### Say how faithful the workspace is: `fidelity`
Where a repo has more than one possible test target — the one CI uses, and a lighter, faster
stand-in — `prepare` **MUST** say which one it produced:
| `fidelity` | Means |
|---|---|
| `ci-parity` | this workspace tests against what CI tests against. A pass here predicts a pass there |
| `reduced` | a faster stand-in: a different database engine, a stubbed dependency, a smaller fixture set |
**Default to `ci-parity`.** A `reduced` target is a last resort — for a quick smoke of one
focused change, or when the parity target genuinely is not available here — because a different
engine produces both false failures *and* false passes.
**A `reduced` pass is not evidence CI will pass**, and `ops-change · verify` must carry that
through to whatever it reports. Silently downgrading the test target and reporting a clean pass
is worse than not testing: it spends the loop's trust on a result that did not earn it.
A repo with exactly one test target reports `ci-parity` and never thinks about this again.
## Action: `teardown`
Remove the workspace **and everything `prepare` created** — the worktree, the database, the
container, the claimed port. A teardown that leaves the database behind is why the tenth run
fails on a name collision.
**MUST succeed when there is nothing to remove.** Teardown is reached from failure paths, and
a teardown that errors because the thing was already gone turns one failure into two.
**Never remove anything you did not create.** Not the main checkout, not a branch that has not
landed, not a sibling workspace. A `teardown` that guesses is a data-loss bug with a routine
running it unattended.
## Rules
- **Never called by a loop.** If a loop is preparing a workspace, `ops-change` has been
bypassed.
- **`prepare` leaves it buildable**, or reports why it could not.
- **Both actions are idempotent**, and `teardown` is safe on an absent workspace.
- **Teardown destroys only what prepare made.**
- **One workspace per change.** Never two changes in one tree — the whole point is that a
failing build belongs to exactly one change.
View on GitHub