feat(ce-demo-reel): add local save as alternative to catbox upload (#647)
Some checks failed
CI / pr-title (push) Has been cancelled
CI / test (push) Has been cancelled
Release PR / release-pr (push) Has been cancelled
Release PR / publish-cli (push) Has been cancelled

This commit is contained in:
Luca Henn
2026-04-22 20:28:44 +02:00
committed by GitHub
parent 7ddfbed33b
commit fdf5fe4af5
6 changed files with 348 additions and 15 deletions

View File

@@ -194,7 +194,7 @@ Use this branch diff (not the working-tree diff) for the evidence decision. If t
**Evidence decision (before delegation).** If the branch diff changes observable behavior (UI, CLI output, API behavior with runnable code, generated artifacts, workflow output) and evidence is not otherwise blocked (unavailable credentials, paid services, deploy-only infrastructure, hardware), ask: "This PR has observable behavior. Capture evidence for the PR description?"
- **Capture now** -- load the `ce-demo-reel` skill with a target description inferred from the branch diff. ce-demo-reel returns `Tier`, `Description`, and `URL`. Note the captured evidence so it can be passed as free-text steering to `ce-pr-description` (e.g., "include the captured demo: <URL> as a `## Demo` section") or spliced into the returned body before apply. If capture returns `Tier: skipped` or `URL: "none"`, proceed with no evidence.
- **Capture now** -- load the `ce-demo-reel` skill with a target description inferred from the branch diff. ce-demo-reel returns `Tier`, `Description`, `URL`, and `Path`. Exactly one of `URL` or `Path` contains a real value; the other is `"none"`. If capture returns a public URL, pass it as steering to `ce-pr-description` (e.g., "include the captured demo: <URL> as a `## Demo` section") or splice into the returned body before apply. If capture returns a local `Path` instead (user chose local save), pass steering that notes evidence was captured but is local-only (e.g., "evidence was captured locally — note in the PR that a demo was recorded but is not embedded because the user chose local save"). If capture returns `Tier: skipped` or both `URL` and `Path` are `"none"`, proceed with no evidence.
- **Use existing evidence** -- ask for the URL or markdown embed, then pass it as free-text steering to `ce-pr-description` or splice in before apply.
- **Skip** -- proceed with no evidence section.
@@ -202,7 +202,7 @@ When evidence is not possible (docs-only, markdown-only, changelog-only, release
**Delegate title and body generation to `ce-pr-description`.** Load the `ce-pr-description` skill:
- **For a new PR** (no existing PR found in Step 3): invoke with `base:<base-remote>/<base-branch>` using the already-resolved base from earlier in this step, so `ce-pr-description` describes the correct commit range even when the branch targets a non-default base (e.g., `develop`, `release/*`). Append any captured-evidence context or user focus as free-text steering (e.g., "include the captured demo: <URL> as a `## Demo` section").
- **For a new PR** (no existing PR found in Step 3): invoke with `base:<base-remote>/<base-branch>` using the already-resolved base from earlier in this step, so `ce-pr-description` describes the correct commit range even when the branch targets a non-default base (e.g., `develop`, `release/*`). Append any captured-evidence context or user focus as free-text steering (e.g., "include the captured demo: <URL> as a `## Demo` section", or "evidence captured locally — not embedded" for local saves).
- **For an existing PR** (found in Step 3): invoke with the full PR URL from the Step 3 context (e.g., `https://github.com/owner/repo/pull/123`). The URL preserves repo/PR identity even when invoked from a worktree or subdirectory; the skill reads the PR's own `baseRefName` so no `base:` override is needed. Append any focus steering as free text after the URL.
**Steering discipline.** Pass only what the diff cannot reveal: a user focus ("emphasize the performance win"), a specific framing concern ("this needs to read as a migration not a feature"), or a pointer to institutional knowledge. Do NOT dump an exhaustive scope summary or a numbered list of every change — `ce-pr-description` reads the diff itself. Over-specified steering encourages the downstream skill to cover everything passed in, producing verbose output. Cap steering at roughly 100 words; if a longer framing feels necessary, trust the diff and cut.

View File

@@ -154,12 +154,16 @@ Return these values to the caller (e.g., ce-commit-push-pr):
Tier: [browser-reel / terminal-recording / screenshot-reel / static / skipped]
Description: [1 sentence describing what the evidence shows]
URL: [public URL or "none" (multiple URLs comma-separated for static screenshots)]
Path: [local file path or "none" (multiple paths comma-separated for static screenshots)]
=== End Evidence ===
```
The `Description` is a 1-line summary derived from the capture hypothesis in Step 0 (e.g., "CLI detect command classifying 3 project types and recommending capture tiers"). The caller decides how to format the URL(s) into the PR description.
- `Tier: skipped` or `URL: "none"` means no evidence was captured.
- `Tier: skipped` means no evidence was captured; both `URL` and `Path` are `"none"`.
- When uploaded to catbox: `URL` has the public URL, `Path` is `"none"`.
- When saved locally: `Path` has the local file path, `URL` is `"none"`.
- For all non-skipped tiers, exactly one of `URL` or `Path` contains a real value; the other is `"none"`.
**Label convention:**
- Browser reel, terminal recording, screenshot reel: label as "Demo"

View File

@@ -1,6 +1,6 @@
# Upload and Approval
Upload a temporary preview for the user to review, then promote to permanent hosting on approval.
Upload a temporary preview for the user to review, then either promote to permanent hosting or save locally based on user choice.
## Step 1: Preview Upload (Temporary)
@@ -14,24 +14,33 @@ The last line of output is the preview URL (e.g., `https://litter.catbox.moe/abc
For multiple files (static screenshots tier), upload each file separately.
**If upload fails** after retry, fall back to opening the local file with the platform file-opener (`open` on macOS, `xdg-open` on Linux) so the user can still review it. Include the local path in the approval question instead of a URL.
**If upload fails** after retry, fall back to opening the local file with the platform file-opener (`open` on macOS, `xdg-open` on Linux) so the user can still review it. Include the local path in the destination choice question instead of a URL.
## Step 2: Approval Gate
## Step 2: Destination Choice
Present the preview URL to the user for approval. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
Present the preview URL to the user and ask how to handle the evidence. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
**Question:** "Evidence preview (1h link): [PREVIEW_URL]"
**Question:** "Evidence preview (1h link): [PREVIEW_URL]. Where should the evidence go?"
**Options:**
1. **Use this in the PR** -- promote to permanent hosting
2. **Recapture** -- provide instructions on what to change
3. **Proceed without evidence** -- set evidence to null and proceed
1. **Upload to catbox (public URL)** -- promote to permanent hosting for PR embedding
2. **Save locally** -- save to a stable OS-temp path ($TMPDIR/compound-engineering/ce-demo-reel/)
3. **Recapture** -- provide instructions on what to change
4. **Proceed without evidence** -- set evidence to null and proceed
If the question tool is unavailable (headless/background mode), present the numbered options and wait for the user's reply before proceeding.
### On "Upload to catbox (public URL)"
Proceed to Step 3: Promote to Permanent Hosting.
### On "Save locally"
Proceed to Step 3b: Local Save.
### On "Recapture"
Return to the tier execution step. The user's instructions guide what to change in the next capture attempt. After recapture, upload a new preview and repeat the approval gate.
Return to the tier execution step. The user's instructions guide what to change in the next capture attempt. After recapture, upload a new preview and repeat the destination choice.
### On "Proceed without evidence"
@@ -39,7 +48,7 @@ Set evidence to null and proceed. The preview link expires on its own.
## Step 3: Promote to Permanent Hosting
After the user approves, upload to permanent catbox hosting. The command accepts either the preview URL (preferred) or the local file path (fallback):
After the user selects "Upload to catbox", upload to permanent catbox hosting. The command accepts either the preview URL (preferred) or the local file path (fallback):
```bash
python3 scripts/capture-demo.py upload [PREVIEW_URL or ARTIFACT_PATH]
@@ -51,10 +60,26 @@ The last line of output is the permanent URL (e.g., `https://files.catbox.moe/ab
For multiple files, promote each separately.
## Step 3b: Local Save
After the user selects "Save locally", save the artifact to the default OS-temp path using the pipeline script:
```bash
python3 scripts/capture-demo.py save-local --file [ARTIFACT_PATH] --branch [BRANCH_NAME]
```
Determine `[BRANCH_NAME]` from `git branch --show-current` or the PR context discovered in Step 0 of the SKILL.md.
The last line of output is the absolute path of the saved file. Use this path in the output.
For multiple files (static screenshots tier), save each file separately.
**If save fails** (permission denied, disk full), report the error and offer to retry or fall back to catbox upload (Step 3).
## Step 4: Return Output
Return the structured output defined in the SKILL.md Output section: `Tier`, `Description`, and `URL` (the permanent catbox URL). The caller formats the evidence into the PR description. ce-demo-reel does not generate markdown.
Return the structured output defined in the SKILL.md Output section: `Tier`, `Description`, and either `URL` (permanent catbox URL) or `Path` (local file path). The caller formats the evidence into the PR description. ce-demo-reel does not generate markdown.
## Step 5: Cleanup
Remove the `[RUN_DIR]` scratch directory and all temporary files. Preserve nothing -- the evidence lives at the permanent URL now.
Remove the `[RUN_DIR]` scratch directory and all temporary files. Preserve nothing -- the evidence lives at the permanent URL or has been copied to the local save path.

View File

@@ -11,15 +11,18 @@ Subcommands:
terminal-recording --output OUT --tape TAPE Run VHS tape file
preview FILE Upload to litterbox (1h expiry) for preview
upload FILE_OR_URL Upload/promote to catbox.moe (permanent)
save-local --file F --branch B Save artifact locally instead of uploading
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
from datetime import datetime, timezone
from pathlib import Path
@@ -640,6 +643,35 @@ def cmd_upload(args):
_upload_with_retry(CATBOX_API, source, "catbox.moe")
# --- Save local ---
def _sanitize_branch(branch):
sanitized = branch.replace("/", "-")
sanitized = re.sub(r"[^a-zA-Z0-9_-]", "", sanitized)
sanitized = re.sub(r"-+", "-", sanitized).strip("-")
return sanitized[:60]
def cmd_save_local(args):
src = Path(args.file)
if not src.exists():
die(f"File not found: {src}")
output_dir = Path(args.output_dir)
os.makedirs(output_dir, exist_ok=True)
branch_part = _sanitize_branch(args.branch) if args.branch else "unknown"
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
stem = re.sub(r"[^a-zA-Z0-9_-]", "", src.stem)[:40] or "artifact"
filename = f"{branch_part}-{timestamp}-{stem}{src.suffix}"
dest = output_dir / filename
shutil.copy2(src, dest)
dest_abs = str(dest.resolve())
print(f"Saved: {dest_abs}")
print(dest_abs)
# --- Main ---
def main():
@@ -656,6 +688,7 @@ Commands:
terminal-recording --output O --tape T Run VHS tape
preview FILE Upload to litterbox (1h expiry)
upload FILE_OR_URL Upload/promote to catbox.moe (permanent)
save-local --file F --branch B Save artifact locally instead of uploading
""",
)
sub = parser.add_subparsers(dest="command")
@@ -702,6 +735,13 @@ Commands:
p_upload = sub.add_parser("upload", help="Upload or promote to catbox.moe (permanent)")
p_upload.add_argument("source", help="Local file path or URL to promote")
# save-local
p_save = sub.add_parser("save-local", help="Save artifact locally instead of uploading")
p_save.add_argument("--file", required=True, help="Artifact file to save")
p_save.add_argument("--branch", default="", help="Branch name for filename")
default_dir = str(Path(os.environ.get("TMPDIR", "/tmp")) / "compound-engineering" / "ce-demo-reel")
p_save.add_argument("--output-dir", default=default_dir, help="Target directory")
args = parser.parse_args()
if not args.command:
@@ -717,6 +757,7 @@ Commands:
"terminal-recording": cmd_terminal_recording,
"preview": cmd_preview,
"upload": cmd_upload,
"save-local": cmd_save_local,
}
dispatch[args.command](args)