diff --git a/build-pod/CLAUDE.md b/build-pod/CLAUDE.md index 6759d0d..f98f22d 100644 --- a/build-pod/CLAUDE.md +++ b/build-pod/CLAUDE.md @@ -61,7 +61,10 @@ Check these environment variables for context about what you're building: - `APP_NAME` — the human-readable app name (if set) - `APP_DESCRIPTION` — what the app should do (if set) -If `APP_DESCRIPTION` is set, use it as your starting context. The user expects the app to match this description. If the current state of the app doesn't match, proactively offer to fix it. +If `APP_DESCRIPTION` is set, treat it as helpful background about what the app was originally meant to do. + +- **Brand-new app:** you'll receive an initial instruction to build it — follow that and get a working first version into the preview right away. +- **Existing app you're resuming:** the description may be the *original* spec, and the app may have intentionally moved on from it. Don't assume any difference is a mistake, and don't start changing things unprompted. Wait for the user to tell you what they want; only offer to align the app with the description if it seems genuinely relevant. --- diff --git a/build-pod/entrypoint.sh b/build-pod/entrypoint.sh index 6210e27..5ba80ae 100755 --- a/build-pod/entrypoint.sh +++ b/build-pod/entrypoint.sh @@ -70,20 +70,33 @@ fi APP_DIR="/repo/${APP_TEAM:-_new}/${APP_SLUG:-_new}" mkdir -p "$APP_DIR" -# If this is a new app, create a minimal sus.json. +# If this is a new app, create a minimal sus.json. The absence of sus.json is +# also our "brand-new app" signal (an existing app's clone brings its own down), +# and it's restart-safe: a recreated pod re-clones the now-committed scaffold, so +# NEW_APP reads 0 and we don't re-kick a build on restart. +NEW_APP=0 if [ ! -f "$APP_DIR/sus.json" ]; then - cat > "$APP_DIR/sus.json" <), so a quote or + # newline spliced into hand-written JSON yields an invalid sus.json — and + # catalog.py silently drops apps whose manifest won't parse. + APP_DIR="$APP_DIR" python3 - <<'PYEOF' +import datetime, json, os +data = { + "name": os.environ.get("APP_NAME") or "New App", + "description": os.environ.get("APP_DESCRIPTION", ""), + "owner": os.environ.get("USER_ID") or "anonymous", + "team": os.environ.get("APP_TEAM") or "_new", + "created_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d"), + "visibility": ["default"], + "default_stack": "python+htmx", + "tags": [], } -SUSJSON +with open(os.path.join(os.environ["APP_DIR"], "sus.json"), "w") as f: + json.dump(data, f, indent=2) + f.write("\n") +PYEOF git add -A 2>/dev/null || true git commit -m "chore: scaffold ${APP_TEAM:-_new}/${APP_SLUG:-_new}" 2>/dev/null || true fi @@ -257,5 +270,63 @@ with open(path, "w") as f: json.dump(data, f) PYEOF +# For a brand-new app with a description, hand Claude an initial prompt so it +# starts building to spec on the first turn — otherwise the form description +# only reaches the session as an env var CLAUDE.md *asks* Claude to read, which +# isn't guaranteed. Gated to NEW_APP: an existing-app "Build" session must NOT +# be auto-kicked — its sus.json description may be stale and the user is about +# to say what they want. +# +# One-shot, and this matters: ttyd spawns a fresh `claude` per *client +# connection*, and the frontend reconnects on its own (terminal-iframe reload on +# a not-ready blip, a full page reload after ~15s, a manual refresh, a second +# tab). If every connection were seeded, a reconnect hours into a session would +# re-run "build the initial version" over the user's work — and autosave would +# commit the clobber. So we drop the prompt in a seed file and let the inner +# shell claim it with an atomic `mv` that succeeds exactly once: only the first +# connection is seeded; every later one falls through to a plain interactive +# session. (NEW_APP alone can't gate this — it's fixed for the pod's lifetime.) +# +# APP_DESCRIPTION is untrusted: it's expanded into the seed file by the heredoc +# (bash does not re-scan an expansion's contents) and reaches claude as a single +# "$(cat …)" word — never spliced into the command string, so it cannot break +# quoting or inject shell. +if [ "${NEW_APP:-0}" = "1" ] && [ -n "${APP_DESCRIPTION:-}" ]; then + SUS_SEED_FILE=/tmp/sus-initial-prompt + cat > "$SUS_SEED_FILE" </dev/null; then + exec claude --dangerously-skip-permissions --model "$CLAUDE_MODEL" "$(cat "$SUS_SEED_FILE.used")" + fi + exec claude --dangerously-skip-permissions --model "$CLAUDE_MODEL" + '