Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ This dogfood bundle uses VHS as the outer camera for real Codex and Claude inter
| -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| [![Codex agent-tty demo](./dogfood/agent-uses-agent-tty/artifacts/codex-thumbnail.png)](./dogfood/agent-uses-agent-tty/artifacts/codex-outer.webm) | [![Claude agent-tty demo](./dogfood/agent-uses-agent-tty/artifacts/claude-thumbnail.png)](./dogfood/agent-uses-agent-tty/artifacts/claude-outer.webm) |

GitHub may show these checked-in WebM proof files as raw downloads. See [`VIDEO_PLAYBACK.md`](./dogfood/agent-uses-agent-tty/VIDEO_PLAYBACK.md) for the upload-ready H.264 MP4 flow that turns the thumbnails into GitHub video-player links.

See [`dogfood/agent-uses-agent-tty/`](./dogfood/agent-uses-agent-tty/) for the Hero Demo reproducer, outer transcripts, inner Neovim recordings, and final file proofs.

## Common Usage
Expand Down
2 changes: 2 additions & 0 deletions dogfood/agent-uses-agent-tty/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
This bundle is the README-facing **Hero Demo** for real coding-agent TUIs using `agent-tty`.
VHS records the outer Codex and Claude Code TUIs as the presentation layer. The product proof is the inner `agent-tty` artifact set produced while each real agent explores the skill and CLI, drives Neovim, and exports recordings.

GitHub may show checked-in WebM files as raw downloads; see [VIDEO_PLAYBACK.md](./VIDEO_PLAYBACK.md) for the H.264 attachment flow used to turn these thumbnails into GitHub video-player links.

| Agent | Outer Hero Demo | Inner proof artifacts | File proof |
| ------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------ |
| Codex | [![Codex Hero Demo](./artifacts/codex-thumbnail.png)](./artifacts/codex-outer.webm) | [cast](./artifacts/codex-inner-nvim.cast), [WebM](./artifacts/codex-inner-nvim.webm) | [proof](./artifacts/codex-final-file-proof.txt) |
Expand Down
66 changes: 66 additions & 0 deletions dogfood/agent-uses-agent-tty/VIDEO_PLAYBACK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Hero Demo GitHub video playback

GitHub repository file pages may show checked-in video files as raw downloads instead of a player. The canonical Hero Demo recordings stay checked in as WebM proof artifacts, but README-facing playback should use GitHub-uploaded H.264 MP4 attachment URLs.

## Current recommendation

1. Keep these checked-in proof artifacts as the source of truth:
- `artifacts/codex-outer.webm`
- `artifacts/claude-outer.webm`
2. Generate upload-only H.264 MP4 copies from those WebMs.
3. Upload the MP4 copies through GitHub's Markdown attachment flow.
4. Replace the README thumbnail link targets with the resulting `https://github.com/user-attachments/assets/...` URLs.

The MP4 copies are derived playback assets, not canonical proof artifacts. They should live under `.debug/video-upload/` locally unless the project later chooses a Pages gallery or another committed-media route.

## Prepare upload assets

From the repository root:

```bash
mise run demo:agent-uses-agent-tty:upload-assets
```

The task uses the pinned `ffmpeg`/`ffprobe` from `mise.toml`, converts both outer WebMs to H.264 MP4 files, writes ffprobe metadata, and writes checksums under `.debug/video-upload/`.

Expected constraints for the promoted 2026-05-21 recordings:

| Agent | Upload file | Expected codec | Expected dimensions | Expected size |
| ------ | ------------------------------------------- | ----------------- | ------------------- | ------------- |
| Codex | `.debug/video-upload/codex-outer-h264.mp4` | H.264 / `yuv420p` | 1600x900 | ~3.3 MB |
| Claude | `.debug/video-upload/claude-outer-h264.mp4` | H.264 / `yuv420p` | 1600x900 | ~3.0 MB |

Both expected sizes are below GitHub's 10 MB video attachment limit for free plans.

## Upload through GitHub

GitHub does not expose a supported PAT-backed API for creating `user-attachments` video URLs. Use the web Markdown editor flow:

1. Open any GitHub Markdown text area with write access to `coder/agent-tty`:
- a draft issue body,
- a PR comment,
- or the web editor for a Markdown file.
2. Drag `codex-outer-h264.mp4` into the text area and wait for GitHub to replace it with a `https://github.com/user-attachments/assets/...` URL.
3. Drag `claude-outer-h264.mp4` into the text area and copy its URL too.
4. The comment or draft does not need to be submitted if the URLs have been copied.
5. Verify each copied URL opens a GitHub video player before editing the README.

## README patch after upload

Apply the copied attachment URLs with the helper task:

```bash
mise run demo:agent-uses-agent-tty:apply-video-urls -- \
--codex-url https://github.com/user-attachments/assets/REPLACE-CODEX-H264-MP4 \
--claude-url https://github.com/user-attachments/assets/REPLACE-CLAUDE-H264-MP4
```

The task updates the root README, the bundle README, and the bundle manifest entry for `README.md`. Verify the result with:

```bash
npm run validate-bundle:canonical
```

## Fallback

If GitHub attachment URLs are rejected for maintainability, use a GitHub Pages gallery with `<video controls>` and committed H.264 MP4 playback copies. Do not point README thumbnails at committed repository videos as the primary playback path; GitHub may show those as raw downloads.
10 changes: 8 additions & 2 deletions dogfood/agent-uses-agent-tty/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,14 @@
{
"path": "README.md",
"description": "Hero Demo bundle README",
"sha256": "dac35e0a5702cd749f726428ececc4905183c89cb4b3580e666c39bcb444fc8b",
"bytes": 1406
"sha256": "d6e0d3741409d1dd677f5b6e8236548c8e82ba832e79161d14bcd40c12fb4cf5",
"bytes": 1600
},
{
"path": "VIDEO_PLAYBACK.md",
"description": "GitHub video playback guidance for the Hero Demo",
"sha256": "0c7447a0d8f77694048eb686e5313719925ceda37c083439200fc15ffd81865c",
"bytes": 3315
},
{
"path": "reproduce.sh",
Expand Down
2 changes: 1 addition & 1 deletion mise.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html

[[tools.actionlint]]
version = "1.7.12"
Expand Down
34 changes: 32 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
actionlint = "1.7.12"
communique = "1.1.3"
zizmor = "1.24.1"
# Live-demo-only recorder tools are pinned inside src/tools/hero-demo.ts so ordinary CI stays credential- and recorder-tool-free.
# CI installs [tools] with `mise install --locked`; update mise.lock whenever tool versions or supported CI platforms change.
node = "26"
python = "3"
Expand Down Expand Up @@ -37,7 +36,38 @@ run = "npm run smoke:install -- --skip-build"

[tasks."demo:agent-uses-agent-tty"]
description = "Regenerate the real-agent Hero Demo proof bundle"
run = "npx tsx src/tools/hero-demo.ts"
run = '''
PATH="$(mise bin-paths vhs@0.11.0 ttyd@1.7.7 ffmpeg@8.1.1 | paste -sd: -):$PATH" \
npx tsx src/tools/hero-demo.ts
'''
tools.ffmpeg = "8.1.1"
tools.vhs = "0.11.0"
tools.ttyd = "1.7.7"

[tasks."demo:agent-uses-agent-tty:upload-assets"]
description = "Prepare H.264 MP4 upload assets for the Hero Demo"
run = '''
HERO_VIDEO_FFMPEG="$(mise which ffmpeg --tool ffmpeg@8.1.1)" \
HERO_VIDEO_FFPROBE="$(mise which ffprobe --tool ffmpeg@8.1.1)" \
npx tsx src/tools/hero-video-playback.ts prepare-upload-assets
'''
tools.ffmpeg = "8.1.1"
sources = [
"dogfood/agent-uses-agent-tty/artifacts/codex-outer.webm",
"dogfood/agent-uses-agent-tty/artifacts/claude-outer.webm",
"src/tools/hero-video-playback.ts",
]
outputs = ["dogfood/agent-uses-agent-tty/.debug/video-upload/*"]

[tasks."demo:agent-uses-agent-tty:apply-video-urls"]
description = "Apply GitHub user-attachment video URLs to the Hero Demo READMEs"
run = "npx tsx src/tools/hero-video-playback.ts apply-video-urls"
sources = [
"README.md",
"dogfood/agent-uses-agent-tty/README.md",
"dogfood/agent-uses-agent-tty/manifest.json",
"src/tools/hero-video-playback.ts",
]

[tasks.validate-bundles]
description = "Validate canonical proof bundles against the canonical schema"
Expand Down
72 changes: 72 additions & 0 deletions src/tools/canonicalBundleArtifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { join } from 'node:path';
import { pipeline } from 'node:stream/promises';

import type {
CanonicalBundleArtifact,
CanonicalBundleManifest,
} from './bundleManifestSchema.js';
import { CanonicalBundleManifestSchema } from './bundleManifestSchema.js';
import {
readValidatedJsonFile,
writeValidatedJsonFile,
} from '../storage/manifests.js';
import { invariant } from '../util/assert.js';

export async function sha256File(path: string): Promise<string> {
const hash = createHash('sha256');
await pipeline(createReadStream(path), hash);
return hash.digest('hex');
}

export async function canonicalBundleArtifactEntry(
bundleDir: string,
relativePath: string,
description: string,
): Promise<CanonicalBundleArtifact> {
const fullPath = join(bundleDir, relativePath);
const stats = await stat(fullPath);
return {
path: relativePath,
description,
sha256: await sha256File(fullPath),
bytes: stats.size,
};
}

function validateCanonicalBundleManifest(
_path: string,
data: unknown,
): CanonicalBundleManifest {
return CanonicalBundleManifestSchema.parse(data);
}

export async function readCanonicalBundleManifest(
path: string,
): Promise<CanonicalBundleManifest> {
const manifest = await readValidatedJsonFile({
path,
pathLabel: 'canonical bundle manifest path',
allowMissing: false,
readErrorMessage: `Failed to read canonical bundle manifest at ${path}.`,
invalidJsonMessage: `Canonical bundle manifest contains invalid JSON at ${path}.`,
validate: validateCanonicalBundleManifest,
});
invariant(manifest !== null, 'canonical bundle manifest must exist');
return manifest;
}

export async function writeCanonicalBundleManifest(
path: string,
manifest: CanonicalBundleManifest,
): Promise<void> {
await writeValidatedJsonFile({
path,
pathLabel: 'canonical bundle manifest path',
data: manifest,
writeErrorMessage: `Failed to write canonical bundle manifest at ${path}.`,
validate: validateCanonicalBundleManifest,
});
}
67 changes: 13 additions & 54 deletions src/tools/hero-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ import { basename, dirname, join, resolve } from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';

import type { CanonicalBundleArtifact } from './bundleManifestSchema.js';
import { CanonicalBundleManifestSchema } from './bundleManifestSchema.js';
import { canonicalBundleArtifactEntry } from './canonicalBundleArtifacts.js';
import { invariant } from '../util/assert.js';
import { isDirectExecution } from '../util/isDirectExecution.js';

const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const DEFAULT_BUNDLE_DIR = join(REPO_ROOT, 'dogfood/agent-uses-agent-tty');
const VIDEO_PLAYBACK_DOC = 'VIDEO_PLAYBACK.md';
const DEFAULT_EXPECTED_TEXT =
'agent-tty nested Neovim proof from a real coding agent.';
const DEFAULT_RECORD_SECONDS = 3 * 60;
Expand All @@ -40,8 +41,6 @@ const OUTER_FONT_SIZE = 14;
const CLAUDE_VISUAL_REDACTION_HEIGHT = Math.floor(OUTER_HEIGHT / 5);
const CLAUDE_VISUAL_REDACTION_FILTER = `drawbox=x=0:y=0:w=iw:h=${String(CLAUDE_VISUAL_REDACTION_HEIGHT)}:color=black:t=fill`;

const DEMO_TOOL_SPECS = ['vhs@0.11.0', 'ttyd@1.7.7', 'ffmpeg@8.1.1'];

type AgentName = (typeof AGENTS)[number];

export interface HeroDemoOptions {
Expand Down Expand Up @@ -447,33 +446,10 @@ async function assertNonEmpty(path: string): Promise<void> {
);
}

let cachedDemoToolPathPrefix: string | undefined;

function demoToolPathPrefix(): string {
if (cachedDemoToolPathPrefix === undefined) {
run('mise', ['install', ...DEMO_TOOL_SPECS]);
}
cachedDemoToolPathPrefix ??= run('mise', ['bin-paths', ...DEMO_TOOL_SPECS])
.trim()
.split('\n')
.filter(Boolean)
.join(':');
invariant(cachedDemoToolPathPrefix !== '', 'demo tool PATH is empty');
return cachedDemoToolPathPrefix;
}

function demoToolEnv(): NodeJS.ProcessEnv {
return {
...process.env,
PATH: `${demoToolPathPrefix()}:${process.env.PATH ?? ''}`,
};
}

function runDemoTool(command: string, args: string[], cwd = REPO_ROOT): string {
return execFileSync(command, args, {
cwd,
encoding: 'utf8',
env: demoToolEnv(),
stdio: ['ignore', 'pipe', 'pipe'],
});
}
Expand Down Expand Up @@ -677,7 +653,6 @@ async function runOne(
runDir,
vhsLog,
(options.recordSeconds + RECORD_TIMEOUT_BUFFER_SECONDS) * 1000,
demoToolEnv(),
);
ensureThumbnail(runDir);

Expand Down Expand Up @@ -765,29 +740,8 @@ function sha256Text(text: string): string {
return createHash('sha256').update(text).digest('hex');
}

async function sha256File(path: string): Promise<string> {
return createHash('sha256')
.update(await readFile(path))
.digest('hex');
}

async function artifactEntry(
bundleDir: string,
relativePath: string,
description: string,
): Promise<CanonicalBundleArtifact> {
const fullPath = join(bundleDir, relativePath);
const stats = await stat(fullPath);
return {
path: relativePath,
description,
sha256: await sha256File(fullPath),
bytes: stats.size,
};
}

async function cleanBundle(bundleDir: string): Promise<void> {
const keep = new Set(['.gitignore']);
const keep = new Set(['.gitignore', VIDEO_PLAYBACK_DOC]);
for (const entry of await readdir(bundleDir, { withFileTypes: true })) {
if (keep.has(entry.name)) {
continue;
Expand Down Expand Up @@ -901,6 +855,10 @@ async function promote(
path: 'README.md',
description: 'Hero Demo bundle README',
});
promotedPaths.push({
path: VIDEO_PLAYBACK_DOC,
description: 'GitHub video playback guidance for the Hero Demo',
});

const reproducePath = join(options.bundleDir, 'reproduce.sh');
await writeExecutable(reproducePath, renderReproduce(options));
Expand All @@ -914,7 +872,7 @@ async function promote(
const manifestArtifacts = [];
for (const promoted of promotedPaths) {
manifestArtifacts.push(
await artifactEntry(
await canonicalBundleArtifactEntry(
options.bundleDir,
promoted.path,
promoted.description,
Expand Down Expand Up @@ -973,11 +931,10 @@ function safeToolVersion(
}

function collectToolVersions(): Array<[string, string]> {
const demoEnv = demoToolEnv();
return [
['vhs', safeToolVersion('vhs', ['--version'], demoEnv)],
['ttyd', safeToolVersion('ttyd', ['--version'], demoEnv)],
['ffmpeg', safeToolVersion('ffmpeg', ['-version'], demoEnv)],
['vhs', safeToolVersion('vhs', ['--version'])],
['ttyd', safeToolVersion('ttyd', ['--version'])],
['ffmpeg', safeToolVersion('ffmpeg', ['-version'])],
['codex', safeToolVersion('codex', ['--version'])],
['claude', safeToolVersion('claude', ['--version'])],
];
Expand Down Expand Up @@ -1026,6 +983,8 @@ function renderReadme(): string {
This bundle is the README-facing **Hero Demo** for real coding-agent TUIs using \`agent-tty\`.
VHS records the outer Codex and Claude Code TUIs as the presentation layer. The product proof is the inner \`agent-tty\` artifact set produced while each real agent explores the skill and CLI, drives Neovim, and exports recordings.

GitHub may show checked-in WebM files as raw downloads; see [${VIDEO_PLAYBACK_DOC}](./${VIDEO_PLAYBACK_DOC}) for the H.264 attachment flow used to turn these thumbnails into GitHub video-player links.

| Agent | Outer Hero Demo | Inner proof artifacts | File proof |
| --- | --- | --- | --- |
| Codex | [![Codex Hero Demo](./artifacts/codex-thumbnail.png)](./artifacts/codex-outer.webm) | [cast](./artifacts/codex-inner-nvim.cast), [WebM](./artifacts/codex-inner-nvim.webm) | [proof](./artifacts/codex-final-file-proof.txt) |
Expand Down
Loading
Loading