owenwork
v0.1.0
Published
Execution-side CLI companion to owenloop: parks, holds, and runs delivery orders against the hub.
Readme
owenwork
owenwork is the execution-side runner for owenloop.
owenloop is the workflow engine that decides what should happen next. owenwork is the small local process that actually shows up to do it: it parks at a hub, picks up an order, holds it with a heartbeating lease, and runs it to completion — handing the work back cleanly if the process dies so nothing strands.
One binary, a handful of roles. A standing proxy conductor watches a hub and
dispatches orders; a hold keeps an interactive agent session's order lease warm;
a detached exec child owns a command order end to end; and a few supporting
roles (prepare, lint, release, settings, join) handle setup and
draining. All of them read their hub credentials through owenloop's own
credential store — owenwork never invents its own secret storage.
Quickstart
owenwork is a single TypeScript package (Node >= 22.13.0, plain npm).
npm install -g owenwork # or run ad hoc with: npx owenwork <role>
owenwork --helpConnect an agent account to a hub (this writes to owenloop's credential store — see Credentials):
owenwork join <join-code> --hub https://your-hub.exampleThen park a conductor at the hub and let it dispatch orders:
owenwork proxy --workflow <workflow-id>Build from source
npm ci # or: npm install
npm run build # compile src/ -> dist/
node bin/owenwork.mjs --helpThe full gate (what CI runs) is:
bash .dev/checks.sh # npm ci + typecheck + lint + build + testsRoles
owenwork <role> [args].
| Role | What it does |
| ----------------------------------------------- | ----------------------------------------------------------------------- |
| prepare <workflow> [--origin <url>] | fetch, cache & compile per-step agent templates |
| lint <workflow-name \| path> | validate x.claude-code bags in a workflow def |
| proxy [options] | park at the hub and dispatch orders |
| hold --order <id> | hold an order with a heartbeating lease |
| exec <order-id> | run a command order in a self-leasing loop |
| release --session <id> | drain a session's held claims (exec-held exempt) |
| settings | print the resolved settings file |
| join <code> [--hub <origin>] [--as <account>] | redeem a join code, store the agent credential (one-time provisioning) |
--help / -h / help print usage; --version prints the version.
Exit-code contract
Stable across roles — scripts may depend on it:
| Code | Meaning |
| ---- | -------------------------------------------------------------------------- |
| 0 | success, or --help / --version, or lint clean / warnings-only |
| 1 | runtime failure (fetch / hub error, cache write), or lint found an error |
| 2 | usage error — unknown/missing role, or a missing required arg |
| 3 | role not implemented |
1 separates a real failure (a hub 404, a bag that fails lint) from a usage
mistake (2). proxy returns 0 on a clean shutdown, 1 on a runtime failure,
2 on a usage error.
proxy
owenwork proxy [--origin <url>] [--as <account>] [--name <n>] [--serve-pools a,b] [--cap <n>] \
[--workflow <id>] [--poll-interval <ms>] [--settle-margin <ms>] \
[--once] [--mcp] [--no-stamp] [--cache-dir <p>] [--agents-dir <p>] [--state-dir <p>]proxy is the standing conductor loop. It parks at the hub and, each tick:
Presence — refreshes conductor presence via
presence_pingevery ~60 s (the hub marks a conductor offline after ~3 min), carrying--nameand the serve pools.--serve-pools a,bnarrows this conductor to a subset of the key's pools; with no flag it serves every pool on the key.Wake — a cheap
wake(cursor)pre-check ("anything servable changed since cursor X?"). It only pays for a fullwhats_nextsweep whenwakesays changed, and it always adopts the returned cursor (monotonic). Bootstrap (no stored cursor) does one full sweep, then polls with the cursor.Sweep + meter — free capacity is
cap − in-flight(default cap 3). At zero free capacity the loop skipswhats_nextentirely (keeps wake + presence). Otherwise it sweeps (inbox → per-instancewhats_next, or just--workflow <id>when pinned) and dispatches at most the free capacity.Dispatch — by kind. The proxy makes no
get_orderfirst contact for any order; it holds no leases. The bound worker makes first contact and closes the hub's ~2-min pickup window, so a failed hand-off lapses back no-fault and re-offers. Each order it will dispatch is stamped into a per-order agent file from the compiled template, then, after a one-time settle-margin wait (default 5 s), dispatched by kind:- command orders spawn a detached
owenwork exec <workflow>/<run> --origin <url>child that self-leases; - agent orders are not spawned — they are returned to the conductor as
lean orders
{ order:"<workflow>/<run>", agent:"<stamped stem>" }(each stamped agent carries its own born-boundhold). In the standing CLI loop (no conductor to return to) they are logged and left for the pickup window; the--mcpwhats_nexttool is what actually returns them.
Orders it will not dispatch (over capacity, or a command routed to a conductor) are left for the pickup window. Metering counts both live exec children and outstanding agent records (see below).
- command orders spawn a detached
Defaults: --cap 3, --poll-interval 5000, --settle-margin 5000, presence
every 60 s. --once runs a bootstrap wake + one sweep and exits (the demo/e2e
hook). --no-stamp dispatches without writing agent files. --as <account>
(default default) picks the agent:<account> credential slot and is resolved
once here, then propagated into each stamped hold's argv (--as) and each
exec child's spawn env (OWENWORK_ACCOUNT) — see Credentials.
The cap and the agents/state dirs each take a settings-file fallback (see
Settings): cap resolves --cap → settings.dispatchCap → 3;
agentsDir resolves --agents-dir → $OWENWORK_AGENTS_DIR → settings.agentsDir
→ default; stateDir resolves --state-dir → $OWENWORK_STATE_DIR →
settings.stateDir → XDG default. A CLI flag or env var always wins over the
settings file.
Command routing (commandRouting + x.owenwork.routing)
worker: command steps can run here or be left for a human/session. Two inputs
decide, and most restrictive wins:
- the machine-level
commandRoutingsetting ('proxy'default, or'conductor'); - a per-step
x.owenwork.routingoverride ('proxy'|'conductor') — thex.owenworkbag is owenwork's own namespace.
If either says 'conductor', the proxy does not auto-run that command order
(one log line, left for the pickup window). An invalid/unknown value anywhere
fails closed to 'conductor' plus a warning. Agent orders are not routed
through this knob.
In-flight state + restart recovery
Detached children survive a proxy restart, so in-flight count is reconstructed
from a state dir, not from memory. One JSON record per in-flight order lives
under --state-dir → $OWENWORK_STATE_DIR → settings.stateDir →
$XDG_STATE_HOME/owenwork/exec → $HOME/.local/state/owenwork/exec. There are
two record kinds:
kind:'exec'({ workflow, run, pid, spawnedAt }) — a detached command child. Liveness is probed withkill(pid, 0); dead ones are reaped.kind:'agent'({ workflow, run, pid:0, spawnedAt, def, hash, step }) — a dispatched agent order. There is no local process, so liveness is a TTL (~60 min): an agent record older than the TTL is reaped. The storeddef/hash/stepprovenance lets the GC re-vouch (re-stamp) a live agent's file after a restart, even with an empty agents dir.
On startup and each sweep the loop scans the records, counts the live ones (across both kinds) as in-flight, and reaps the rest. A re-offered agent run (its handout lapsed past the pickup window) replaces its stale record and is re-emitted without consuming a fresh slot — the record is already counted. Metering off this count is an efficiency mechanism — the engine's race-safety and the pickup/lease TTLs are the correctness backstop, so a rare miscount is harmless.
Shutdown
SIGINT/SIGTERM drains: the in-flight sweep finishes, run() returns 0, and no
hub call is made afterward. Detached children are not killed — that is the
drain semantic (a session going away hands its agent-held orders back via
owenwork release --session, but exec-held command orders keep running). A
second signal hard-exits.
--mcp mount (stdio-MCP)
--mcp mounts the same loop core behind a stdio-MCP server instead of the
self-driven park, so a conductor drives dispatch on demand over JSON-RPC. stdout
is the protocol channel (every loop/diagnostic line is redirected to stderr);
the process stays alive on stdin and exits on its EOF. Dormant until the first
tool call — no hub traffic until the conductor asks. Mutually exclusive with
--once. Three tools:
| tool | args | returns |
| --- | --- | --- |
| whats_next | { wait_ms? } | parks up to wait_ms (default 25 s) running iterate() until it yields agent lean orders, then { orders:[{order, agent}], cap, free }. Command orders are exec-spawned as a side effect and never returned. Cancellable; emits a keepalive progress frame ~every 25 s while parked. One park at a time — an overlapping call fast-fails. |
| set_dispatch_cap | { cap } | sets the live cap (non-negative integer); returns { cap, free }. |
| submit | { workflow, run, path, value, done? } | posts an artifact receipt through the proxy's hub connection; returns { outcome, closed, text }. closed:true frees the run's dispatch slot immediately (its stamped file is reaped on the next iterate). |
hold
owenwork hold --order <workflow>/<run> [--origin <url>] [--as <account>] [--session <id>]
[--heartbeat-interval <ms>] [--ignore-stdin] [--mcp]
or: owenwork hold --order <run> --workflow <wf> [...]hold keeps one order's lease alive on behalf of an interactive session and
performs a final-breath handoff when that session goes away, so work re-offers
instead of stranding. It first calls get_order (which beats the lease, closes
the pickup window, and validates the workflow/run pairing), then heartbeats on
a safe cadence — default 60s, well inside the hub's reap TTL — with
jitter/backoff on transient failures. A 403 is a loud ownership error; a clock
jump (laptop sleep) triggers a lease re-check before the next beat.
On SIGINT, SIGTERM, or stdin EOF it takes a targeted release (the
"final breath", bounded at 5s) so the order re-offers on the next tick. Release
is idempotent hub-side, so a normal completion and a reaper race both land
cleanly. A second signal hard-exits.
--order <workflow>/<run>— the order to hold; composite splits on the first/only. Bare--order <run>requires--workflow <wf>.--session <id>(envOWENWORK_SESSION) — rides every call as the holder tag{kind:'session', id}. Optional hub-side; when absent no holder is tagged.--heartbeat-interval <ms>— renew cadence (default 60000).--ignore-stdin— don't final-breath on stdin EOF, for backgrounded/detached use where stdin is/dev/nullor already closed at spawn.--mcp— run as a stdio-MCP work-holder (see below). Mutually exclusive with--ignore-stdin(stdin is the transport, and its EOF is the final-breath signal).
Exit codes: 0 on a clean stop — order completed elsewhere, final breath
released, or stopped before the hold was ever established; 1 on a runtime
failure — ownership-error (403), lease-lost, hub-unreachable (transient
failures spanned the window), or release-failed; 2 on a usage error (missing
--order, malformed target, no origin/token).
--mcp mount (born-bound work-holder)
A stamped agent's frontmatter declares
mcpServers.owenwork = owenwork hold --order <wf>/<run> --origin <url> --as <account> --mcp,
so when the agent session boots it launches hold --mcp as its stdio-MCP server
(the --as <account> is the proxy-resolved account, stamped in at dispatch).
The ids arrive on argv — they never pass through the model. Underneath the tools
the same lease loop keeps the order's lease warm (first contact captured via
onOrder so get_order needn't re-fetch); stdout is the JSON-RPC channel, every
diagnostic goes to stderr, and stdin EOF (the session died) fires the final
breath. Two tools:
| tool | args | returns |
| --- | --- | --- |
| get_order | {} (none) | the bound order's { workflow, run, order, text }. The run is fixed at launch. |
| submit | { path, value, done? } | posts a receipt for an owed output path; returns { outcome, closed, text }. When the hub reports the run closed, the lease loop stops without releasing (the claim is already gone). |
The server keeps answering until the transport ends, not until the lease
does: after a closing submit (or a lost lease) both tools fast-fail with
isError and make no further hub calls, but the process serves until stdin EOF
and then exits with the lease loop's outcome as its exit code — so a closing
submit's reply never races process exit.
The bearer comes from owenloop's agent:<account> slot (see
Credentials); hold takes --as <account> (the proxy stamps it
in), while exec/prepare/release read OWENWORK_ACCOUNT.
exec
owenwork exec <workflow>/<run> [--origin <url>] [--heartbeat-interval <ms>]
or: owenwork exec <run> --workflow <wf> [...]exec is the detached, self-leasing runner the proxy spawns per command
order — it is the one process that actually shells an order's command out. It
shares hold's lease loop (src/lease/loop.ts), so cadence, backoff, clock-jump
handling, and the final breath are identical; on top of it, exec reads the
first-contact order packet and runs the command while the lease stays warm
underneath.
- Holder tag — every
get_order/heartbeatrides{kind:'exec', id}where id is<hostname>:<pid>.kind:'exec'is the drain exemption: an exec-held claim survives a session drain — only a signal aimed at this process hands the order back. - Receipt — when the command settles (any exit code, or a machinery
failure),
execsubmits acommand-receiptto every owed output path:{ exitCode, signal?, error?, outputHash, stdoutBytes, stderrBytes, outputTail (last 4 KiB), timings, orchestrator }.outputHashissha256:<hex>over full stdout then full stderr; if out-of-order stderr overflows the 1 MiB buffer cap, capture degrades to the bounded two-part formsha256:<sha256(stdout)>+<sha256(stderr)>(both streams still fully hashed). The receipt IS the result contract — a failing command still exits0because delivering the receipt is the job; the exit code inside carries the truth. - Misroute — a null / non-
command/ nothing-owed packet is notexec's to fail (an agent could legitimately run it), so it takes a targetedreleaseand exits1. - Lease lost mid-run — if the lease goes terminal while the command runs,
execkills the command's process group (SIGTERM → 5s grace → SIGKILL) and does not submit (a submit would race the re-offer). A signal aimed atexecdoes the same, then releases — killed work never gets a receipt, even when the TERM'd command settles before the release round-trip finishes.
Exit codes: 0 on submitted (receipt delivered) or a first-contact
completed; 1 on misroute, killed, lease-lost, ownership-error,
hub-unreachable, submit-rejected, or submit-failed; 2 on a usage error
(missing/malformed order-id, no origin/token). The command runner is a seam, so
unit tests drive a fake runner and never spawn a real child; only the runner's
own tests exercise the real spawn, against harmless fixtures.
release
owenwork release --session <id> [--origin <url>]release drains a session: it releases every claim the given session id holds
so those orders re-offer immediately instead of stranding until their leases
expire. It calls the hub's by-session release verb and prints the hub's
summary line, one line per released <workflow>/<run>, and a fixed note that
exec-held claims are drain-exempt.
Drain semantics. The hub releases the session's agent-held claims
and leaves exec-held claims running — a detached command doing real work
must survive its launching session going away. The hub enforces the exemption
server-side and does not enumerate the exempt claims, so release can only
note the exemption; it never lists which claims were exempt. Draining a session
with zero held claims is a success (exit 0), not an error.
--session <id>(envOWENWORK_SESSION) — the session to drain. An empty string counts as missing. Missing both is a usage error (exit2).--origin <url>(elsesettings.hubOrigin) and the bearer (agent slot forOWENWORK_ACCOUNT, withOWENWORK_TOKENas a dev-only override) resolve exactly as for the other roles.releasehas no--asflag.
Exit codes: 0 on a successful drain (including an empty release list); 1
on a hub/network error (the hub message is surfaced); 2 on a usage error
(missing session or origin, no stored agent key, or an unknown flag).
Settings
owenwork's machine-level config is a single JSON file at
$XDG_CONFIG_HOME/owenwork/settings.json (else
$HOME/.config/owenwork/settings.json). Every knob is optional; a missing file
loads as {}. Each knob is the lowest-precedence fallback for a role option
— the resolution order is always CLI flag > env var > settings file >
built-in default.
| Knob | Type | Fallback for |
| ---------------- | -------------------------- | --------------------------------------------- |
| hubOrigin | string | --origin (prepare, proxy, hold, exec, release); also seeded by owenwork join, first-write-wins |
| cacheDir | string | --cache-dir / OWENWORK_CACHE_DIR |
| agentsDir | string | --agents-dir / OWENWORK_AGENTS_DIR |
| stateDir | string | --state-dir / OWENWORK_STATE_DIR |
| dispatchCap | positive integer | --cap (proxy), default 3 |
| commandRouting | 'proxy' | 'conductor' | proxy command routing (default 'proxy') |
Loading validates the type of every known key (a wrong type is a hard error
naming the key, the expected type, the received value, and the file path).
Unknown keys are not fatal — they pass through untouched (forward
compatible) but are reported as unrecognized so typos are visible.
owenwork settingsprints the resolved file path, whether it exists, each known knob with its value
and provenance (settings when the file supplies it, default / unset
otherwise), and any unrecognized keys. It exits 0 when the settings are valid
(including no file at all), 1 when the file is malformed JSON or a known key
has the wrong type, and 2 on stray arguments (there are no options).
NO secrets, ever. There is deliberately no token/credential knob. Hub
credentials stay in owenloop's own store, read through the CredentialReader
seam (agent:<account> slot; OWENWORK_TOKEN is a dev-only override) — never in
settings.json. See Credentials.
prepare
owenwork prepare <workflow> [--origin <url>]prepare fetches a published workflow def from the hub, caches it keyed by the
def's content hash, and compiles each agent step into a Claude Code agent .md
template. A step's template is a complete agent file with exactly one
substitution token left in it — the literal __OWENWORK_ORDER__ — which the
proxy replaces with the order id when it stamps a per-order agent file.
Not every step compiles to a template, and that is not an error:
- a step with no
x.claude-codebag is a plain-dispatch step (the conductor runs a bare subagent) — listed in the summary, no file written; - a
worker: commandstep or acalls:step is an exec/engine concern — listed as skipped; - a bag whose key no adapter claims (e.g. a future
x.codex) is left alone and noted, so it neither breaks nor silently vanishes.
Origin, token, and cache resolution
- Origin —
--origin <url>→settings.hubOrigin→ usage error (exit 2). - Bearer — the
agent:<account>slot from owenloop's store (account fromOWENWORK_ACCOUNT, defaultdefault);OWENWORK_TOKENis a dev-only override. A missing agent key is a refuse (exit 2) naming a runnableowenloop logincommand.preparehas no--asflag — a conductor setsOWENWORK_ACCOUNT. See Credentials. - Cache dir —
OWENWORK_CACHE_DIR→settings.cacheDir→$XDG_CACHE_HOME/owenwork→$HOME/.cache/owenwork(throws if none resolve).
Cache layout
<cacheDir>/bundles/<workflow-name>/<hash>/
bundle.json # the validated fetch response + fetchedAt + version
templates/<step>.md # one compiled template per agent stepKeying by content hash means invalidation is automatic: a republished def gets a
new hash and a new dir, and the superseded hash dir for that workflow is pruned.
A re-run against an unchanged hash is idempotent — nothing is rewritten. Writes
are atomic (temp file + rename) and byte-identical for a given hash, so a
concurrent prepare for the same def is safe: last rename wins with no torn
files.
After compiling, prepare runs a four-layer garbage-collect over the stamped
per-order agent files (in OWENWORK_AGENTS_DIR, default
~/.claude/agents/owenloop): vouch open orders → delete ended orders → evict by
TTL (48 h) → hard-cap oldest-first (500). Only files matching the stamped-name
pattern are ever touched, and every filesystem op fails open — GC never kills a
run. prepare calls it with empty open/ended sets (TTL and cap still apply); the
proxy is the live caller with real run info.
Environment variables
| Var | Purpose |
| -------------------- | ----------------------------------------------------------- |
| OWENWORK_ACCOUNT | agent-slot account to read (agent:<account>, default default) |
| OWENWORK_TOKEN | dev-only bearer override; when set, skips the store |
| OWENWORK_CACHE_DIR | cache root (overrides settings / XDG / HOME) |
| OWENWORK_AGENTS_DIR| stamped-file GC directory |
Hub bundle-serving note
The hub does not expose a bundle verb yet. Today GET /api/workflows/:name
returns each step's name/consumes/produces/terminal but not its body,
model, worker, or x — the four fields prepare needs to compile a
template. Against a hub that does not yet serve those fields, prepare detects
an agent step missing its body and fails with an actionable error naming the
gap rather than caching a bundle it cannot compile. Tests run against a mocked
hub serving the enriched shape.
calls: limitation
prepare fetches exactly one def per run. Frozen child defs for calls: steps
(multi-def bundles) have no hub publish surface yet, so when a def contains a
calls: step, prepare notes it in the summary
(child workflow '<x>' not fetched — bundle serving lands with the hub's publish
surface) and compiles only the parent. The cache layout deliberately leaves room
in the hash dir for those child defs to land later.
lint
owenwork lint <workflow-name | path>lint validates each step's x.claude-code bag against the Claude Code agent
field surface. The target is resolved by shape:
- a path ending
.yaml/.ymlis parsed as a raw owenloop def; - a path ending
.jsonis a cached / exported bundle; - a bare name loads the latest cached bundle for that workflow (errors with
run \owenwork prepare ` first` if none is cached).
It does not reimplement owenloop's full def validator — only what it needs to
reach the bags. Per step it checks: the bag is a map; reserved keys name /
description are absent (they are generated); known keys have the right types
(tools/disallowedTools, model, permissionMode, maxTurns, effort,
skills, background, memory, mcpServers, hooks, isolation, color,
initialPrompt); unknown keys warn; a
bag model alongside a first-class step model warns (the step field wins); an
author mcpServers.owenwork entry warns (it is overwritten at compile time).
Output is one line per finding — error|warning step <step>:
x.claude-code.<field>: <message> — then a summary. Exit 0 when clean or
warnings-only, 1 when any error is found.
join
owenwork join <code> [--hub <origin>] [--as <account>]One-time provisioning: redeem a hub-issued join code and store the resulting agent credential. This is the only place owenwork writes to owenloop's credential store — every other role only reads it (see Credentials).
Origin rule. The hub origin comes from --hub, or else from an existing
settings.hubOrigin — one of the two is required. The code itself never
selects or influences the origin, and the hub's redeem response is not
trusted to redirect it either: a pasted code can only redeem against the hub
the human named, never one it points to itself.
join posts the code (unauthenticated — there is no credential yet) to the
hub's /enroll/redeem route, then:
- writes the returned agent token to owenloop's store at slot
agent:<account>(--as <account>, default: the agent name the hub returns) via owenloop'sstoreCredential; - records
settings.hubOrigin, first-write-wins — an existing, differinghubOriginis left untouched with a warning, never overwritten.
The confirmation output never prints the token or the code. Exit 0 on
success; 1 on a redeem/store failure; 2 on a usage error or an
unresolved/invalid origin. A burned or already-used code, an invalid code,
and a hub rate-limit each surface as a distinct, actionable message.
Hub client
src/hub/ is a typed client for the hub verb surface the roles call
(whats_next, get_order, heartbeat, release, submit, whoami),
matching the hub's REST shapes. Transport and auth wiring are real; tests
exercise it only against a fake fetch or a throwaway local server. The
presence verb is intentionally not included yet — its hub surface has not
landed, and inventing its shape is off-limits.
Credentials
owenwork reads hub credentials through owenloop's store only, and never
reads the macOS keychain or credentials.json formats directly. At runtime
(proxy/hold/exec/prepare/release) it is strictly read-only over the
store — join is the sole exception, a one-time provisioning write
via owenloop's own storeCredential. src/credentials/reader.ts ships the
CredentialReader seam plus a FakeCredentialReader for tests; the live
OwenloopCredentialReader (src/credentials/owenloop.ts) wires it to owenloop
0.4's readStoredCredential.
Agent slot only, never human. owenwork always addresses the
agent:<account> slot (account from OWENWORK_ACCOUNT, default default) — the
OwenloopCredentialReader is the one enforcement point, so a human-only origin
reads as absent. When no agent key is stored, every role refuses to start
(exit 2) with a message naming the origin, the account, and a runnable
owenloop login --hub <origin> --as <agent[:account]> command.
Account handoff. The proxy resolves the account once (from --as
<account>, default default) and propagates it two ways: into each stamped
hold agent file's argv (hold … --as <account>) and into the exec child's spawn
env (OWENWORK_ACCOUNT). hold accepts --as; exec, prepare, and release
read OWENWORK_ACCOUNT (a conductor sets it for a non-default account).
OWENWORK_TOKEN is a documented dev-only override. When set (trimmed,
non-empty) it is used verbatim and the store + account are skipped — handy for
local runs against a scratch token. It is not the normal path and is never
required; the agent slot is. Precedence is OWENWORK_TOKEN → agent slot →
refuse, shared by all five roles via resolveBearer.
Connecting / storing an account. owenwork's runtime roles are read-only over the store, so you store an agent account for an origin by running owenloop, not owenwork:
owenloop login --hub <origin> --as agent:<account>Use --as agent for the default account. Provisioning a fresh box from a
hub-issued join code is the one owenwork-side alternative — see join:
owenwork join <code> --hub <origin>owenwork then reads the stored slot at run time via --as <account>
(proxy/hold) or OWENWORK_ACCOUNT (exec/prepare/release).
Listing accounts. owenwork intentionally cannot enumerate stored accounts —
it never reads the keychain or credentials.json directly, and owenloop 0.4
exposes no enumeration API for it to call. Account listing is an owenloop-side
capability; to see which slots are stored for an origin, use owenloop.
License
Apache-2.0 © Typical Day LLC. Contributions require a signed CLA — see CONTRIBUTING.md.
