carlos / platform Public

spec: project defaults — zero-arg deploy, resolved -app, saved state

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 306052f2232ea7072a39d6e57450bed1d5a5123f parent c42ec0c
1 file changed, +117 −0
  • docs/superpowers/specs/2026-08-13-project-defaults-design.md +117 −0
diff --git a/docs/superpowers/specs/2026-08-13-project-defaults-design.md b/docs/superpowers/specs/2026-08-13-project-defaults-design.md
new file mode 100644
index 000000000..69c7cf26d
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-13-project-defaults-design.md
@@ -0,0 +1,117 @@
+# Project defaults: zero-arg deploy, resolved -app, saved project state
+
+**Date:** 2026-08-13 · **Status:** approved (Paul, in-session; all three
+open calls answered: prompt-once-save-suggest artifacts, offer app create,
+app resolution on every app-scoped member verb)
+
+The capstone of the proactive-CLI direction (#157, #160–#162): a command
+run with no arguments resolves what it can, asks about what is genuinely
+ambiguous, and offers to remember the answers as project state in
+`./.carlos/config` — so `carlos deploy`, alone, in a fresh checkout, is
+the entire onboarding: login offered, account resolved, app chosen or
+created, artifact named once, and every later `carlos deploy` is
+zero-question. Scripts and non-TTY callers keep exact current behavior;
+explicit flags always win and never touch a prompt.
+
+## Config schema (all additive; rewrites already preserve unknown keys)
+
+`cliConfig` gains:
+
+ app — the project's app name
+ kind — "binary" | "static"
+ artifact — project-relative path deploy ships (dir for static,
+ file for binary)
+ target — GOOS-GOARCH, binary kind only
+
+The pairing rule extends to the triple: `app` (and the deploy block) are
+honored only when the SAME file's `console` matches the session's console
+— the file means "this checkout deploys as this app on this console(/
+account)", and `-console` to another stack must drag none of it along.
+The `account` key keeps its existing rule unchanged.
+
+## Omitted `-app` (API mode, every app-scoped member verb)
+
+deploy, ship, promote, rollback, channels, releases (+ retention),
+restart, logs, env, secrets, instances (signed-in verbs), store, domains,
+ledger, version target — one seam (`resolvedApp`, sibling of
+`resolvedAccount`), in order:
+
+1. **`-app` flag** — verbatim, exactly as today.
+2. **Config `app`** (nearest walk, console-paired as above) — muted
+ `app: <name> (the default from <file>)` line.
+3. **The resolved account's apps**: `GET /api/cli/apps` already returns
+ each membership's `apps` array — the SAME single fetch defaultAccount
+ memoizes, now kept whole on the session (accounts + apps; one request
+ per invocation total, however many of account/app resolve).
+ - exactly one app → use it, muted `app: <name>`;
+ - several + TTY → numbered chooser (the #159 chooser generalized),
+ then ONE save offer covering the resolution (below);
+ - several + non-TTY → refusal listing the apps (discovery, like
+ `severalAccountsErr`);
+ - zero → **deploy offers to create** (next section); every other verb
+ errors naming the fix: `account <name> has no apps — create one
+ with: carlos apps create -app <name>`.
+4. Old console (endpoint 404) → today's "usage: … -app required" error;
+ nothing new to fall back on.
+
+## Zero-arg `carlos deploy`
+
+With no `-app` and no file arguments, deploy becomes the wizard; each
+step is skipped when config or flags already answer it:
+
+1. Account, app: the seams above. **Zero apps in the account** → offer:
+ `no apps in <account> yet — create "<dir>" (this directory's name)?
+ [y/N]` where `<dir>` is the cwd base name, client-validated by
+ `validAppName` first (an invalid name prompts for one instead). y runs
+ the existing `createApp` door (rate limiter and server validation
+ unchanged).
+2. Artifact: config `artifact`/`kind`/`target`, else prompt with a
+ detected SUGGESTION — never a silent guess: if exactly one of
+ `public/`, `dist/`, `_site/` exists it pre-fills the prompt
+ (`artifact to deploy [./public]:`); an empty answer takes the
+ suggestion; no suggestion means no pre-fill. The answer must exist on
+ disk: a directory means `kind: static`, an executable file means
+ `kind: binary` (target prompted with `linux-arm64` pre-filled).
+ Shipping stays byte-identical to `ship` with the same values —
+ detection only ever fills a PROMPT, the human always confirms.
+3. **One save offer at the end of first-run resolution**, before the
+ ship: `save these as this project's deploy defaults (.carlos/config)?
+ [y/N]` — writes console + account + app + kind + artifact (+ target),
+ one write, `saveConfigAccount`'s rules (skipped from `$HOME`,
+ warn-and-continue on failure). Values that came FROM config are not
+ re-offered.
+4. Version, channel, host, watch: deploy's existing logic untouched
+ (`gitShortHead`, channel from instances or the one channel, `-host`
+ for instance-less apps). One addition: where channel resolution
+ already errors as ambiguous, a TTY gets a chooser over the app's
+ channels instead of the error (no save — channels are per-run state).
+ The instance-less `-host` requirement keeps its current error; hosts
+ move per-deploy and a stale saved host would ship a lie.
+
+Non-TTY zero-arg deploy with no config: one error naming both remedies
+(`-app <a> <artifact> …, or run carlos deploy once at a terminal to save
+project defaults`).
+
+## Testing
+
+- resolvedApp table: flag bypass, config honored/console-mismatch
+ ignored, one-app auto (+muted line), several TTY chooser + non-TTY
+ refusal naming apps, zero-apps error naming apps create, single fetch
+ shared with account resolution (hit counter stays 1).
+- Wizard: artifact suggestion detection (each candidate dir, ambiguity =
+ no pre-fill, missing path re-prompts), kind/target inference, create
+ offer (y creates via a stub console, n stops with the named error,
+ invalid dir name prompts instead), the one combined save write, config
+ round-trip making the second run zero-question (assert NO prompts by
+ giving the chooser an empty reader).
+- Non-TTY: exact errors; explicit flags never prompt (hit counter +
+ empty chooser reader).
+- `carlosTestEnv` rules for anything shelling the binary.
+
+## Out of scope
+
+- Building artifacts (rastrillo/CI's job — the CLI ships bytes it is
+ handed, it does not make them).
+- Saving channel/host (per-run state; a stale saved value ships lies).
+- `ship` zero-file form (deploy is the porcelain; ship stays explicit).
+- Registry-mode/direct-mode verbs (unchanged throughout).