Skip to content

Latest commit

 

History

History
2251 lines (1959 loc) · 124 KB

CHANGELOG.md

File metadata and controls

2251 lines (1959 loc) · 124 KB

Changelog

All notable changes to this repository should be recorded here. Sections correspond to released git tags; Unreleased collects work merged after the latest tag.

Unreleased

v0.22.0 — 2026-10-13

Added

  • Git sources can declare several remotes (remotes:), with one push_default and one canonical remote (phase 2 of .agents/plans/jj-native-operator.md, steps 1-4). A remote is a bare URL or a mapping with url, canonical and branches; repo: still declares the one remote origin.
    • A new cache, or clone-mode slot, is cloned from the canonical remote (else origin, else the only remote) under that remote's name, whatever git's clone.defaultRemoteName says. It gets the other remotes, remote.pushDefault, and a push URL on the canonical remote that makes every push to it fail (angee-fetch-only://<name>). A cache is configured in a staging directory beside its path and moved into place only then, with a plain clone's permissions. A symlinked cache_path is cloned into its target, and an existing empty directory (a mount point, say) in place, configured right after. A path that holds anything else is refused.
    • angee source fetch and source pull reconcile an existing cache's declared remotes, push block and push default, warning for each change. Under remotes: they also make default_ref track the clone remote, so a cache cloned under repo:, whose origin remotes: now names a fork, pulls from the canonical remote. Bring-up and workspace provisioning only warn about differences, as they already did for origin. Undeclared remotes are never touched, and a repo: source never reads or writes push URLs or tracking.
    • workspace status and workspace repair warn when a clone slot's (mode: clone, or a git slot with no mode) remotes differ from the manifest's, such as a remote marked canonical after the slot was created. They change nothing.
    • The manifest refuses repo and remotes together, a push_default that is undeclared or canonical, two canonical remotes, branches on the canonical remote, invalid remote names or names differing only in case, relative URLs under remotes, URLs (repo included) with control characters or surrounding spaces, invalid branches patterns, and several remotes with no clone remote. A workspace template's inline source is checked the same way before anything is cloned. The mapping form accepts << merge keys, as the rest of the manifest does.
    • angee workspace source publish keeps to the remotes (step 5): see Changed.
  • angee doctor checks for what jj slots will need (phase 5 of .agents/plans/jj-native-operator.md). A new tool.jj check reports jj --version and whether jj workspace add --colocate exists, and tool.git warns when git is older than 2.42. Both are warnings, never errors, since no stack can declare a jj slot yet. The jj driver behind them (internal/vcs/jjdriver) reads a slot's status and diff, is tested in CI against the jj commit in .github/jj-rev, and is not reachable from a manifest.

Changed

  • Publish picks its remote from the source and keeps to its rules (phase 2 step 5 of .agents/plans/jj-native-operator.md).
    • Without --remote (REST and GraphQL: no remote), publish goes where git itself would push the branch: its pushRemote, else remote.pushDefault (which push_default sets in the cache and in clone slots), else the remote it tracks, else origin, else the only remote. It used to go to origin whatever the source declared. When git names no remote, as with several remotes and no push default, publish is refused with git's reason (INVALID_INPUT, field remote) instead of guessing.
    • A remote the slot doesn't have is refused (INVALID_INPUT, field remote); when the slot's own push configuration named it, the error says so.
    • A publish to the canonical remote is refused (INVALID_INPUT, field remote). It used to fail at the remote's push block, after creating a detached slot's branch, which a retry then tripped over.
    • A branch that matches none of the target remote's branches patterns is refused (INVALID_INPUT, field branch).
    • All of these are checked before a detached slot's branch is created or anything is pushed.
  • The base ref is read from the source's clone remote. A slot's base (its ref, else default_ref) was read from origin/<ref> first and then from the other remotes in an unspecified order, which varied between calls. It now comes from the remote branch git has the local branch track, then the clone remote's, then the other remotes' by name. This decides a new worktree slot's start, status counts and sync-base. A repo: source resolves as before unless its local branch tracks another remote.
  • The workspace slot verbs (fetch, pull, push, workspace push, sync-base, publish, merge, rebase, and abort and continue) go through the slot driver (phase 4b of .agents/plans/jj-native-operator.md), with no change in behaviour, except that a failed fetch (in a slot fetch, pull or sync-base) names the source's clone remote rather than origin when the source declares remotes:. The slot driver also decides what a push and a publish do (phase 4d), with no change in behaviour. It also creates and removes slots (phase 4c): a worktree's staging, install and registration repair, a clone, and removal with git worktree prune. The git commands are the same, except that a workspace with several worktree slots of one source prunes once per slot rather than once per source.
  • A workspace no longer renders a stack (#110). A workspace and a stack are separate concerns: a workspace is files (its rendered template and its source slots), and a stack's services and jobs reach it through workspace:// mounts, workdir and env.
    • A workspace template that declares chain or chain_root is refused with INVALID_INPUT, naming #110, before anything is materialized: on workspace create, workspace update, create preflight and template. templates leaves such a template out. A stack template's own chain: is unchanged.
    • The operator no longer reads a stack inside a workspace: workspace status, the topology and the snapshot stop querying its Docker and process-compose. That stack's own operator reports it.
    • A stack inside a workspace no longer reads its parent: stack update --template takes its ports from its own answers file, like any stack, where it used to override them with the parent's workspace allocations.
    • A workspace created from a chaining template keeps its files, the stack inside included, now an ordinary stack at that path. Its status reports no inner stack and workspace destroy works as before, but workspace update refuses it with INVALID_INPUT naming #110: a render of the workspace template alone would overwrite or delete what the chain rendered, and an update cannot switch the recorded template. Destroy it and create it again. The record is left as it is.
    • angee doctor reports a chaining workspace template as an error.
    • workspace destroy --purge deletes a workspace's directory without stopping a stack that lives inside it, which the operator does not know about; stop such a stack first. The guide now says so.

Deprecated

  • Workspace inner-stack fields and workspace logs (#110), removed in a later release once the consoles in use stop selecting them:
    • innerStack (REST inner_stack) is always null, and innerError, chain and chainRoot (REST inner_error, chain, chain_root) are always empty, on workspace status and refs. REST and --json keep sending "chain": [] until the fields are removed; before, every workspace sent its own template there;
    • workspaceLogs, onWorkspaceLogs, GET /workspaces/{name}/logs and angee workspace logs return nothing, since a workspace has no logs of its own; the CLI command prints a deprecation note on stderr.
    • angee workspace status no longer prints chain_root, inner_stack or inner_error lines.

Fixed

  • stack update --template finds the answers file of a stack whose ANGEE_ROOT has more than one segment (#128). copier writes the answers file in the directory ANGEE_ROOT is relative to, two levels above the stack root for deploy/.angee. The update looked only in the stack root and its parent, so it refused such a stack with "missing answers file".

    • It now walks up from the stack root, nearest first, and takes the first answers file whose recorded ANGEE_ROOT puts the stack at the stack root by the rule stack init places it with. No ANGEE_ROOT means the file's own directory, an absolute one names the stack root itself, and a relative one is joined to the file's directory, ./deploy/.angee and deploy/.angee/ included. A relative one that climbs out with .., which stack init refuses, never matches. stack update --template -i reads the answers the same way.
    • Any other answers file on the way is skipped, such as an unrelated project's further up, or one in the stack root that names another root. The update used to take a file in the stack root whatever it recorded. Only a skipped file's ANGEE_ROOT is read, so its other answers can hold anything, and files above the stack's own are never read.
    • A file on the way that can't be read as a YAML mapping stops the update with INVALID_INPUT on template, naming the file, since it might be the stack's own answers, damaged. So does an answer in the stack's own file that is neither a scalar nor a list, which used to be an internal error.
    • This covers templates that declare ANGEE_ROOT as a copier question, and templates that leave it out of the questions with the default ., as angee-django's stack templates do: copier records no ANGEE_ROOT for them, and the stack sits beside the answers file. A template that declares ANGEE_ROOT only under _angee.inputs with another default, such as .angee, records none either, and its answers still can't be found, as before.
    • Paths are compared cleaned, without resolving symlinks, as the parent check did. A --root that runs through a symlink to a directory between the stack root and the answers file does not find them. A stack root or render directory that is itself a symlink is refused when the render opens it, as before.
  • stack update --template no longer moves path inputs (#121). It read a type: path answer from the stack's answers file, where copier recorded it already relative to ANGEE_ROOT, and resolved it again as if it were new input relative to the directory copier renders into. On a stack whose ANGEE_ROOT is not ., every update moved the path up by the depth of ANGEE_ROOT: under .angee/, PROJECT_PATH: .. became ../.., then ../../... Stacks with ANGEE_ROOT=., as the angee-django templates use, were not affected.

    • A recorded answer is now replayed as it is, and so is a value passed on the update that equals the recorded string, as the -i form and the immutability check already take it. Any other value passed on the update (--input, --answers, or a field changed in -i) is resolved once, as stack init resolves it: relative to the directory holding the answers file. So is the default of a path question the template gained since the last render.
    • The -i form shows a recorded path relative to ANGEE_ROOT, but reads an edited one relative to the directory holding the answers file.
    • A chained template that inherits one of the stack template's own path inputs unchanged takes the recorded answer as it is too. Any other input it inherits, such as one the stack declares type: str, and any input its chain entry sets are resolved for that layer, as at init.
    • Known limits: two chain layouts still move a chained template's path input once, on the first update, and keep it there (before, they moved on every update). One is a chain entry that sets its own ANGEE_ROOT: with ANGEE_ROOT: ".", app becomes ../app. The other is a chain entry that forwards the stack's path input: with PROJECT_PATH: "${inputs.PROJECT_PATH}", ../app becomes ../../app. No shipped chained template declares a path input.
    • A stack that has already been updated keeps the moved value in its answers file. Pass the right one once with angee stack update --template --input PROJECT_PATH=<path>, relative to the directory holding the answers file. An immutable path input refuses a new value: edit the answers file by hand. A question that changed from type: str to type: path is replayed as recorded, the raw value, and the same --input repair fixes it; spell the value differently from the recorded string (./app for app), since an equal value is kept as recorded.
  • A log read's byte limit keeps the newest whole lines of the whole read (#118). stackLogs(services: [api, worker], limit: 60) returned api | api-1495 … api \n[truncated]: cut mid-line, with nothing from worker. The process-compose backend capped each process before labelling it, the GraphQL field then kept the start of the joined text, and the container backend kept the oldest bytes of its output.

    • The limit now applies once, to the labelled read of both runtimes, in the platform: the runtimes return every line the tail asks for. It keeps the last whole lines, cut at a line boundary. A read it shortens starts with a [truncated] line, which counts toward the limit. The marker used to come last, after the limit. A limit of 12 bytes or less has room only for the marker, so a read it shortens is the marker alone, longer than a limit below 12.
    • "Newest" means last in read order, not in time. Container lines come first, as docker compose orders them, then local lines grouped by service in the order requested. So with [worker, api] and a small limit, api's last lines are kept and worker can be left out.
    • angee job run keeps a local job's output within 1 MiB by the same rule. The remote client's limited read, served by /stack/logs, does too.
    • The platform holds only the last limit + 1 bytes of a read while it collects it, and a failed docker command's error carries the last 4 KiB of its output, cut on a line boundary, instead of all of it.
    • The CLI and the REST log endpoints take no byte limit, as before.
  • The process-compose API token is also read from PC_API_TOKEN_PATH (#118). Only PC_API_TOKEN was read, so a supervisor given its token through a file answered 401 to log reads, to process updates and to GET /live, and the stack could not be brought up. angee now reads the token as process-compose v1.120.0 does (GetApiToken in src/config/config.go): PC_API_TOKEN when it is set and not empty, as it is; else the contents of the file PC_API_TOKEN_PATH names, with surrounding whitespace trimmed. A file that can't be read gives no token, as in process-compose, and the supervisor answers as it will. A relative path is read from the stack root, where angee runs the supervisor. When the env file sets a key twice, the last one counts, as it does for the supervisor.

  • process-compose down and list get the stack's env file. They ran with angee's inherited environment alone, so a token set only in the stack's env file never reached them. angee status showed local services as unknown, a local job run failed, angee down failed, and angee dev waited for the supervisor forever. A missing env file behaves as before.

  • Stacks and the CLI take turns on a shared source cache (decision D9 of .agents/plans/jj-native-operator.md). The operator holds a lock file beside a git source's cache, .<name>.angee.lock (beside a symlinked cache_path's target), while it changes the cache's worktree registrations or talks to its remotes: cloning or fetching the cache (bring-up, source fetch), source pull, source push, and adding or pruning workspace worktrees (workspace create, repair, destroy --purge, and a failed create's rollback). Two stacks sharing a cache, or a stack's operator and the CLI, could run these at the same time, so one could prune a worktree the other was still installing, or remove the other's clone staging directory.

    • One now waits for the other, with a warning that another angee process holds the cache, and gives up after ANGEE_LOCK_TIMEOUT when that is set, with a CONFLICT error. A rollback that cannot get the lock still removes what it created and leaves the prune to the next change.
    • The verbs on a slot (fetch, pull, push, sync-base, merge, rebase, publish) take no lock: git keeps each ref update whole, and they never prune registrations.
    • The directory holding the cache must be writable, for the lock file. A cache_path bind-mounted on its own, whose parent the operator cannot write, now fails with an error saying to mount the parent directory instead.
  • Worktree slots work when the user's git writes relative worktree paths. With worktree.useRelativePaths = true in git's config (git 2.48+), a mode: worktree slot got a .git file pointing relative to the temporary directory it was staged in, so once installed at its path every git command in it failed with "not a git repository". The add also set extensions.relativeWorktrees and core.repositoryformatversion = 1 on the cache, and git before 2.48 and go-git refuse to open a repository with that extension, the cache and every slot of it.

    • Worktrees are now added with absolute links (git -c worktree.useRelativePaths=false worktree add; older git ignores the key), so the slot works and the cache's repository format is left as it was.

    • A cache hit before this fix keeps the extension, and its slots stay broken: this release changes neither. To return the cache and its slots to absolute links, run, in this order:

      git -c worktree.useRelativePaths=false -C <cache> worktree repair <slot>...
      git -C <cache> config --unset extensions.relativeWorktrees

      Name each slot by its absolute path: with -C, git reads a relative path from the cache's directory. Whichever slots it is given, the repair checks every worktree registered in the cache and fixes each broken one.

v0.21.3 — 2026-10-11

Changed

  • Services restart on failure by default (#111). A service that exited stayed down until someone restarted it, because neither generated file carried a restart policy. Services now take restart: on_failure | always | no, and the default is on_failure, so on upgrade every service that sets nothing restarts after a non-zero exit, a crash or an outside kill.

    • Each runtime applies its own policy, with its own backoff. process-compose gets an availability block and waits its default second before each restart. Docker Compose gets restart: on-failure, or unless-stopped for always, and doubles its own wait up to a minute.
    • Neither limits the number of restarts. process-compose counts every restart since the process was last started, so a limit would eventually leave a long-running service down for good. A crash loop's output stays bounded by each local service's rotated run/logs/<name>.log (#113).
    • A stop either runtime was asked for (angee down, service stop, service restart) never triggers a restart.
    • Jobs take no restart; it is refused on a job.
    • The edge container restarts on failure too.
    • process-compose now restarts a local service whose readiness probe fails, where it used to leave it stopped.
    • Set restart: no to keep the old behaviour for a service.
    • On upgrade, the first angee up recreates container services, because Compose sees a changed definition. A running process-compose supervisor applies the policy when Angee next starts or restarts a process, or after angee down.
  • A crash loop is visible.

    • A service waiting out its restart backoff has the outcome restarting, counted under summary as restarting (REST and GraphQL). A local service also reports the exit that ended its last run and restarts, process-compose's restart count; Docker's status reports neither for a container.
    • angee status lists restarting services under the counts, and angee service list shows restarting (exit 1, 3 restarts) or running (3 restarts).
    • A pending job or service waiting on a service the runtime keeps restarting has a reason naming it. A running service counts only when it has a readiness probe, the only case in which process-compose holds its dependents back.
    • angee dev and angee dev -d print that reason once while they wait for jobs, and a service still restarting when they finish may be in the summary line: it is, when the last status read finds it in its backoff.
    • To apply a fix to angee.yaml to a crash loop, run angee service restart <name>. angee service up <name> now also passes a changed definition to a local service caught in its restart backoff, where it used to leave it alone; once the service has relaunched, up sees it running and leaves it. A job waiting on the service is skipped when process-compose replaces it.
  • Workspace and source status agree with workspace destroy on whether a source is pushed. They computed it separately. Status now uses destroy's rule from v0.16.0: a source on a detached HEAD counts only commits that no branch, remote branch or tag holds, and a source without an upstream counts only commits that neither its base ref nor its remote counterpart (<remote>/<ref>, origin's first) holds.

    • After sync-base, a source reads ahead of its local base but pushed, where it used to read unpushed while push and destroy had nothing to do.
    • The unpushed reason follows the same rule. A diverged source without an upstream says "N commit(s) ahead of base ref main with no upstream", and a detached one "N commit(s) on a detached HEAD that no branch holds".
    • The topology's unpushed count and workspace repair go by pushed, so a clean detached source with commits of its own is counted and named. Repair describes an ahead-but-pushed source as "ahead by N commit(s)".
    • The ahead, behind and diverged states are unchanged.
  • Slot reads go through a driver interface (internal/vcs, with a git driver), the first step towards jj-backed slots (phase 4a of .agents/plans/jj-native-operator.md, now committed). Source caches are read through the same git driver.

  • A failing source diff is reported as GIT_FAILED with git's message, where it was INTERNAL; the HTTP status is unchanged.

Fixed

  • A service that exits non-zero reads as failed (#112). process-compose reports a process that exited as Completed whatever its exit status, and the service state passed that through, so a service that exited 1 looked finished while the summary counted it as failed.
    • Service states keep the runtime's status and add outcome, the verdict the summary counts (running, completed, failed, stopped, skipped, pending, never-run or unknown), and exit_code. This is additive in REST and GraphQL (outcome, exitCode). outcome can be filtered, ordered and grouped on like status.
    • A failed service whose command could not start has the reason could not start.
    • angee status lists each failed service under the counts, and angee service list shows failed (exit 1) in place of the runtime's status.
    • A local service killed by a signal still reads as stopped: process-compose reports exit code -1 for a crash by signal, a SIGKILL (the OOM killer's included) and a deliberate stop alike.
  • angee logs shows the newest lines (#113). With no --tail, process-compose process logs replays the supervisor's whole 1000-line buffer over a websocket that drops lines once a client falls about 256 lines behind. The read returned the oldest ~256 lines and missed the newest.
    • A local service or job is read through the supervisor's log range endpoint (GET /process/logs/{name}/0/{n}), which returns the newest lines whole in milliseconds; process logs took about a second per process. Several local services are read at once. A follow still runs process logs --follow --tail N.
    • Log reads and follows, from the CLI and the operator, ask both runtimes for the last 200 lines by default. Container logs used to return the whole history. The log stream endpoint's default backlog is 200 lines too.
    • angee logs, service logs and job logs take -n/--tail, the REST /stack/logs and /services/{name}/logs take ?tail=, and the GraphQL stackLogs, serviceLogs and onServiceLogs take tail, up to 10000. A local service holds its last 1000 lines. Following a local service or job replays at most 256 lines; a larger tail is refused with an error naming its log file. The log socket refuses a tail that is not a number instead of reading it as the default.
    • A local service the running supervisor has not loaded (declared after it started) is reported at once as not found (NOT_FOUND, REST 404), where process logs waited on it forever. A read while process-compose replaces a service with its updated definition is a CONFLICT that asks for a retry.
    • angee job run shows every line the supervisor holds of a local job's run (the last 1000), where it showed the oldest 256, and a failure names the job's log file.
    • Logs of several local services show every one of them. Only the first was shown, because process-compose process logs reads one process. When a read covers more than one service, every local line is labelled with its service as docker compose labels its own.
    • Each stack's process-compose supervisor logs to run/process-compose.log under the stack root, without color codes, unless PC_LOG_FILE names another file. Before, every stack logged to one shared file in $TMPDIR.
    • Each local service and job also writes its output to run/logs/<name>.log through process-compose's log_location: one plain text line per output line with its time and level, flushed as it arrives and rotated by process-compose at 10 MB with three rotated files kept. Output past the 1000-line buffer, or after the supervisor exits, is no longer lost.
  • workspace destroy let a clone source go while commits only its own branches held, on a detached HEAD or without an upstream. A clone is a repository of its own, so its branches go with it. Destroy and status now count, for a clone, every commit that no remote branch or tag holds ("N commit(s) on a detached HEAD that no remote branch or tag holds", "N commit(s) that no remote branch or tag holds, with no upstream"). Tags count because git keeps fetched tags with local ones, so a clone pinned at a release tag still reads pushed. Source cache status uses the same rule. Worktree sources keep the rule above: their branches stay in the source cache. A clone without an upstream whose base branch was deleted can now be destroyed when every commit is on a remote branch; it used to be refused.
  • workspace status reported a source whose base exists only on a remote (origin/<ref>) in the cache as an error; it now counts against that remote ref.

v0.21.2 — 2026-10-09

Security

  • Values from requests reach git as arguments only. The workspace source verbs passed a ref, branch or remote from the API to git as given, so a value shaped like a git option ran as one. A rebase ref could run a command as the operator's user, a push ref or publish branch could mirror the remote's branches, and a diff ref could write a file on the host.
    • Those values, and the refs, repositories and paths angee passes to the same git commands from templates and the manifest, now follow --end-of-options. Git refuses an option-shaped one as a bad revision, refspec or reference.
    • This covers merge, rebase, push, publish, diff, sync-base, worktree creation, ahead/behind and commit counts, and template clone.
    • Template refresh resolves its ref to a commit before checking it out, because checkout before git 2.44 does not honour --end-of-options.
    • Publish pushes its branch as refs/heads/<branch>:refs/heads/<branch>, so a branch carrying refspec syntax, such as :<branch> (which would delete it), is refused. A push --ref is still passed as a refspec.
    • It needs git 2.24 or newer. Each form was checked on git 2.24.1 to 2.50.

v0.21.1 — 2026-10-09

Fixed

  • Templates that include files outside their directory render again. copier-go v0.1.0 (in v0.21.0) confines {% include %} to the template's own root, so angee-django's stacks, which include the collection's _shared files, failed with "unable to resolve template". angee now passes the template's _angee.include_root to copier-go v0.1.1 (WithIncludeRoot) for every render, after checking that it is a relative ancestor of the template and not the filesystem root. Include names resolve against the template root, as upstream Copier resolves them, so a stack includes ../_shared/AGENTS.md.jinja, not ../../_shared/… (angee-django updates its templates to match). For a workspace chain template, the pinned snapshot's copier.yml must still declare the include_root it was pinned for, so a declaration rewritten while pinning cannot widen the sandbox beyond the snapshot.

v0.21.0 — 2026-10-09

Added

  • when: in the template input form. A question whose condition is false, given the answers before it, is no longer asked: the interactive form hides it, line prompts skip it, and it takes its default, as copier answers it. A value passed for it is not validated. copier-go evaluates the condition.

  • ~ in type: path inputs. ~ and ~/… expand to the home of the user running the render before the value is made relative to the stack root. The expanded absolute path is what the answers record.

  • Template validators are enforced. copier-go (now synced with upstream Copier 9.18.2) runs a question's validator: on every answer the render uses: inputs passed with --input or by --yes, and the defaults it falls back to. A failing validator, a value outside a question's choices, or a required question with neither an input nor a default fails angee init, stack update --template, workspace create and service create with code INVALID_INPUT, and copier's message names the question. angee init --yes used to accept, for example, serve_mode=production with runtime_mode=process.

Changed

  • Template defaults are left to copier. angee passes copier the inputs given, generated values and rewritten path inputs, but no longer a template's plain defaults. A templated default ("{{ name|lower }}") is rendered instead of being taken as its literal text. On stack update --template, an answer recorded last time is kept rather than reset to the default. An inactive or secret question's default is not validated.
  • copier-go is bumped to v0.1.0, its sync with upstream Copier 9.18.2. It adds the ask: question key, and the trust list matches a URL with percent-encoding, backslashes or doubled slashes only exactly. A template whose _min_copier_version is newer than 9.18.2 now fails with code PRECONDITION_FAILED and cause template_too_new; before, the check was skipped. copier records only answers to questions, so angee keeps a service's workspace in its own render state. Services rendered earlier still read it from their answers file.
  • A read-only bind mount must exist. A bind://…:ro mount compiles to compose's long syntax with create_host_path: false, so a missing host path fails the container's start (bind source path does not exist) instead of Docker creating an empty root-owned directory, for example a ~/.ssh that would then hide a later SSH setup. Writable binds are unchanged.

v0.20.0 — 2026-10-09

Added

  • More host names per path-routed stack. ingress.aliases lists host names the caddy edge serves as the same site as ingress.domain, each with its own automatic certificate under tls: auto. The domain stays the canonical host for route URLs. Each alias must be a host name, not the domain, and unique; it needs ingress.type: caddy and routing: path.

v0.19.0 — 2026-10-09

Added

  • An error contract for every failed operation. GraphQL errors carry extensions.code (NOT_FOUND, INVALID_INPUT, CONFLICT, PRECONDITION_FAILED, GIT_FAILED, TIMEOUT, JOB_FAILED or INTERNAL). When they apply, they also carry:

    • a cause such as uncommitted_changes, slot_missing or merge_conflict;
    • the operation and the workspace, slot, source, job or service it concerns;
    • the masked remote;
    • the paths involved;
    • a hint;
    • git's redacted output as detail (at most 4 KiB);
    • the request_id.

    REST error bodies carry the same keys. Causes come from angee's own state and the step that failed; git's and ssh's diagnostics are passed through, never interpreted. See Errors.

  • Messages that say what failed and what to do. For example, Sync base for workspace "src": source slot "django" has uncommitted changes in addons/.../operator.graphql. Commit or restore the changes, then retry. A git failure names the action and remote, then quotes git's own line, instead of leading with git's raw output.

  • Checks before git runs. workspace sync-base and workspace push check every slot before touching any, and sync-base fetches every slot before merging any, so a refusal never leaves the workspace half done. The slot verbs and source push refuse up front, each with its own message:

    • a checkout with uncommitted changes, listing its files;
    • a missing slot, pointing at workspace repair;
    • a slot off its branch;
    • a push from a detached HEAD;
    • a source with no cache.
  • Failed operations are logged. Each is logged with the request ID, operation, code and cause: at WARN when the client can act on it, at ERROR otherwise. A failed GraphQL operation used to leave only status=200 in the log. Responses carry X-Request-ID; a request that sends one keeps it.

  • Job runs name what failed. A chained job run whose dependent fails reports which one and why, such as service "frontend" failed: ..., instead of "one or more dependents failed". The receipt carries error_code and error_cause (GraphQL errorCode, errorCause).

  • Git over SSH from the operator image. The angee-operator and angee-cli images now include the OpenSSH client; before, any git source with an SSH URL failed with ssh: not found. SSH itself stays the platform's: mount the deploy user's ~/.ssh read-only in the operator's home. See Running in a container.

Changed

  • REST statuses now follow the error code:

    • uncommitted changes and a branch mismatch answer 409 instead of 500;
    • a request made while another mutation is running answers 409 instead of 400.

    git failures and timeouts still answer 500.

  • A dirty checkout is detected one way, by git status; go-git's status, which could disagree about untracked files, is no longer used.

  • URLs in errors and logs have their query values masked as well as their user information, since a token is sometimes passed as a query parameter.

  • The operator container's user has a real home. It is the HOME the container is started with, or adduser's default /home/<name>; it used to be /tmp. A stack-root uid the image already has, such as Alpine's guest (home /dev/null), gets one too. The docs also show the platform-native form (user:, group_add:, the host's /etc/passwd and /etc/group mounted read-only), which needs no entrypoint.

Removed

  • The kind, name, field and reason error keys. They are gone from REST error bodies and GraphQL extensions; use code, cause and the context keys. A not-found error names its object in workspace, slot, source, job or service. The "stack root exists" conflict of stack init is cause: stack_root_exists, with the root in paths.

v0.18.1 — 2026-10-09

Fixed

  • A chained restart keeps routed services on the edge. angee job run <job> --chained-restart, from the CLI or through the operator (GraphQL jobRun(chainedRestart: true), the console's "Restart application"), recreated the routed services it reached without their caddy labels and off the edge network, so caddy-docker-proxy served an empty config and the site went down until each service was restarted on its own. The job run compiled a view of the stack without its ingress, to leave out the stack-wide edge service, and with it lost the wiring the edge gives routed services. That wiring is now added to the view from the whole stack (path routing numbers its label keys over every routed service), so a recreated service gets the same labels, networks and ports as a full compile; the edge service itself is still left out. Broken since v0.15.0.

Security

  • Built with Go 1.26.9 and golang.org/x/net v0.60.0. Release binaries and images were built with Go 1.26.6, whose net/http, crypto/tls, net/textproto and os have vulnerabilities fixed in 1.26.9 (GO-2026-6603, -6604, -6605, -6607, -6608, -6610, -6611, -6612, -6613, -6617). govulncheck reports none on this release.

v0.18.0 — 2026-10-06

Added

  • Job status in angee job list. Each job shows the latest run the runtime holds: never-run, pending, running, completed (exit 0), failed (exit 1), skipped or unknown, so a failed job no longer looks like one that never ran. A skipped job, or one whose command could not start, gets a reason column. --json, REST GET /jobs and GraphQL jobs carry status, exit_code (exitCode) and reason, and GraphQL jobs filters and sorts on status (#86).
  • Outcome counts in angee status. The service and job counts list how many are running, completed, failed and skipped, such as services: 8 (5 running, 3 skipped). A service stopped by angee stop or Ctrl-C counts as stopped, not failed. --json, REST GET /stack/status and GraphQL stackStatus carry the counts as summary (#86).
  • angee job logs <name> shows the output of a job's last run for as long as the runtime keeps it, and angee logs and GET /stack/logs?service= accept job names. job logs used to fail with "job logs are returned by job run" (#86).

Changed

  • Stopped containers show in service list and status. The status query uses docker compose ps --all, so a container job that finished reports its exit, and a container service that exited or was never started shows exited or created instead of declared (#86).
  • The process-compose supervisor outlives its processes. It is started with --keep-project, so it stays up after every local process has ended, until angee down (or Ctrl-C for a foreground angee dev). It used to exit on its own at that point and take every process state and log with it, so a failed job whose dependents were all skipped left nothing for job list, job logs or angee dev to report, and angee job run and angee restart had no supervisor to talk to (#86).

Fixed

  • angee dev reports a failed job instead of exiting 0. When a job exits non-zero, or the runtime skips services and jobs because a dependency failed, dev prints one line once the jobs have finished, such as job "deps" failed (exit 1); 3 skipped: codegen, frontend, storybook; see `angee job logs deps` , and exits non-zero. In the foreground the line appears while the rest of the stack keeps running, and again when dev ends. angee dev -d now waits for the stack's jobs before returning, bounded by ANGEE_JOB_TIMEOUT (default 30m), and names any job not finished when the bound runs out. A healthy bring-up prints nothing new and exits 0 (#86).
  • Skipped services say why. service list adds the failed job or service a skipped service's dependency chain stopped at, such as job deps failed (exit 1), and --json, REST and GraphQL carry it as reason. process-compose skips a process only when a dependency fails; a process left out by configuration shows as disabled (#86).
  • angee service up starts local services under a running supervisor. v0.17.0 detached the process-compose supervisor with up -D, which fixed up hanging when no supervisor was running but broke the usual case where one already serves the control port, such as during angee dev: the second up -D forked a daemon that died on the taken port while the parent exited 0, so angee service up <local service> (and a second angee dev -d) reported success and the services stayed Disabled. Before v0.17.0 the same call failed outright. A running supervisor now owns the project: up starts the named services through it, each after any local dependency it has never run, starts a stopped named service again and leaves running ones alone. An up that names no service starts what that supervisor has never run, such as the services an earlier angee service up left out. A service added to the manifest after the supervisor started is reported, with nothing started, as needing a supervisor restart, because adding it through the process-compose API restarts the processes the supervisor runs. A fresh up -D now waits for the new supervisor to answer, and fails, naming the port and the process-compose log when PC_LOG_FILE sets it, if the daemon never does. A control port held by something that is not a process-compose supervisor fails before any launch, and one served by another stack's supervisor (two stacks sharing a process_compose port) is refused instead of driven.

v0.17.0 — 2026-10-06

Added

  • angee workspace repair [name]. Materializes the source slots declared for an existing workspace whose paths are missing, such as a slot added to workspaces.<name>.sources after the workspace was created (which neither workspace update nor lifecycle commands create) or one deleted from disk. Each missing slot is cut the way workspace create cuts it (mode, branch, ref / default_ref, subpath, cache refresh). Anything else at a slot's path is never changed: a slot is reported as ok, or as needs-attention when it is dirty, diverged or on the wrong branch, or its path holds something that is not the slot. A slot that cannot be materialized is reported as failed, with nothing left at its path, while the others are still repaired, and the command then exits non-zero. Repairing a repaired workspace changes nothing. Also exposed as REST POST /workspaces/{name}/repair (200 with per-slot outcomes, ok: false when a slot failed) and GraphQL workspaceRepair(name:) (#95).

Fixed

  • Template lookup says why the template registry failed. When no local template matched, a failure to fetch the template registry was reported as template "stacks/dev" was not found locally or in the template registry, so a machine without git (a fresh Ubuntu install, for example) looked like a missing template. A missing git is now named outright; any other fetch failure (no network, an unwritable cache) shows git's error, with credentials in an ANGEE_TEMPLATE_REGISTRY URL masked. "Not found" is kept for a registry that was fetched but has no such template, and now names the registry.
  • Unmaterialized workspace source slots fail with a clear message. angee workspace sync-base, workspace push and the workspace source verbs (fetch, pull, push, diff, merge, rebase, the aborts and publish) on a workspace with a declared slot that is missing on disk now fail before touching any slot, naming the slot and pointing at angee workspace repair <name> (HTTP 409), instead of with a raw git chdir ... no such file or directory error. The same applies to a slot that is a dangling link or, for a git slot, a directory that is not its own checkout, where git would otherwise have acted on an enclosing repository. workspace status keeps reporting a missing slot as missing (#95).
  • angee doctor probes process-compose correctly. It called process-compose --version, a flag process-compose rejects, so a working install was always reported as version check failed: exit status 1. The probe now runs the version subcommand and reports the first line carrying a version number, skipping the banner and structured debug records process-compose writes around it (#50). Carried over from #57 by @JesseWilsonUSC.
  • angee doctor waits long enough for Node tools. Each probe had 2s, which a cold Node start regularly exceeds: pnpm was reported as context deadline exceeded while answering fine by hand. The budget is now 10s (#51). Carried over from #57 by @JesseWilsonUSC.
  • angee doctor checks the docker compose plugin. It checked the docker binary but never docker compose, the separate CLI plugin the compose backend shells out to, so doctor passed clean on installs where angee up then failed with unknown shorthand flag: 'f' in -f. The plugin is now its own tool.docker-compose check, with a distinct hint when docker itself is missing, and a failed probe reports its stderr instead of only the exit status (#55). Carried over from #57 by @JesseWilsonUSC.
  • A missing manifest names the next step. Commands that need a manifest reported open /path/angee.yaml: no such file or directory when run outside a stack. They now report that no angee.yaml was found and suggest angee init or --root (#52). Carried over from #57 by @JesseWilsonUSC.
  • angee up detaches the process-compose supervisor. The process-compose backend started the supervisor with up -d, but process-compose's -d is --hide-disabled; detached mode is -D. So angee up and angee service up for local services ran the supervisor in the foreground and blocked until every process exited. They now pass -D. Carried over from #44 by @litnimax.
  • A template ref of the wrong kind or shape says what was expected. template "…" does not match kind "stack" named neither the accepted syntax nor the kind. The error now lists the forms a ref of that kind takes: <name> or stacks/<name>, <owner>/<repo>//<path>, each optionally pinned with @<ref>, an absolute path, or a GitHub URL. A guess such as gh:owner/repo//path gets that message instead of a failed clone of https://github.com/gh:owner/repo.git. The --template flag help names the same forms, the templates guide documents each with an example, and the angee service create examples no longer use a relative path, which is not a template ref (#54).
  • angee source fetch and source pull follow the repo in angee.yaml. A git source's repo was used only for the first clone; afterwards fetch and pull used the cache's own origin, so a cache cloned from another URL (for example the HTTPS URL a stack template rendered) kept using it after repo was changed to an SSH host alias with a deploy key, and angee source pull failed with Permission denied (publickey). The two verbs now set the cache's origin to repo first (adding origin if it is missing) and log a warning naming the source when that changes it. Stack bring-up, workspace create and workspace repair only warn about a mismatch, since a cache can be shared by several stacks or be your own clone. Worktree slots share the cache's remotes; other remotes are left alone. Git errors returned to CLI and API clients now mask credentials in remote URLs.

Documentation

  • How stack update --template merges sources. A source declared by hand since the last render survives a re-render; a source the template also renders takes the template's definition, replacing local edits to it. The stack update docs say so, and a merge test pins both cases and the change summary (#60).
  • angee init --help names the real template registry, ang-ee/angee-django (or ANGEE_TEMPLATE_REGISTRY), and that fetching it needs git.

v0.16.0 — 2026-10-05

Added

  • Worktree sources without a branch. A workspace source with mode: worktree and an empty branch is cut on a detached HEAD at its base instead of failing (or quietly creating a tracking branch) when the source cache has that ref checked out. It needs a ref or the source's default_ref to start from; without one, create fails before anything is materialized. angee workspace source publish --branch <name> creates the branch at the current commit and pushes it with upstream tracking.
  • Substitutable source mode. A workspace template's source mode accepts ${...} substitution like branch and ref, so a stack can pick it through an input.

Changed

  • Unknown workspace source modes are rejected. A workspace source mode other than worktree, clone or empty fails manifest loading and workspace create, naming the workspace and source. Previously any other value on a git source silently produced a full clone, so an existing workspace with such a mode is a clone: set mode: clone or remove the line.
  • Push leaves untouched sources alone. angee workspace push and angee workspace source push without --ref skip a source that has no upstream and no commits of its own beyond its base, instead of creating an empty branch on the remote, and log a warning naming it. A source on a detached HEAD is refused (HTTP 409) with a pointer to source publish --branch. source publish without --branch reports nothing to publish for such a source.
  • "Unpushed" ignores commits that came from the base. Push, publish and the workspace destroy guard count a source's own commits as those that neither its base ref nor origin/<ref> holds. A source that sync-base moved past the cache's local base no longer looks unpushed, so destroy no longer blocks on it. For a source on a detached HEAD, destroy refuses commits that no branch, remote branch or tag holds.

Fixed

  • angee restart and chained restarts restart process-compose services again. process-compose answers success to a definition update without restarting anything when it judges the definition unchanged, and Angee took that answer for the restart: angee restart <name> and the service steps of angee job run --chained-restart reported success with the old process still running, typically for every service with a ready probe. Angee now compares the supervisor's process state before and after an update and restarts the process itself unless the update relaunched it. That also covers a service left out of an angee service up <name> selection, which a restart with a changed definition used to stop and leave down, and a local job re-run, which no longer waits out ANGEE_JOB_TIMEOUT without running the job (#91).
  • Worktree sources on a base that exists only on the remote. A worktree source whose ref exists in the cache only as origin/<ref> now starts from that commit. Previously git created and checked out a local branch named after the base instead of the source's branch, which then showed as a branch mismatch. The new branch does not track the base.
  • Workspace publish over HTTPS with the gh credential helper. The stripped environment of workspace git operations (ws source publish, merge and rebase) now passes GH_TOKEN, GITHUB_TOKEN, GH_HOST, GH_CONFIG_DIR, XDG_CONFIG_HOME and GIT_ASKPASS, so a push no longer fails with "could not read Username" when gh authenticates from the environment. Other variables are still not inherited.

v0.15.0 — 2026-09-13

Added

  • Managed application restarts. angee job run --chained-restart reruns a root job and then, once it succeeds and its dependents are ready, reruns the downstream jobs and restarts the dependent services in dependency order; failed or blocked prerequisites are reported per node without touching their descendants, and upstream services keep running. The run is a daemon-owned operation with a durable receipt across REST and GraphQL: POST /jobs/{name}/run accepts optional inputs/chained_restart and returns 202 Accepted with a receipt immediately, GET /job-runs/{id} polls its status, current step, per-node outcomes, and terminal output, GET /job-runs/latest returns the most recently started receipt, and GET /jobs/{name}/run-preview (GraphQL jobRunPreview) lists the affected jobs and services before starting. Receipts are held in bounded daemon memory so a client can disconnect and reconnect while an operation runs, but they are not persisted — restarting or stopping the operator loses them.
  • stop_grace_period service field. A service can bound graceful shutdown before the runtime force-stops it, in Go duration syntax (30s, 2m); the default is 10s. Compose receives the duration directly and process-compose rounds it up to whole seconds. Migration limitation: an active legacy process-compose process started without a shutdown timeout must first be stopped, or its supervisor relaunched, before its configuration can be updated; the operator reports that condition rather than force-stopping the worker.
  • angee stack update --template --skip <pattern>. The repeatable --skip flag keeps matching existing files untouched, including files removed from the newer template; a matched file that does not yet exist is still added. Skip patterns cannot match stack documents, Copier answers, or reconciliation state, and any unskipped conflict still stops the update.

Changed

  • angee dev no longer owns container lifetime. Container services start detached and angee dev follows their logs alongside the local-process supervisor; Ctrl-C stops the local processes and the log stream only, leaving containers running, and angee down is now the single owner of container shutdown. The container follower replays a bounded recent tail instead of the whole accumulated history and re-attaches after a container is recreated (for example by angee restart <name> from another shell).
  • angee restart <name> applies the current service definition. Restarting a service now re-applies the freshly compiled configuration — including updated command and environment — on both the container and local runtimes, instead of restarting the previously running definition.
  • Lifecycle changes are serialized by a shared mutation lease. Stack and service lifecycle operations take a single-slot, reentrant lease, so competing mutations no longer interleave.
  • Log reads no longer rewrite the stack. Streaming a service's logs (and angee logs) no longer runs stack prepare first, so reading logs cannot regenerate runtime files or re-resolve secrets.

Fixed

  • process-compose stderr no longer contaminates job output. Job commands now capture stdout and stderr separately, so diagnostics written to stderr are kept out of the output returned to the caller.
  • A stuck job can no longer hold the mutation lease forever. A detached job run is bounded by ANGEE_JOB_TIMEOUT (default 30m; 0 disables the bound), so a container job whose process never exits times out and releases the lease instead of blocking every later lifecycle change until the daemon restarts.
  • process-compose job completion is detected only on genuine terminal states. Completion is read only from Completed/Error/Skipped; a poll landing in the launch window (Pending/Launching/Running/…) is no longer misread as a completed exit 0, so a re-run's success is not reported prematurely. A re-run of an unchanged job, which process-compose restarts in place while keeping the previous end time, is now recognised by its new process id instead of waiting out ANGEE_JOB_TIMEOUT.
  • process-compose control-plane calls time out. Every process-compose control-plane HTTP request now uses a client with a 30-second timeout instead of the unbounded default, so a hung supervisor cannot stall a job or status call indefinitely.

v0.14.0 — 2026-09-08

Fixed

  • Container services see the current env-file values. The compose backend now exports the stack env file into docker compose's environment (as the process-compose backend already did), so a secret updated after the operator started (secretSet, agent reprovision) is rendered fresh instead of being shadowed by the stale copy the operator inherited from its own environment at start; up therefore recreates the container with the new value. Status (docker compose ps) reads the same env file so it interpolates the compose file from the same fresh values.

v0.13.0 — 2026-09-05

Added

  • Interactive template input form. angee init and angee stack init open a single scrollable screen listing every question in copier.yml order, with its help text, choice lists, Yes/No toggles, masked secrets, and an origin marker (default, answers, flag, recorded, changed). Tab/Enter move forward, Shift+Tab moves back, ↑↓ pick a choice, ←→ toggle, and a final Render the template? confirm gates the render. No or Ctrl+C prints aborted, nothing rendered and exits 130. After confirmation the answers are summarised to stderr. Piped stdin, ANGEE_ACCESSIBLE=1, and TERM=dumb fall back to line prompts that print help and choices and re-ask invalid answers; --yes accepts defaults with no prompts and reports every missing required input with the exact --input flag.
  • Answers files. --answers <file> (repeatable) on init, stack init, workspace create, service create, and the template update commands loads a YAML mapping such as a recorded .copier-answers*.yml: _ keys are ignored, scalars keep their source text, lists become the multiselect JSON array encoding. Files layer over template defaults and stack workspace_defaults, under --input and form edits.
  • Form on workspace and service create. angee workspace create and angee service create gain --yes/-y and --interactive/-i, open the form with -i or when required inputs are missing on a terminal, and send only explicit inputs to the platform; fully specified scripted calls keep running without prompts.
  • Interactive update with recorded answers. angee stack update --template -i, angee workspace update <name> -i, and angee service update <name> --template -i open the form pre-filled with the previous render's answers (recorded), list immutable inputs read-only, ask again for secrets that are never recorded, and re-render with the changes. stack update --template also accepts --input and --answers.
  • Recorded inputs over the API. GET /stack/template-inputs, GET /workspaces/{name}/template-inputs, GET /services/{name}/template-inputs and the GraphQL queries stackTemplateInputs, workspaceTemplateInputs, serviceTemplateInputs return the template descriptor with the recorded, non-secret answers for one target.
  • Richer template descriptors. api.TemplateInputDescriptor (CLI template get --json, REST GET /templates/{ref...}, GraphQL) carries order, help, placeholder, secret, multiselect, ordered choices (list, Label: value map, and Jinja string forms), when, and validator; inputs follow copier.yml order with metadata-only inputs last, and services/<name> refs resolve. A name declared both as a question and under _angee.inputs keeps the question's presentation and the metadata's required/immutable/generated flags.

Changed

  • --input and --answers values are validated against declared types and choices in every mode, including --yes; runtime_mode=prod now fails with template input runtime_mode must be one of: process, docker instead of rendering.
  • Stack, workspace, and service template updates reject a change to an immutable input before rendering, naming the input without echoing a secret value.
  • angee template get prints one readable block per input (type, default, choices, flags, wrapped help, when) instead of input.<name> lines; secret defaults show as default set.
  • The template-author guide documents how help, choices, secret, multiselect, placeholder, and declaration order shape the form.

v0.12.1 — 2026-09-04

Fixed

  • Container images report their release version. The operator and CLI images stamped only the CLI's version variable, so angee-operator --version inside the published image (and the operator's startup log) said dev. Both Dockerfiles now set internal/operator.Version as well, matching make build.

v0.12.0 — 2026-09-04

Added

  • Service readiness probes. Services can declare a ready block with an HTTP, TCP, command, or non-empty-file probe plus timing controls. Angee emits native Docker Compose healthchecks and process-compose readiness probes; after: and depends_on: now wait for a probed service to become healthy while preserving started-only and completed-job semantics elsewhere. This replaces noisy per-container wait loops in stack templates.

  • Stack-level workspace input defaults. A manifest may declare workspace_defaults, keyed by workspace template ref (workspaces/src, or the bare src), with inputs the operator layers under every angee workspace create --template <ref> for that template. The layering, lowest to highest: the template's _angee.inputs defaults, the stack's workspace_defaults, a declared workspace's inputs, then the request's --input values (an explicit empty value still wins). angee workspace preflight folds them into effective_inputs and reports the stack's share as stack_defaults. The framework-dev stack template renders the block from its work_state_source answer so every src-template workspace binds the private work-state slot (.work/) without repeating --input work_state_source=... on each create.

  • Verbose command diagnostics. The global --verbose/-v flag names each phase as it starts and heartbeats slow steps; repeating it as -vv traces every subprocess and HTTP call with durations and redacted attributes.

  • Operator request logging. The daemon now emits structured server logs at INFO by default, including one completion line per request with an eight-character request ID, status, and duration. -v (or ANGEE_VERBOSE=1) enables request starts, resolved authentication scopes, and Platform debug narration carrying the same request ID; health-check records stay at DEBUG.

  • angee version. The new command prints the CLI version in plain text or JSON with --json (closes #53).

Changed

  • -v now means verbose. It no longer aliases the version flag; use --version or angee version to print the CLI version.
  • Git network work is bounded and non-interactive when no terminal is available. Operator and piped/CI git commands disable terminal credential prompts and use SSH batch mode unless GIT_SSH_COMMAND is already set. Clone, fetch, pull, and push operations now time out after two minutes by default (ANGEE_GIT_TIMEOUT; 0 disables the deadline).
  • Long waits now expose ownership and deadlines. run/operator.lock records its holder and can be bounded with ANGEE_LOCK_TIMEOUT (unbounded by default). Non-streaming remote-operator requests time out after 30 minutes by default (ANGEE_OPERATOR_TIMEOUT; 0 disables), while streams remain unbounded. Operator keep-alive connections have a 120-second idle timeout.

Fixed

  • Runtime status failures no longer masquerade as stopped services. An absent process-compose supervisor remains the silent declared state, but parse, HTTP, timeout, and Docker daemon failures now warn with their cause and report affected services as unknown.
  • OpenBao bootstrap now fails at the readiness deadline. If OpenBao does not accept requests within 30 seconds, bootstrap returns the readiness error (including the last probe failure when available) instead of continuing and failing later with an unrelated secret error.

v0.11.0 — 2026-09-04

Removed

  • angee dev interactive control menu. The keyboard menu layered over the foreground log stream (added in v0.10.0) is gone: angee dev now only streams logs, and keypresses are ignored. It was unreliable by construction. The menu was drawn inline on a terminal that docker compose and process-compose write to directly, so the service picker and its prompt were trampled by live logs. The byte-at-a-time key reader had no escape-sequence handling, so every arrow, Home/End, or function key opened and closed the menu on its own and swallowed the next real keypress. And "restart" depended on a synchronous status probe that silently degraded into "start" whenever process-compose's API did not answer (the bug class fixed in v0.10.1), which reported success for a container without restarting it. Ctrl-C still tears the whole stack down; restart or stop one service with angee restart <name> or angee stop <name> from another shell. Non-terminal stdin (pipes, CI, the operator) already bypassed the menu, so that path is unchanged. The cbreak/termios helpers under internal/cli go with it, as does the SIGQUIT catch that only existed to restore the terminal: Ctrl-\ is back to Go's default (goroutine dump and exit) rather than a graceful stack shutdown.

v0.10.8 — 2026-09-03

Fixed

  • Workspace materialization no longer hard-fails when an existing source cache cannot be refreshed. Before cutting worktrees, materializeSource refreshes a source's clone cache with git fetch --all --prune. For a private or SSH source the (containerized) operator cannot authenticate, that fetch failed (cannot run ssh, Host key verification failed) and aborted POST /workspaces and stack bring-up with a 500 — even though the already-cloned cache held everything needed to cut the worktree. Refreshing an existing cache is now best-effort: on failure the operator logs a warning and materializes from the cache. Cloning a missing cache still requires the remote and surfaces errors, and the explicit update verbs (angee source pull, angee workspace source pull / fetch, angee workspace sync-base) still fetch and fail loudly — caches are refreshed through those paths.

Security

  • Bump golang.org/x/crypto to v0.56.0, clearing govulncheck advisories GO-2026-6354 and GO-2026-6355. This raises the module's go directive to 1.26 (the toolchain CI and releases already build with).

v0.10.1 — 2026-08-26

Fixed

  • Local (process-compose) service status was always reported as declared. process-compose's ANSI-coloured "new version available" notice is folded into command output (CombinedOutput), and its escape codes contain a [, which defeated the "trim to the first [" JSON extraction in the status parser — so every runtime: local service showed as declared in angee status and to the angee dev control menu. The parser now locates the real JSON array robustly (tries each [ and decodes the first valid value). This also fixes angee dev restart, which read the wrong status and tried to start an already-running process ("process is already running") instead of restarting it.
  • Bump github.com/gorilla/websocket to v1.5.3, clearing govulncheck advisory GO-2026-6278 (cryptographically weak PRNG for the WebSocket mask key) reached through the operator's log-stream endpoint.

Changed

  • angee dev control menu: R and Q are direct hotkeys. Pressing R or Q opens the restart/quit picker in a single keystroke; any other key still shows the menu hint. Previously any key opened the menu and R/Q needed a second press.

v0.10.0 — 2026-08-25

Added

  • angee dev interactive control menu. When attached to a terminal, pressing any key during the foreground log stream opens a control menu: R restarts a service, Q stops one. Each opens a numbered picker of every service (with its runtime and observed state) plus A for the whole stack and Esc to cancel. Restarting a stopped service starts it; quitting a single service stops just it while the rest keep streaming, and quitting the whole stack tears everything down and exits. The terminal runs in cbreak mode so the streamed logs keep rendering and Ctrl-C still shuts the stack down; a non-terminal stdin (pipes, CI) falls back to the plain streaming behaviour unchanged.

v0.9.0 — 2026-08-15

Changed (breaking)

  • angee init --dev is removed. Plain angee init renders the default dev stack template — the framework-dev stack: a project host at the stack root whose manifest declares the framework repos as git sources and the src workspace. The redundant angee init stack subcommand is deleted (angee stack init <template> stays). The repo-local .angee/ overlay story is gone with it; see angee-django's docs/howto/upgrade-to-split-repos.md.

Added

  • Template registry. Bare and kind-qualified template names (dev, stacks/dev) resolve from the local search paths first and fall back to ang-ee/angee-templates; ANGEE_TEMPLATE_REGISTRY overrides the registry (URL, owner/repo, or a local path). The ref grammar gains name@ref pins and explicit owner/repo//subpath references.
  • angee dev cuts declared workspaces. StackPrepare materializes every stack-declared workspace whose directory does not exist yet (WorkspaceCreate adopts declared-but-unmaterialized records), so angee init && angee dev is the whole bring-up: sources, the src workspace, provision, boot.
  • Workspace slot source names are input-substitutable, and an optional slot whose source is undeclared is skipped — the opt-in work-state and bridge/example slots render only where their sources exist.
  • scripts/install.sh honours ANGEE_VERSION for pinned installs.

Fixed

  • Template repo caches refresh on every hit (fetch + detach at origin/<ref>, or origin/HEAD for an unpinned name), so a moving ref never serves a stale template.
  • Path-input translation defaults to project-at-root: a template with path inputs but no ANGEE_ROOT input no longer gains a spurious ../ hop.
  • Stack init and dev tolerate local sources inside the stack root that do not exist yet (workspace slots the declared src workspace cuts on the same bring-up).
  • Create and update of _preserve_symlinks templates carrying relative in-template symlinks (CLAUDE.md -> AGENTS.md) are covered by regression tests.

Packaging

  • Homebrew install: brew tap ang-ee/tap && brew trust ang-ee/tap && brew install angee. Installs angee, angee-operator, and process-compose. Docker is deliberately left out — it is only needed for runtime: container services and container jobs, and the formula's caveats point at Docker Desktop, OrbStack, and Colima instead.
  • scripts/update-homebrew-formula.sh regenerates the formula from a published release's SHA256SUMS. The release workflow runs it and pushes to ang-ee/homebrew-tap, skipping with a warning when HOMEBREW_TAP_TOKEN is unset rather than failing the release.
  • scripts/update-process-compose-formula.sh regenerates the vendored process-compose formula from upstream's. It is vendored rather than depended on across taps because Homebrew 6 auto-trusts only the formula named on the command line and refuses to load dependencies from an untrusted tap, so a cross-tap dependency would stop every install at a trust prompt for a third-party tap the user never named.

v0.8.6 — 2026-08-13

Added

  • Templates declare how far outside their own directory they reach with _angee.include_root. A chain template is snapshotted before rendering so the render is pinned against mid-flight edits; include_root names the ancestor to snapshot instead of the template directory alone, so a relative include like {% include "../../_shared/AGENTS.md.jinja" %} keeps resolving from the pinned copy. It must name an ancestor of the template and stay inside the workspace — pointing it at the workspace root or beyond is rejected. Unset (the default) pins exactly the template directory. The workspace is the only bound, so a template inside a materialized Source can pin that whole checkout; docs/guide/templates.md says so out loud.

Fixed

  • A chain template ref that normalizes to the workspace root (., a/..) no longer snapshots the entire workspace — sources, worktrees and all — and is rejected with a pointed error instead. Present since v0.8.5.
  • A malformed copier.yml in a chain template now fails resolution instead of deferring an unrelated error to copier-go, and its _angee block is parsed from the bytes the guarded probe already read rather than reopened by pathname, which a swapped-in symlink could have redirected.

Changed

  • v0.8.5 decided the snapshot root by matching directory names against a built-in list of collection segments (stacks, workspaces, projects). That heuristic is replaced by the include_root declaration above. The name list was wrong in both directions: a collection named anything else (say hosts/) still lost its relative includes, while any */stacks/* ref pinned its whole sibling collection whether or not it reached outside itself. How a template collection is laid out is the collection author's business, not a vocabulary this repo should carry — projects in particular was never a primitive here and never reachable through the snapshot path.

v0.8.5 — 2026-08-13

Fixed

  • angee ws create no longer fails to snapshot a chain template that contains symlinks (snapshot workspace chain template ...: template snapshot contains unsupported symlink "templates"). The guarded snapshot now copies symlinks verbatim — mirroring the v0.8.3 reconciliation policy — instead of rejecting the _preserve_symlinks links the dev/local stack templates ship. Verbatim copies never read the link target and never recurse through a directory link, and copier-go itself refuses to follow a symlink escaping the template root when _preserve_symlinks is off.
  • Workspace chain templates shaped like <collection>/<kind-family>/<name> (e.g. templates/stacks/dev) are now snapshotted from the collection root instead of the template directory alone, so relative includes into sibling entries (the dev stack's {% include "../../_shared/AGENTS.md.jinja" %}) keep resolving from the pinned snapshot. Together these unblock angee ws create --template dev.
  • The template subpath re-attached to that collection snapshot is re-guarded component by component inside the snapshot rather than joined lexically. Because snapshots now retain symlinks, a plain join could have traversed a directory link and handed copier-go a template root outside the snapshot — copier-go's own containment check is lexical against that root and would not have caught it. Guarded-source containment is therefore preserved by the re-guard, not by symlink copying being inherently safe. Resolution also fails early and pointedly when the subpath is missing or is not a real directory.

v0.8.4 — 2026-07-16

Fixed

  • Staging a local source whose path is the render target root itself no longer fails with validate local source "framework": path "." escapes render target. preparedAbsolutePathOpener now routes the target-root case (rel == ".") to the absolute opener, the same as a path outside the target, instead of asking the target capability to open ".". This unblocks angee init --dev in the repo layout, where the dev stack's framework local source resolves to the repo root (the render target), one level above .angee.

v0.8.3 — 2026-07-16

Fixed

  • Stateful Copier reconciliation now preserves _preserve_symlinks: true templates instead of refusing them, so angee init --dev (and every other reconciled render) can materialize the stack templates' CLAUDE.md -> AGENTS.md and .angee/templates -> ../templates links. A rendered symlink is already a first-class entry kind end-to-end — fingerprinted by its link target, applied through a rooted symlink write, and round-tripped through render state — so the earlier blanket rejection was unnecessary.

v0.8.2 — 2026-07-15

Added

  • The standalone operator now streams manual job output (angee job run) to its terminal live as it runs, while still returning the captured output to the caller. Per-job status markers ([job <name>] running|finished|failed) keep concurrent jobs readable on a shared terminal. CLI invocations keep the existing buffered, once-at-the-end behaviour.

v0.8.1 — 2026-07-15

Added

  • Added stateful, ordered Copier reconciliation shared by stacks, workspaces, chains, and template-created services. Rendered files now carry versioned fingerprints, conservative three-way conflict handling, dry-run support, transactional apply/rollback, and explicit overwrite behavior.
  • Added angee service update <name> --template [--input K=V] [--dry-run] [--overwrite], REST POST /services/{name}/template/update, and GraphQL serviceUpdateFromTemplate. Service manifests merge maps recursively while scalars and lists remain atomic; service identity, workspace binding, and allocations stay stable.

Changed

  • angee stack update --template now reconciles the complete stack and chain output, so newly added files such as AGENTS.md are installed. Bare stack update remains derived-files-only. --overwrite replaces conflicts.
  • angee workspace update now actually re-renders its workspace and chain templates. Locally edited rendered files are preserved by default and can be replaced with --overwrite; inner-stack runtime state and allocated ports are retained.
  • Template state and output writes are now rooted and symlink-aware. Reconcile operations reject Copier _preserve_symlinks, verify declared local Source targets before writes, retain fixed destination/source/state capabilities through commit and rollback, revalidate live file and document baselines at apply time, and retain current or previously declared persistent roots during template deletions.

Fixed

  • Stack template merges now refresh the ingress section while preserving operator-managed runtime state.
  • Stack template updates now remove obsolete docker-compose.yaml, process-compose.yaml, and run/secrets.env artifacts transactionally, and roll back newly staged persist paths and source clones if reconciliation or state publication fails.
  • Updated the CI, release, and CodeQL toolchain to Go 1.25.12 and go-git to 5.19.1, resolving the reachable standard-library and SSH transport security advisories reported by govulncheck.

v0.7.6 — 2026-07-01

Added

  • angee stack init now renders a stack template's declared _angee.chain hosts into the target before the stack's own files, so a kind: stack that chains a project host (e.g. stacks/local → projects/web) produces the host's files overlaid by the stack instead of only the stack's own files. The chain was previously parsed but never rendered. Chain entries share the schema and ${...} substitution grammar of workspace chains: each entry's template ref and inputs resolve against the stack's inputs; a ../../projects/web ref resolves relative to the stack template while a stacks/<name> ref resolves through the template search path; and an optional root / chain_root places a host in a sub-directory (empty root overlays in place). An unresolved input reference now fails loudly instead of being written through verbatim.

Known issues

  • A flat chain overlay (a chain entry with no root) shares Copier's .copier-answers.yml with the stack render that overwrites it, so a later angee stack update refreshes the stack layer but not the chained host layer. Give a chained host its own root to keep it independently updatable.

v0.7.5 — 2026-07-01

Changed

  • angee stack init (and its init --dev / init stack variants) now fall back to running locally when ANGEE_OPERATOR_URL (or --operator) is set but the operator is not reachable, instead of failing with a raw connection error. Init is a bootstrap command that only renders template files into a new root — the operator for that root often does not exist yet — so a short GET /healthz probe decides reachability, and an operator that refuses or does not answer the probe prints a notice on stderr and proceeds in-process. A reachable operator still handles init remotely; an interrupted probe (Ctrl-C) surfaces as a cancellation rather than silently falling back; and runtime commands (up, down, logs, …) are unchanged — they route to the operator and surface its errors as before.

v0.7.4 — 2026-07-01

Added

  • The GraphQL serviceEndpoint query now returns a logStream descriptor ({ url, target, protocol, token, expiresAt }), reaching parity with the REST GET /services/{name}/endpoint log_stream field — a GraphQL consumer can now open a service's live log socket without dropping to REST for the credential.
  • The operator takes a --log-backend flag selecting the per-service log streaming backend (default ephemeral, the dev live-proxy). This realizes the LogStreamer config seam: an unrecognized value now fails fast at startup with a clear error instead of silently defaulting. A durable production backend registers behind the same seam once it exists (prodStreamer remains a fail-closed stub).

Docs

  • Documented the workspace file tools primitive (shipped v0.7.3) that was missing from the published surfaces: the angee file get/set CLI in the commands guide, the GET/PUT /files REST routes and file/fileWrite GraphQL in the operator-API reference, and a Files row (plus ingress/endpoints, route-token, and onStackSnapshotChange entries) in the README control-plane table.
  • Reconciled docs/proposals/ with shipped reality: removed five proposals whose work fully landed in code, docs, and this changelog (compose-project-isolation, ingress-routing-modes, graphql-websocket-transport, stack-snapshot-subscription, and the Hasura-superseded graphql-nestjs-query-shape); trimmed four partially-shipped proposals to their remaining scope with an Implementation status header; and added docs/proposals/ROADMAP.md indexing all open work.

v0.7.3 — 2026-06-30

Added

  • Operator workspace file tools: a generic, scoped file read/write API for editing files inside a stack source over the operator, modeled on the secrets API. GET /files?source=&path= returns { path, source, content, etag } and PUT /files writes with an optional etag precondition (a stale etag is a 409 Conflict). Exposed on REST, GraphQL (file query / fileWrite mutation), the typed client (FileRead / FileWrite), and the CLI (angee file get / angee file set). Paths are confined to the source root (traversal and symlink-escape are rejected) and content is treated as UTF-8 text within a 1 MiB cap.

Changed

  • Introduced an internal/store substrate: a minimal generic key→bytes Store (modeled on Vault's physical.Backend) with optional capability interfaces (Lister / Deleter / Versioned) discovered by type assertion, plus a backend registry. The new localfs backend (atomic temp+rename writes and compare-and-set etags) backs the file tools; env-file and OpenBao are now registered as store backends. Existing secret behavior is unchanged.

v0.7.2 — 2026-06-26

Fixed

  • Dynamically-provisioned service build contexts are now installed at <root>/services/<name>/ instead of <root>/.angee/services/<name>/. The stack root is already the control directory (ANGEE_ROOT, normally .angee, and always .angee for a workspace inner-stack), so the old path double-prepended .angee and the build context landed at .angee/.angee/services/<name>/ — an exception to the convention that workspaces/, run/, sources/, and the generated compose files all sit directly under the root. The build-context writer, the destroy cleanup, and the build.context containment validator now use services/<name>/.
    • Service template contract change: rendered services must now declare build.context: ./services/<name>/docker (previously ./.angee/services/<name>/docker). Update service templates in lockstep with this release; existing provisioned stacks keep building on the old doubled path until re-provisioned (the orphaned directory is gitignored and harmless).

v0.7.1 — 2026-06-26

Added

  • angee workspace create --sync reconciles a worktree left behind by an earlier create that failed after materializing it: the leftover worktree is removed and re-added — gated behind --sync because it discards the leftover's contents — instead of failing with already exists and is not empty. A stale "missing but already registered" worktree (no working tree to lose) is reclaimed automatically, without --sync. Exposed on the CLI (--sync), REST, and GraphQL (workspaces_insert_input.sync / WorkspaceCreateInput.sync) surfaces.

Fixed

  • angee workspace create is now transactional: a create that fails after materializing git worktrees (e.g. an invalid --ttl or a template render error) rolls those worktrees and the workspace directory back, instead of stranding "missing but already registered" worktrees that block the next create. A pre-existing leftover directory is left untouched on failure.

v0.7.0 — 2026-06-26

Changed (breaking — operator GraphQL)

  • The operator GraphQL surface was reshaped to the Hasura dialect (Angee now standardizes its consoles on Hasura) so the off-the-shelf @refinedev/hasura data provider drives an Angee console with no custom mappers. Clean cut, no backward compatibility — GraphQL clients must update. (The intermediate nestjs-query shape from earlier in this changelog is superseded.)
    • Console config required: namingConvention: "hasura-default" and idType: "String" (the operator's keys are names, surfaced as id).
    • services, jobs, sources, workspaces, templates, secrets are Hasura tables: <t>(where: <t>_bool_exp, order_by: [<t>_order_by!], limit, offset): [Row!]!, <t>_by_pk(id: String!), and <t>_aggregate(where) { aggregate { count … } nodes } (sources_aggregate adds sum/avg/min/max over ahead/behind).
    • Filtering uses Hasura where (_and/_or + String_/Boolean_/Int_comparison_exp with _eq _neq _gt _gte _lt _lte _in _nin _like _ilike _nilike _is_null …); sorting uses the order_by enum; paging uses top-level limit/offset.
    • CRUD for services / secrets / workspaces is Hasura single-row: insert_<t>_one(object: <t>_insert_input!), update_<t>_by_pk(pk_columns, _set), delete_<t>_by_pk(id). All prior verb/CRUD mutations (serviceCreate/secretSet/workspaceCreate/the nestjs *One) are removed.
    • Realtime: per-table live-query subscriptions (subscription { services(where, order_by, limit, offset) { … } }, etc.), backed by a per-subscription poller, so refine live mode works.
    • Lifecycle, git, token, log, and workspaceSource* operations stay as custom GraphQL operations (consumed via refine custom hooks); GitOpsTopology.*, WorkspaceStatus.*, and StackSnapshot remain plain views/lists.
    • The generic in-memory internal/query engine (filter/sort/paging/aggregate) is reused; the service.API push-down stays, so the REST surface still filters via ?query=<json> → {nodes,total_count} (REST contract unchanged).
    • Grouped aggregation (NDC typed-key shape): alongside the ungrouped Hasura <t>_aggregate, the services_groups / sources_groups custom roots mirror the strawberry-django-hasura grouping contract exactly, so one shared useGroupBy/useFacets hook (refine useCustom) drives the operator and the Django <model>_groups backend through a single shape. Each group pairs a typed key (one field per selected dimension) with the FREE <t>_aggregate_fields aggregate — the same type <t>_aggregate exposes, no reshape. The roots take group_by: [<t>_group_by_spec!]! (a { field, granularity } spec mirroring the upstream Granularity enum for structural parity; the operator has no granular dimensions, so a non-null granularity is rejected), where (pre-group filter), having (a predicate over the aggregate measures: count / sum_* / avg_* / min_* / max_* with the eight gt,lt,lte,gte,eq,neq,in,not_in comparisons), order_by ([<t>_group_order!], by a dimension or measure alias), and limit/offset paging — all applied in-memory over the materialized engine groups. This replaces the earlier interim { dimensions: [KeyValue!]!, aggregates } shape; the shape now tracks the Hasura v3/DDN group-by direction (graphql-engine #10786). Preview/non-stock: the stock list/aggregate/CRUD SDL is unaffected (grouping is additive).

Added

  • The operator GraphQL schema is exported as SDL to docs/public/angee.graphql (make graphql-schema, also produced by make generate and checked by make check-generated) for frontend codegen; live introspection at /graphql.
  • A contract test (internal/operator/hasura_contract_test.go) validates the exact documents @refinedev/hasura generates (getList/getOne/getMany/create/ update/deleteOne/aggregate/subscription + all filter operators, idType:String) against the executable schema — a regression-guarded consumability guarantee.

Deferred

  • Bulk *Many Hasura mutations (insert_<t> / update_<t> / delete_<t> with affected_rows / returning, i.e. refine's createMany/updateMany/deleteMany); the single-row variants cover refine's core create/update/delete.

v0.6.2 — 2026-06-17

Fixes

  • The Docker Compose project name is now derived from the absolute stack root instead of the manifest name: verbatim. The project name is a daemon-global namespace keying container, network, and volume names, but name: is a non-unique display label — every dev workspace defaults to the same example name. Two stacks sharing a name: were merged into one Compose project even across different roots and operator daemons, so the second to come up clobbered the first's containers (seen as a cross-wired edge closing the agent WebSocket with 1006). stack.Name stays the friendly label for the API, gitops, and console; containers now read like notes-angee-1a2b3c4d-edge-1 (readable base, bounded to 30 chars, plus a stable 8-hex per-root suffix). No other compose path is affected — every subcommand resolves the project from the rendered file's name:.

    Upgrade note: the rename re-namespaces a stack's containers, networks, and volumes; the old project's containers linger as orphans after the first up. Run angee down with the old binary before upgrading to clean them up. App data persisted via bind persist: subpaths under the stack root is unaffected; any genuinely named Docker volume would be recreated empty.

v0.6.1 — 2026-06-17

Improvements

  • The per-service log socket (GET /services/{name}/logs/stream) accepts ?tail=<n> (alias ?n=<n>) to replay the last n still-available lines before the live follow begins — mapped to the compose / process-compose --tail flag. The value is clamped to [0, 10000]; 0, a missing param, or a non-numeric value defers to the backend's default backlog.

v0.6.0 — 2026-06-17

Features

  • Live per-service log streaming. The operator serves a WebSocket per service at GET /services/{name}/logs/stream that streams structured LogLine frames (service, runtime, message, best-effort level) as the runtime produces them. Because each socket is scoped to one service, attribution is exact — no log-prefix parsing. GET /services/{name}/endpoint advertises the socket in a log_stream descriptor (resolved URL, target of operator/edge/production, and a short-lived minted route token) so a consumer connects without minting its own credential. Auth runs in the handshake (per-service route token aud=svc:<name> or the admin/operator tier) behind the same Origin allowlist as the GraphQL WebSocket. In development the operator proxies the live follow stream ephemerally (no persistence); a pluggable production backend is stubbed for a future durable store.

Fixes

  • Follow-mode logs now actually stream. The compose and process-compose backends buffered logs --follow via CombinedOutput/cmd.Run, delivering nothing until the stream was torn down — which also meant the onServiceLogs / onWorkspaceLogs GraphQL subscriptions never emitted live. A line-by-line streaming runner (and concurrent cross-backend fan-in for whole-stack follows) fixes both.
  • ingress.port is now rejected unless ingress.type: caddy and ingress.tls: off. It only affects the plain-HTTP edge, but a port set under tls: auto (or type: none) was silently accepted as a dead no-op; it now surfaces as a validation error, matching the existing mode-mismatch rejections for route.host/route.path. Also documented the field in the manifest guide.

v0.5.17 — 2026-06-17

Improvements

  • ingress gained an optional port field for the plain-HTTP dev edge. With tls: off, the Caddy edge previously always published :80, so two stacks on one host (e.g. parallel dev workspaces) both tried to bind :80 and the second failed. Setting ingress.port: <n> maps the chosen host port to the edge's container :80, letting each stack bind a distinct edge port; the consumer route URL carries the same port (ws://<host>:<n>/…) whenever it is non-default. The prod (tls: auto) edge keeps the standard 443/80 and ignores the field. The field validates to 1–65535 in both the Go loader and the published JSON schema.

v0.5.16 — 2026-06-17

Features

  • onStackSnapshotChange GraphQL subscription. A single subscription emits the aggregate stack snapshot (stackStatus, services, jobs, sources, workspaces, templates, secrets, gitOpsTopology, health) whenever the daemon-polled aggregate's hash changes, so the web console can drop its client-side snapshot poll. One server-side poller (the existing 2 s hash-and-publish EventHub) serves every subscriber; secret values are never part of the payload.

v0.5.15 — 2026-06-16

Fixes

  • Template rendering no longer HTML-escapes manifest and config values. The copier-go engine (pongo2) shipped with autoescape on by default, so a rendered value containing ", <, >, or & came out as &quot; / &amp; — corrupting non-HTML output such as a quoted angee.yaml value, which then failed to parse. Bumped github.com/fyltr/copier-go to the build that disables autoescape globally to match Jinja2 / Python Copier semantics (templates that genuinely emit HTML can still opt in with {% autoescape on %}).
  • scripts/install.sh no longer calls the GitHub REST API to resolve the latest release. The unauthenticated API is rate limited to 60 requests/hr/IP, so on shared or CI networks (or after a few retries) the lookup returned an empty version and the installer silently fell back to building from source — which fails outright when Go isn't installed. It now downloads straight from …/releases/latest/download/, which needs no API call and isn't rate limited; building from source is now only a fallback when the binary download itself fails. Also fixed the release workflow's version pin, which stamped ANGEE_VERSION=v0.5.14 and produced a doubled vv0.5.14 download URL.

v0.5.14 — 2026-06-15

Improvements

  • ingress gained two edge-routing controls. ingress.routing selects how the Caddy edge matches inbound requests: host (default, unchanged) gives one subdomain per service (wss://<service>.<domain>/), while path puts every routed service under one shared host with a stripped prefix (wss://<domain>/<service>/, override per service with route.path). ingress.tls: off drops the edge to plain HTTP so URLs become ws://… and the edge publishes only :80. Together (routing: path, tls: off, domain: localhost) they give a zero-setup local dev path — ws://localhost/<service>/ with no wildcard DNS and no local-CA cert trust — while the defaults keep existing host/auto stacks byte-identical. The same per-service forward_auth → /edge/verify auth model applies in both modes.

v0.5.13 — 2026-06-15

Improvements

  • angee dev now streams logs from every service regardless of runtime. It previously ran container services detached (docker compose up -d) and let process-compose own the foreground, so a mixed stack's container/agent logs never appeared. dev now brings both runtimes up attached and concurrently — docker compose up (no -d) interleaving its native per-service coloured output with the process-compose stream — and a shared context ties them together so Ctrl-C (or either backend exiting) shuts the other down cleanly. New angee dev -d/--detach starts the whole stack in the background and returns.

Fixes

  • service create now declares the secrets its service references. A service template is (correctly) forbidden from declaring secrets: itself, but a service legitimately references ${secret.NAME} in its env/command — so the operator now declares those referenced secrets in the stack on the service's behalf, as plain external entries whose value comes from the secrets backend (e.g. a prior secretSet). Previously such a reference failed the post-create compose re-render with secret "…" is not resolved (only declared secrets are resolvable), which blocked agent provisioning whose per-agent token secret is set via the REST API rather than declared in a stack template. Referencing a secret grants no value: resolution still fails if none was ever set.

v0.5.12 — 2026-06-14

Fixes

  • angee stack update --template now honours a workspace's allocated ports when re-rendering a workspace's inner stack. Previously the re-render sourced its inputs solely from the inner stack's frozen .copier-answers.yml; if that file drifted to template defaults (e.g. a stray direct re-render reset it), the update silently baked the default ports into the inner stack, colliding with the host stack and other workspaces. The update now detects a managed workspace inner stack (<root>/workspaces/<name>/<chain_root>) and overlays the authoritative ${alloc.*}-derived port inputs from the parent stack's workspaces.<name>.resolved.allocations record. Only allocation-bearing inputs are reconciled — project/source inputs still come from the answers file, so a stale workspace record cannot repoint them — and the answers' already-resolved path inputs are reused verbatim instead of being re-resolved.

v0.5.10 — 2026-06-04

Repository & module rename

  • The repository is now ang-ee/angee-operator and the Go module path is github.com/ang-ee/angee-operator (was github.com/fyltr/angee). Install with go install github.com/ang-ee/angee-operator/cmd/angee@latest / .../cmd/angee-operator@latest. The angee / angee-operator binary names are unchanged. GitHub redirects the old repo URL, but update any pinned imports or clone URLs.

Architecture (single control-plane contract)

  • Extracted service.API, one exported interface that is now the single control-plane contract. Both the in-process *service.Platform and a remote HTTP client implement it, and the CLI, REST operator, and GraphQL operator all dispatch through it — replacing the private, hand-maintained cli.platformClient interface (deleted) that had drifted from the concrete type. The remote client moved out of internal/cli into a new internal/platformclient package (platformclient.RemoteClient), paired with the contract it implements. Compile-time var _ service.API assertions on both implementations keep them in lockstep.
  • Closing remote gaps so the contract is uniform across transports:
    • Interactive angee init / stack init now works against --operator: the prompt set is derived from the Template(ref) descriptor (served over local, REST, and GraphQL alike) instead of a local-only call. Template input descriptors gain a question field distinguishing answerable Copier questions from generated/immutable metadata inputs.
    • angee up / angee dev against --operator now stream the combined foreground output via new GET /stack/up/stream and GET /stack/dev/stream routes, matching local behavior.
    • New REST routes GET /services/{name}/endpoint and GET /ingress/status give the previously GraphQL-only ingress lookups full REST parity.

v0.5.9 — 2026-06-02

Stacks

  • angee stack update --template re-renders angee.yaml from the stack's Copier template before regenerating the derived runtime files, so template changes (a new service, job, port, or source) reach an already-initialized stack — parity with workspace update. The merge refreshes template-origin sections (template wins for keys it emits) while preserving user-added keys, allocated ports values, and operator-managed state (operator, workspaces, port_leases). --dry-run prints the additions/refreshes (+/~ per section) without writing. Runs locally; requires the stack's .copier-answers.yml. Default stack update (derived-files-only) is unchanged. Full 3-way conflict detection for locally-edited template keys is a follow-up.

v0.5.8 — 2026-06-02

Ingress (Caddy edge)

  • New optional ingress backend, selected by type and defaulting to none (today's host-published-ports behavior). With ingress.type: caddy, Compile injects a single caddy-docker-proxy edge into the compose file: routed services (those with a route: block) drop their host ports, join a private <name>_edge network, and get stamped with Caddy router labels; only the edge publishes a host port. A route: on a runtime: local service is rejected, and routed services take no operator.port_pool lease.
  • GET /edge/verify — the operator's forward_auth target for the edge. Reads a route token from ?token= / Authorization / Sec-WebSocket-Protocol and verifies aud=svc:<service>; returns 200/401 (never 101). Not behind the admin-bearer gate.
  • New GraphQL queries serviceEndpoint(name) ({routed, url, internalHost, internalPort}) and ingressStatus ({type, domain, routes}), replacing host-side compose-port-scraping.
  • Manifest validation hardens the edge against Caddyfile injection: under ingress.type: caddy, routed service names and route.host are charset-checked and the ingress fields are rejected if they contain Caddy metacharacters, and edge is a reserved service name. The forward_auth wiring (caddy.forward_auth
    • /edge/verify?service=<name>) and the X-Forwarded-Uri token read were validated end-to-end against a live caddy-docker-proxy run-spike.

Internal

  • Bumped the CI/release/CodeQL Go toolchain from 1.25.10 to 1.25.11, clearing the net/textproto (GO-2026-5039) and crypto/x509 (GO-2026-5037) standard-library advisories govulncheck flagged against the pinned version.

v0.5.7 — 2026-06-02

Internal

  • Fixed the release and CI workflows to use the ghcr.io/ang-ee/* container image namespace (and scripts/install.sh to use the ang-ee/angee-operator repo) after the GitHub org rename from fyltr to ang-ee. The v0.5.6 release published its binaries but the docker image push was denied against the old fyltr namespace; v0.5.7 re-publishes with images under the correct one. No source changes — the operator/CLI binaries are identical to v0.5.6.

v0.5.6 — 2026-06-02

Operator auth & tokens

  • The operator now mints scoped, audience-bound JWTs and verifies them centrally. mintConnectionToken gained a scope: [String!] argument and stamps aud=operator; a new mintRouteToken(actor, service, ttl) issues aud=svc:<service> route tokens. Both are exposed over GraphQL and REST (POST /tokens/mint, POST /tokens/route).
  • The operator-API auth middleware is now two-tier: it accepts the admin bearer (full, server-to-server access) or a minted aud=operator token, binding the token's actor and capability scope to the request context. Existing admin-bearer callers are unchanged. A single shared Verify enforces HS256, the angee-operator issuer, a present expiry, and audience membership, and is reused by the WebSocket transport below.
  • Together these let a host backend hold the admin bearer server-side and hand the browser a short-lived, capability-scoped token instead of the admin bearer. (Scopes are carried but not yet enforced per-mutation — that follows.)

GraphQL

  • Added a graphql-transport-ws WebSocket transport on GET /graphql, alongside the existing SSE transport, so browser GraphQL clients can run subscriptions over their built-in transport. Authentication happens in the connection_init handshake (token in connectionParams) via the same two-tier check as the HTTP API. A cross-site-WebSocket-hijacking origin allowlist guards the upgrade — loopback and no-Origin requests are allowed; extend it with the repeatable --allowed-origin flag. SSE, POST queries, and the admin-bearer header gate are unchanged.

Internal

  • Bumped golang.org/x/crypto to v0.52.0, clearing the reachable golang.org/x/crypto/ssh advisories (GO-2026-5013/5015 and related) that govulncheck flagged on the copier git-over-SSH path.
  • Removed the VitePress docs build/deploy CI workflow — the docs site no longer lives in this repo. The docs/ markdown remains as source.

v0.5.5 — 2026-05-21

Services

  • StackStatus now reports real runtime state for process-compose services too (previously they always came back as "declared"). proccompose.Backend.Status queries the supervisor via process-compose list -o json and returns the literal status string lowercased (running, completed, pending, …). If the supervisor is offline or the control port is wrong, services fall back to "declared" exactly as before. Readiness probes (is_ready: Ready) also surface as a healthy health verdict on the response.
  • runtime.Backend.Status now takes a runtime.StatusRequest (Root + ControlPort) instead of a bare root. This is an internal interface change; CLI, REST, GraphQL surfaces are unchanged.

v0.5.4 — 2026-05-21

Internal

  • Simplified the IPv6 EAFNOSUPPORT check in hostPortUnavailable to satisfy staticcheck S1008. No behavioural change; the port allocator still treats hosts without IPv6 as having [::] ports available.

v0.5.3 — 2026-05-21

Services

  • StackStatus (REST GET /stack/status, GraphQL stackStatus, CLI angee stack status) now reports the real runtime state of each service by querying the runtime backends, instead of always returning "declared". ServiceList / angee service list inherit the fix. Services that haven't been brought up still report "declared"; brought-up services report docker's literal state (running, exited, created, …). Backend query errors are swallowed and fall back to "declared" so partial-stack scenarios stay useful.
  • api.ServiceState and the GraphQL ServiceState type gained a health field that mirrors docker's healthcheck verdict (healthy, unhealthy, starting). Empty when the container has no healthcheck declared or the service has not been brought up. Process-compose services don't surface a health verdict yet — the field stays empty for them until the proc-compose Status implementation lands.

v0.5.2 — 2026-05-21

Services

  • New service up verb across all surfaces (CLI angee service up <name>..., REST POST /services/{name}/up, GraphQL serviceUp(name: String!)). Maps to docker compose up -d / process-compose up, so it is idempotent across never-created services. angee service create --start now goes through ServiceUp instead of ServiceStart, so fresh services boot correctly on first try while start/stop/restart keep their literal docker compose semantics.
  • Port allocations skip ports the host is already listening on. The allocator probes 0.0.0.0 and [::] for each candidate; ports in use are skipped so docker compose up does not later fail with address already in use. The IPv6 probe treats EAFNOSUPPORT as "available" so hosts without IPv6 are unaffected.

Operator

  • angee-operator --version now prints the build version, stamped at release time via the same ldflags pipeline that already drove angee --version.

v0.5.1 — 2026-05-19

Breaking

  • Workspaces are now a pure file primitive. The operator no longer owns workspace service lifecycle; an angee workspace instance only renders Copier output (including any chained inner-stack templates as files) and materialises sources. Removed:

    • Platform.WorkspaceStart / WorkspaceStop
    • angee workspace start|stop|restart CLI subcommands
    • --start flag on angee workspace create
    • POST /workspaces/{name}/start|stop|restart REST endpoints
    • workspaceStart / workspaceStop / workspaceRestart GraphQL mutations
    • start field on the GraphQL WorkspaceCreateInput and on api.WorkspaceCreateRequest
    • _angee.chain_lifecycle template metadata field and the corresponding WorkspaceResolved.Lifecycle / api.WorkspaceRef.Lifecycle / api.WorkspaceStatusResponse.Lifecycle fields (and the WorkspaceStatus.lifecycle GraphQL field)

    If a workspace renders an inner stack and you want to bring it up, drive it explicitly with angee stack up --root workspaces/<name>/.angee (or point a second operator at that root with --root / --port). _angee.chain and _angee.chain_root continue to work exactly as before — they only describe what to render and where.

Operator

  • Closed the CLI parity gap for the operator surface. Every operation that was REST + GraphQL-only now has a CLI subcommand: angee gitops topology [--with-commits N], angee source diff <name>, angee workspace preflight, angee workspace source {fetch,pull,push,diff,merge,rebase,merge-abort,rebase-abort,rebase-continue,publish}, angee template {list,get}, angee token mint. The surface matrix in docs/reference/surfaces.md now reads Yes in the CLI column for every Platform method. Subscriptions remain GraphQL-only by design (REST/CLI have no native pubsub).
  • Added template-based service creation. angee service create --template <ref> --workspace <name> (REST POST /services/create, GraphQL serviceCreate(input)) renders a Copier template with _angee.kind: service into the outer stack as one manifest.Service entry. Templates declare name_pattern and ensure port pools; the operator resolves the service name from the workspace name + caller inputs, allocates ports under owner service/<name>/<pool>, installs other rendered files (typically docker/) at <root>/.angee/services/<service_name>/, and strictly rejects anything outside services: in the rendered output. On render failure the port leases are released and the build context is removed. service destroy is updated to release the service-prefixed leases and delete the build-context dir.
  • Added secret CRUD over REST + GraphQL + CLI. Five operations (secrets, secret(name), secretValue(name), secretSet, secretDelete) backed by the existing secrets.Backend interface so both env-file and OpenBao deployments work without changes. The value read (secretValue / GET /secrets/{name}/value / angee secret reveal) is a dedicated endpoint so the privileged path is obvious in audit logs and code review. secrets (list) only returns declared secrets (entries in stack.secrets); set/delete accept any name matching ^[A-Za-z0-9._-]{1,256}$. Mutating calls log to operator stderr (env-file's audit trail; OpenBao continues to own its own audit log).
  • Closed the REST/GraphQL parity gap. Every operation added in this release now has a REST endpoint alongside the GraphQL surface, secured by the same admin-bearer middleware as the rest of the operator API. New endpoints: GET /gitops/topology[?with_commits=N], GET /sources/{name}/diff[?ref=...], GET /workspaces/{name}/sources/{slot}/diff[?ref=...], POST /workspaces/{name}/sources/{slot}/{fetch,pull,push,merge,rebase,merge-abort,rebase-abort,rebase-continue,publish}, POST /workspaces/preflight, GET /templates, GET /templates/{ref...}, POST /tokens/mint. Subscriptions remain GraphQL-only by design (REST has no native pubsub).
  • Added a GraphQL Subscription root over Server-Sent Events. Four operations are live: onGitOpsTopologyChange, onWorkspaceStatusChange, onServiceLogs, onWorkspaceLogs. Snapshot subscriptions poll the underlying query on a 2 s tick and publish only when the result hash changes; log subscriptions ride the existing runtime-backend follow channel. The transport is POST /graphql with Accept: text/event-stream per gqlgen 0.17.
  • Retired the unused GET /events SSE stub (only ever emitted a static ready event). Live event streaming is now exclusively via GraphQL subscriptions on /graphql.
  • Added workspaceCreatePreflight(input) mutation that validates a WorkspaceCreateInput against the resolved template's input declarations without materialising a workspace. Returns the effective inputs (after defaults), a missingRequired list, and an invalidInputs list of {field, reason} for type-mismatch failures.
  • Added mintConnectionToken(actor, ttl) mutation that issues an HS256-signed JWT scoped to the supplied actor. Signing key resolves in this order: explicit --jwt-secret / ANGEE_OPERATOR_JWT_SECRET, then HKDF-derived from the admin --token, then a per-process random fallback for loopback dev. TTL defaults to 1 h, capped at 24 h.
  • Added template-descriptor introspection: templates: [TemplateDescriptor]! walks <root>/.templates/<kind>/* and <root>/templates/<kind>/* and returns descriptors with kind, name, path, and per-input metadata. template(ref) returns the single descriptor for an explicit ref (relative path, absolute path, or supported remote URL).
  • Added commit-DAG fields on GitOpsTopology: gitOpsTopology(withCommits: Int) populates sources[].commits with {sha, parents, refs, time, summary, author} for each git source, capped at withCommits. Default value is 0 so the polling subscription stays cheap; clients opt in for the DAG renderer view.
  • Added sourceDiff(name, ref) and workspaceSourceDiff(workspace, slot, ref) queries that return unified-diff [DiffFile{hunks: [DiffHunk]}] payloads parsed via bluekeyes/go-gitdiff. ref empty means uncommitted (working-tree-vs-HEAD); otherwise it diffs against the named revision.
  • Added higher-level convergence mutations on workspace source slots: workspaceSourceMerge(ref), workspaceSourceRebase(ref), workspaceSourceMergeAbort, workspaceSourceRebaseAbort, workspaceSourceRebaseContinue, and workspaceSourcePublish(remote, branch). Each returns a GitOpResult carrying {ok, conflicted, conflictFiles, message}. On conflict the worktree is left in the conflicted state for the caller to resolve; conflict files come from git ls-files -u rather than parsing free-form stderr.
  • Added integration tests covering behind/diverged worktrees, dirty worktrees blocking pull/push, missing workspace-source paths, and undeclared source references in gitOpsTopology.

Templates

  • Added the bundled templates/agent-runtime/ workspace template with a documented env-var contract: AGENT_ID (required), MCP_URL / MCP_TOKEN (optional, caller-supplied), ACP_PORT (allocated from the host stack's acp port pool), ACP_TOKEN (resolved via ${secret:acp_token}). v1 ships a placeholder service that prints the contract and sleeps; downstream consumers (e.g. the angee-django agents addon) replace the command block with a real runtime invocation. Contract documented in docs/guide/templates.md.

Dependencies

  • github.com/golang-jwt/jwt/v5@v5.3.1 (MIT) for mintConnectionToken.
  • github.com/bluekeyes/go-gitdiff@v0.8.1 (MIT) for the diff queries.

v0.4.12 — 2026-05-15

Operator

  • Moved the stack-teardown shutdown trigger from SIGHUP to SIGINT (Ctrl-C in a foreground operator, or what process-compose forwards when a dev stack is interrupted). SIGTERM still performs a graceful HTTP shutdown only, and SIGHUP is back to its default disposition. This supersedes the SIGHUP-based behavior shipped in v0.4.11.

v0.4.11 — 2026-05-15

Operator

  • The operator now treats SIGHUP as "shut down and bring the local stack down with you": after the HTTP server has shut down it runs platform.StackDown, terminating docker-compose services and process-compose-managed local processes. SIGINT and SIGTERM keep their prior behavior of stopping only the HTTP server. (Superseded by v0.4.12.)

v0.4.7 — 2026-05-10

Documentation

  • Replaced the docs logo with the angee-django isometric cube SVG and rendered it as the homepage hero image.
  • Reframed the homepage and Concepts page around the engine / Host boundary: the angee CLI + operator is the stack manager; the Host runtime (today angee-django) composes Blocks into a working app on top.
  • Added docs/guide/concepts.md covering Stack, Source, Workspace, Service, Host, and how self-building works end-to-end.

Documentation (carried from prior Unreleased)

  • Stood up a VitePress site under docs/ published at docs.angee.ai via GitHub Pages (.github/workflows/docs.yml). Existing markdown moved to docs/guide/ and docs/reference/; internal design notes moved to .agents/notes/.
  • Added prebuild scripts that render internal/operator/schema.graphql into docs/reference/graphql/ and docs/public/angee.schema.json into docs/reference/manifest-schema.md on every site build.
  • Moved the canonical manifest schema to docs/public/angee.schema.json so it is served at https://docs.angee.ai/angee.schema.json; cmd/schema now stamps $id accordingly.
  • Reworked repository documentation to describe current implemented behavior.
  • Moved changelog material to this root CHANGELOG.md.
  • Documented the current CLI surface, manifest schema, operator API, template resolver, and development commands.
  • Added docs/reference/surfaces.md enumerating every service.Platform method and its CLI / REST / GraphQL exposure (R5).
  • Generated docs/public/angee.schema.json from the manifest types and documented editor LSP integration via # yaml-language-server: $schema=... (R8).
  • Added make schema to docs/guide/development.md (R8).

Operator API

  • Replaced the hand-built graphql-go/graphql schema (~1100 LoC) with a schema-first 99designs/gqlgen resolver layer rooted at internal/operator/schema.graphql. Body-size, content-type, log-output limiting, and the route-level cross-origin protection are preserved (R2).
  • Added a make check-generated step that runs gqlgen and fails on out-of-date internal/operator/gql/ (R2).
  • Added typed domain errors (*service.NotFoundError, *service.ConflictError, *service.InvalidInputError) with consistent REST (404 / 409 / 400), GraphQL extensions, and CLI status preservation. The remote CLI client now decodes api.ErrorResponse and returns errors.As-able typed errors instead of flattening every non-2xx to a single string (R6).
  • Added GraphQL surface coverage for gitOpsTopology, workspace source fetch/pull/push, and WorkspaceLogsLimited / StackLogsLimited (gaps identified by R5).

CLI

  • angee workspace start|stop|restart|logs accept an optional [name] when run from inside ANGEE_ROOT/workspaces/<name>, matching status and sync-base (R10).
  • CLI no longer string-matches error messages to classify failures; uses errors.As against typed domain errors (R6).

Service / runtime

  • Migrated read-only git operations (RefExists, CurrentBranch, CurrentRef, Upstream, AheadBehind/AheadCount, Config, Remotes, Dirty) to go-git/v5. Network, worktree, and merge/rebase ops continue to shell out to the git CLI; the boundary is documented in the package doc-comment (R1a).
  • Native git CLI fallback for read-only status calls when go-git cannot parse extensions.worktreeConfig, so workspace branch-mismatch is based on actual current_ref != branch rather than a parse failure (R10).
  • Process-compose runtime targets now carry a control port; the backend passes --address 127.0.0.1 --port <port> for up, down, start, stop, restart, and logs. Root stacks default to 8080; rendered stacks declare ports.process_compose.value. Workspace lifecycle and log commands inherit the rendered inner-stack port (R10).
  • Workspace JSON, REST responses, GraphQL, and CLI text status expose process_compose_port, playwright_mcp_name, and playwright_mcp_url (R10).
  • dev-pr and dev-pr-multi workspace templates render ${workspace.path}/.angee/data/chrome as the Playwright --user-data-dir, isolating browser state per workspace; Django ANGEE_DATA continues to live at {{ ANGEE_ROOT }}/data (R10).
  • Application asset loader adopts an existing target row when exactly one matching unique field is present and creates the missing ledger row, instead of attempting a duplicate insert (R10).

Internal / refactors

  • Hoisted root discovery into internal/stackroot.Resolve; CLI, doctor, and operator now share one walk-up implementation (R4).
  • Promoted JobRunRequest and WorkspaceUpdateRequest to the shared api/ package; deleted three sets of duplicated inline structs across CLI and operator (R3).
  • Split Stack.Defaults() (mutating) from Stack.Validate() (pure) and introduced go-playground/validator for struct-tag constraint checks (R8).
  • Replaced sortedServiceStates, sortedJobStates, and sortedWorkspaceRefs in internal/operator/graphql.go with a single generic sortedMapValues helper (R9).

Evaluations (no code change)

  • Evaluated compose-spec/compose-go v2 against the local Compose model and decided to keep the local minimal model. The container runtime renders exactly the fields needed by current manifests and templates; revisit when a template needs an unsupported field (R7). Tracked in .agents/plans/LATEST.md Migration 3.
  • Deferred the full go-git migration (network / worktree / merge) until upstream parallel-checkout (go-git/go-git#1956) lands and credential-helper parity is designed (R1b). Tracked in .agents/plans/LATEST.md.

v0.4.6 — 2026-05-10

  • Operator: added GitOps topology API (#10).

v0.4.5 — 2026-05-09

  • CLI: improved ANGEE_ROOT detection.
  • Workspaces: stopped creating absolute symlinks during materialization.

v0.4.4 — 2026-05-09

  • CLI: angee workspace status now infers the target workspace from the current working directory when invoked without a name.
  • Documentation refresh.

v0.4.3 — 2026-05-09

  • CLI: angee workspace status reports full source state, including branch-mismatch detection (#8).

v0.4.2 — 2026-05-08

  • CLI: added angee doctor for environment diagnostics (#7).

v0.4.1 — 2026-05-08

  • CLI: added angee workspace open (#6).