feat(ce-demo-reel): add local save as alternative to catbox upload (#647)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user