chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A promo of what someone can do with knotless, filmed on the pages
marketing/video/change/sample.jsondeleted+0−310
| @@ -1,310 +0,0 @@ | ||
{ | ||
"host": "knotless.dev", | ||
"repo": "chadtmiller.com/knotless", | ||
"pull": { | ||
"uri": "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.pull/3mzth2v6xpbk7", | ||
"url": "https://knotless.dev/chadtmiller.com/knotless/pull/did:plc:bcgltzqazw5tb6k2g3ttenbj/3mzth2v6xpbk7", | ||
"title": "A profile's private repositories show their pushes to those who may read them", | ||
"author": "chadtmiller.com", | ||
"branch": "serve/private-activity", | ||
"base": "main", | ||
"opened": "2026-11-08T22:20:45.478Z", | ||
"state": "merged" | ||
}, | ||
"diff": { | ||
"files": [ | ||
{ | ||
"path": "crates/knotless-serve/src/lib.rs", | ||
"added": 4, | ||
"removed": 4 | ||
}, | ||
{ | ||
"path": "crates/knotless-serve/src/web/actor.rs", | ||
"added": 30, | ||
"removed": 3 | ||
}, | ||
{ | ||
"path": "crates/knotless-serve/tests/gateway.rs", | ||
"added": 16, | ||
"removed": 9 | ||
}, | ||
{ | ||
"path": "docs/design/access.md", | ||
"added": 1, | ||
"removed": 1 | ||
}, | ||
{ | ||
"path": "docs/reference/api.md", | ||
"added": 5, | ||
"removed": 1 | ||
}, | ||
{ | ||
"path": "lexicons/dev/knotless/getActorActivity.json", | ||
"added": 1, | ||
"removed": 1 | ||
} | ||
], | ||
"excerpt": { | ||
"path": "crates/knotless-serve/src/web/actor.rs", | ||
"lines": [ | ||
{ | ||
"mark": " ", | ||
"n": 373, | ||
"text": "}" | ||
}, | ||
{ | ||
"mark": " ", | ||
"n": 374, | ||
"text": "" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 375, | ||
"text": "/// The pushes to `did`'s private repositories that `viewer` may read" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 376, | ||
"text": "/// ([`private_of`]), as this server's index of their spaces counts them." | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 377, | ||
"text": "fn private_pushes(" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 378, | ||
"text": " app: &Arc<App>," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 379, | ||
"text": " did: &str," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 380, | ||
"text": " viewer: Option<String>," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 381, | ||
"text": " since: Option<&str>," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 382, | ||
"text": " tz: &str," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 383, | ||
"text": ") -> rusqlite::Result<Vec<crate::index::RepoPushes>> {" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 384, | ||
"text": " let shown = private_of(app, did, viewer).unwrap_or_default();" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 385, | ||
"text": " let names: HashSet<String> = shown.into_iter().map(|(name, _, _)| name).collect();" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 386, | ||
"text": " let Some(index) = app.spaces.as_ref().filter(|_| !names.is_empty()) else {" | ||
} | ||
] | ||
} | ||
}, | ||
"pushes": [ | ||
{ | ||
"commit": "65f5493b372769d9b5c4b6d07d4038af49109cda", | ||
"at": "2026-11-08T22:20:52.622Z", | ||
"checks": [ | ||
{ | ||
"workflow": "docs", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:20:52.622Z", | ||
"finished": "2026-11-08T22:21:08.682Z" | ||
}, | ||
{ | ||
"workflow": "check", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:20:52.941Z", | ||
"finished": "2026-11-08T22:21:15.894Z" | ||
}, | ||
{ | ||
"workflow": "web", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:20:52.931Z", | ||
"finished": "2026-11-08T22:21:15.955Z" | ||
}, | ||
{ | ||
"workflow": "lexicons", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:21:14.293Z", | ||
"finished": "2026-11-08T22:21:23.684Z" | ||
}, | ||
{ | ||
"workflow": "ios", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:21:13.329Z", | ||
"finished": "2026-11-08T22:22:16.371Z" | ||
}, | ||
{ | ||
"workflow": "shots", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:21:22.499Z", | ||
"finished": "2026-11-08T22:22:16.685Z" | ||
}, | ||
{ | ||
"workflow": "e2e", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:20:52.623Z", | ||
"finished": "2026-11-08T22:25:49.817Z" | ||
}, | ||
{ | ||
"workflow": "test", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:22:13.073Z", | ||
"finished": "2026-11-08T22:29:08.956Z" | ||
} | ||
] | ||
}, | ||
{ | ||
"commit": "d5a939294d78ef674a3ed1f988b34129feea48b3", | ||
"at": "2026-11-08T22:29:14.102Z", | ||
"checks": [ | ||
{ | ||
"workflow": "docs", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:14.102Z", | ||
"finished": "2026-11-08T22:29:25.385Z" | ||
}, | ||
{ | ||
"workflow": "check", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:14.417Z", | ||
"finished": "2026-11-08T22:29:33.042Z" | ||
}, | ||
{ | ||
"workflow": "lexicons", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:31.445Z", | ||
"finished": "2026-11-08T22:29:35.737Z" | ||
}, | ||
{ | ||
"workflow": "ios", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:23.795Z", | ||
"finished": "2026-11-08T22:30:05.990Z" | ||
}, | ||
{ | ||
"workflow": "shots", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:35.268Z", | ||
"finished": "2026-11-08T22:30:12.063Z" | ||
}, | ||
{ | ||
"workflow": "web", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:30:10.093Z", | ||
"finished": "2026-11-08T22:30:38.109Z" | ||
}, | ||
{ | ||
"workflow": "e2e", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:14.102Z", | ||
"finished": "2026-11-08T22:33:25.681Z" | ||
}, | ||
{ | ||
"workflow": "test", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:30:03.015Z", | ||
"finished": "2026-11-08T22:37:10.101Z" | ||
} | ||
] | ||
} | ||
], | ||
"reviews": [ | ||
{ | ||
"by": "review.knotless.dev", | ||
"verdict": "changes", | ||
"at": "2026-11-08T22:22:05.245Z", | ||
"commit": "65f5493b372769d9b5c4b6d07d4038af49109cda", | ||
"said": "The activity change is sound. getActorActivity now sits behind the viewer middleware and adds only the private repositories private_of allows. The new test covers the member, no one, and an outsider.", | ||
"line": { | ||
"path": ".knotless/workflows/deploy.yml", | ||
"line": 104, | ||
"said": "Dropping chadtmiller.com/group-host here depends on an invite that was never left for the existing share." | ||
} | ||
}, | ||
{ | ||
"by": "review.knotless.dev", | ||
"verdict": "approve", | ||
"at": "2026-11-08T22:29:30.940Z", | ||
"commit": "d5a939294d78ef674a3ed1f988b34129feea48b3", | ||
"said": "The unrelated commit is gone. pull_diff now shows only the activity change, with the matching docs and lexicon text, so the title and body describe the pull.", | ||
"line": null | ||
} | ||
], | ||
"queued": "2026-11-08T22:37:24.182Z", | ||
"landed": { | ||
"commit": "9382c0bbb9f6ad694a8a39deaffc2ebc706c6ac5", | ||
"at": "2026-11-08T22:37:31Z", | ||
"batch": true | ||
}, | ||
"deploy": { | ||
"workflow": "deploy", | ||
"status": "success", | ||
"started": "2026-11-08T22:38:12.898Z", | ||
"finished": "2026-11-08T22:46:23.896Z", | ||
"steps": [ | ||
{ | ||
"name": "tools", | ||
"ms": 1139, | ||
"status": "success" | ||
}, | ||
{ | ||
"name": "build", | ||
"ms": 433042, | ||
"status": "success" | ||
}, | ||
{ | ||
"name": "deploy", | ||
"ms": 40955, | ||
"status": "success" | ||
}, | ||
{ | ||
"name": "image", | ||
"ms": 4894, | ||
"status": "success" | ||
}, | ||
{ | ||
"name": "runner host", | ||
"ms": 6291, | ||
"status": "success" | ||
} | ||
] | ||
} | ||
} | ||
marketing/video/change/story.mjsdeleted+0−276
| @@ -1,276 +0,0 @@ | ||
// A pull's way from its first push to the deploy that shipped it, read | ||
// from the appview through `knotless-mcp --read-only`, as the Change video | ||
// tells it: each push and its checks, each review, the queue, the commit it | ||
// landed as and the deploy of that commit. Written to out/stories/<rkey>.json. | ||
// | ||
// node change/story.mjs https://knotless.dev/<handle>/<name>/pull/<did>/<rkey> | ||
// node change/story.mjs <handle>/<name> <pull at-uri> [--deploy <workflow>] | ||
// | ||
// Nothing here is made up: a stage the pull never reached (unreviewed, merged | ||
// by hand, never deployed) is left out, and the video skips it. | ||
| ||
import { spawn } from 'node:child_process'; | ||
import { mkdirSync, writeFileSync } from 'node:fs'; | ||
import { dirname, join } from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
| ||
const root = join(dirname(fileURLToPath(import.meta.url)), '..'); | ||
const host = new URL(process.env.KNOTLESS_APPVIEW || 'https://knotless.dev').host; | ||
| ||
const args = process.argv.slice(2); | ||
const flag = (name, otherwise) => { | ||
const i = args.indexOf(name); | ||
if (i < 0) return otherwise; | ||
const [, value] = args.splice(i, 2); | ||
return value; | ||
}; | ||
const deployWorkflow = flag('--deploy', 'deploy'); | ||
const [repo, uri] = (() => { | ||
if (args[0]?.startsWith('http')) { | ||
const [handle, name, kind, did, rkey] = new URL(args[0]).pathname.split('/').filter(Boolean); | ||
if (kind !== 'pull') throw new Error(`not a pull's page: ${args[0]}`); | ||
return [`${handle}/${name}`, `at://${did}/dev.knotless.pull/${rkey}`]; | ||
} | ||
if (args.length !== 2) { | ||
console.error('usage: story.mjs <pull page url> | <handle>/<name> <pull at-uri>'); | ||
process.exit(2); | ||
} | ||
return args; | ||
})(); | ||
| ||
/// The tools of a read-only knotless-mcp, called in turn over its stdio. | ||
const mcp = () => { | ||
const child = spawn('knotless-mcp', ['--read-only'], { stdio: ['pipe', 'pipe', 'inherit'] }); | ||
const waiting = new Map(); | ||
let id = 0; | ||
let buf = ''; | ||
child.stdout.on('data', (d) => { | ||
buf += d; | ||
for (let i = buf.indexOf('\n'); i >= 0; i = buf.indexOf('\n')) { | ||
const line = buf.slice(0, i); | ||
buf = buf.slice(i + 1); | ||
if (!line.trim()) continue; | ||
const m = JSON.parse(line); | ||
waiting.get(m.id)?.(m); | ||
waiting.delete(m.id); | ||
} | ||
}); | ||
const ask = (method, params) => | ||
new Promise((resolve, reject) => { | ||
const n = ++id; | ||
waiting.set(n, (m) => (m.error ? reject(new Error(m.error.message)) : resolve(m.result))); | ||
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: n, method, params })}\n`); | ||
}); | ||
const ready = ask('initialize', { | ||
protocolVersion: '2025-06-18', | ||
capabilities: {}, | ||
clientInfo: { name: 'knotless-video', version: '0' } | ||
}).then(() => | ||
child.stdin.write( | ||
`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n` | ||
) | ||
); | ||
return { | ||
/// The tool's text, as JSON when `json`. | ||
call: async (name, args, json = true) => { | ||
await ready; | ||
const r = await ask('tools/call', { name, arguments: args }); | ||
const text = r.content.find((c) => c.type === 'text')?.text ?? ''; | ||
if (r.isError) throw new Error(`${name}: ${text}`); | ||
return json ? JSON.parse(text) : text; | ||
}, | ||
close: () => child.kill() | ||
}; | ||
}; | ||
| ||
/// The diff's files, each with its lines, from pull_diff's numbered form: | ||
/// `old new | ±text` under each `diff --git` and `@@` head. | ||
const parseDiff = (text) => { | ||
const files = []; | ||
let file; | ||
let hunk; | ||
for (const line of text.split('\n')) { | ||
if (line.startsWith('diff --git ')) { | ||
file = { path: line.split(' b/').pop(), added: 0, removed: 0, hunks: [] }; | ||
files.push(file); | ||
continue; | ||
} | ||
if (!file) continue; | ||
if (line.startsWith('@@')) { | ||
hunk = { head: line, lines: [] }; | ||
file.hunks.push(hunk); | ||
continue; | ||
} | ||
const m = /^\s*(\d*)\s+(\d*) \| ([ +-])(.*)$/.exec(line); | ||
if (!m || !hunk) continue; | ||
const [, old, now, mark, body] = m; | ||
if (mark === '+') file.added++; | ||
if (mark === '-') file.removed++; | ||
hunk.lines.push({ mark, n: Number(now || old), text: body }); | ||
} | ||
return files; | ||
}; | ||
| ||
// What a reader of the video learns least from: words about the change, | ||
// rather than the change, and then its tests. | ||
const prose = /(^docs\/|\.md$|^\.changeset\/|CHANGELOG|\.lock$|package-lock\.json$|fixtures)/; | ||
const tests = /(\/tests?\/|_test\.|\.test\.|\.spec\.)/; | ||
| ||
/// The stretch of the diff that says most: of the code (not its tests, if | ||
/// it has any other), the hunk adding the most lines, cut to `most` lines | ||
/// from just before its first change. | ||
const excerpt = (files, most = 14) => { | ||
const code = files.filter((f) => !prose.test(f.path)); | ||
const source = code.filter((f) => !tests.test(f.path)); | ||
const pool = source.length ? source : code.length ? code : files; | ||
const pick = pool | ||
.flatMap((f) => f.hunks.map((h) => ({ path: f.path, h }))) | ||
.map((x) => ({ ...x, added: x.h.lines.filter((l) => l.mark === '+').length })) | ||
.sort((a, b) => b.added - a.added)[0]; | ||
if (!pick) return null; | ||
const lines = pick.h.lines; | ||
const first = lines.findIndex((l) => l.mark !== ' '); | ||
const start = Math.max(0, Math.min(first - 2, lines.length - most)); | ||
return { | ||
path: pick.path, | ||
lines: lines.slice(start, start + most).map((l) => ({ | ||
...l, | ||
text: l.text.replace(/\t/g, ' ').trimEnd() | ||
})) | ||
}; | ||
}; | ||
| ||
/// A body's opening sentences, as many whole ones as fit in `most` | ||
/// characters (the first cut short if it alone does not), as plain text. | ||
const firstSaid = (body, most = 200) => { | ||
const plain = body | ||
.replace(/```[\s\S]*?```/g, '') | ||
.replace(/\*\*|`|^#+\s*|^>\s*/gm, '') | ||
.replace(/\s+/g, ' ') | ||
.trim(); | ||
const sentences = plain.split(/(?<=[.!?])\s+/); | ||
let said = ''; | ||
for (const s of sentences) { | ||
if (`${said} ${s}`.trim().length > most) break; | ||
said = `${said} ${s}`.trim(); | ||
} | ||
if (said) return said; | ||
return `${plain.slice(0, most - 1).trimEnd()}…`; | ||
}; | ||
| ||
const at = (t) => new Date(t).getTime(); | ||
| ||
const tools = mcp(); | ||
try { | ||
const shown = await tools.call('pull_show', { repo, uri }); | ||
const pull = shown.item; | ||
const files = parseDiff(await tools.call('pull_diff', { repo, uri }, false)); | ||
const runs = await tools.call('run_list', { repo, pull: uri }).catch(() => []); | ||
| ||
// A push is the commit the pull's checks ran at, first seen when the first | ||
// of them started; its checks are the latest of each workflow there. | ||
const byCommit = new Map(); | ||
for (const r of runs) { | ||
const list = byCommit.get(r.commit) ?? []; | ||
if (!list.some((s) => s.workflow === r.workflow)) list.push(r); | ||
byCommit.set(r.commit, list); | ||
} | ||
const pushes = [...byCommit.entries()] | ||
.map(([commit, checks]) => ({ | ||
commit, | ||
at: checks.map((c) => c.startedAt).sort()[0], | ||
checks: checks | ||
.map((c) => ({ | ||
workflow: c.workflow, | ||
status: c.status, | ||
skipped: Boolean(c.skipped), | ||
started: c.startedAt, | ||
finished: c.finishedAt ?? null | ||
})) | ||
.sort((a, b) => at(a.finished ?? a.started) - at(b.finished ?? b.started)) | ||
})) | ||
.sort((a, b) => at(a.at) - at(b.at)); | ||
| ||
// Each review, with the first of the line comments it left. | ||
const reviews = shown.comments | ||
.filter((c) => c.verdict) | ||
.map((c) => { | ||
const line = shown.comments.find((l) => l.review === c.uri && l.line); | ||
return { | ||
by: c.handle, | ||
verdict: c.verdict, | ||
at: c.createdAt, | ||
commit: c.commit, | ||
said: firstSaid(c.body), | ||
line: line | ||
? { path: line.line.path, line: line.line.line, said: firstSaid(line.body, 140) } | ||
: null | ||
}; | ||
}); | ||
| ||
// Landed: the commit on the base the merge made, dated when it was made. | ||
const merged = pull.merged ?? null; | ||
const landedCommit = merged && shown.compare?.commits?.find((c) => c.sha === merged); | ||
const landed = merged | ||
? { | ||
commit: merged, | ||
at: landedCommit?.committer?.date ?? null, | ||
batch: Boolean(pull.mergedBatch) | ||
} | ||
: null; | ||
| ||
// Deployed: the deploy workflow's run at the landed commit, on the base. | ||
let deploy = null; | ||
if (merged) { | ||
const onBase = await tools | ||
.call('run_list', { repo, ref: pull.base, commit: merged, workflow: deployWorkflow }) | ||
.catch(() => []); | ||
const run = onBase[0]; | ||
if (run) | ||
deploy = { | ||
workflow: run.workflow, | ||
status: run.status, | ||
started: run.startedAt, | ||
finished: run.finishedAt ?? null, | ||
steps: run.steps | ||
.filter((s) => s.durationMs >= 1000) | ||
.map((s) => ({ name: s.name, ms: s.durationMs, status: s.status })) | ||
}; | ||
} | ||
| ||
const story = { | ||
host, | ||
repo, | ||
pull: { | ||
uri, | ||
url: `https://${host}/${repo}/pull/${uri.split('/')[2]}/${uri.split('/').pop()}`, | ||
title: pull.title, | ||
author: pull.handle, | ||
branch: pull.head.branch, | ||
base: pull.base, | ||
opened: pull.createdAt, | ||
state: pull.state | ||
}, | ||
diff: { | ||
files: files.map(({ path, added, removed }) => ({ path, added, removed })), | ||
excerpt: excerpt(files) | ||
}, | ||
pushes, | ||
reviews, | ||
queued: pull.queue?.createdAt ?? null, | ||
landed, | ||
deploy | ||
}; | ||
const out = join(root, 'out/stories'); | ||
mkdirSync(out, { recursive: true }); | ||
const file = join(out, `${uri.split('/').pop()}.json`); | ||
writeFileSync(file, `${JSON.stringify(story, null, 1)}\n`); | ||
const span = deploy?.finished ?? landed?.at ?? pushes.at(-1)?.at; | ||
const minutes = span ? Math.round((at(span) - at(pull.createdAt)) / 60000) : 0; | ||
console.log( | ||
`${file}: ${files.length} files, ${pushes.length} pushes, ${reviews.length} reviews, ` + | ||
`${landed ? 'landed' : 'not landed'}, ${deploy ? 'deployed' : 'no deploy'}; ${minutes} min` | ||
); | ||
} finally { | ||
tools.close(); | ||
} | ||
marketing/video/public/sfx/click.wavnew+0−0
A binary file, not shown.
marketing/video/public/sfx/key1.wavnew+0−0
A binary file, not shown.
marketing/video/public/sfx/key2.wavnew+0−0
A binary file, not shown.
marketing/video/public/sfx/key3.wavnew+0−0
A binary file, not shown.
marketing/video/shoot/scenes/agent.mjsnew+103−0
| @@ -0,0 +1,103 @@ | ||
// alice asks Claude Code for a change, and it makes it over knotless's MCP | ||
// tools as her, from a branch to an open pull, with no checkout. The run | ||
// is real (and costs model usage): its transcript is what the video shows, | ||
// as public/agent/agent.json. Then the pull it opened, on the pages. | ||
| ||
import { spawn } from 'node:child_process'; | ||
import { mkdirSync, writeFileSync } from 'node:fs'; | ||
import { dirname, join } from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
import { environment, root } from '../../../../harness/shots/world.mjs'; | ||
import { film } from '../camera.mjs'; | ||
import { REPO } from './push.mjs'; | ||
| ||
const here = join(dirname(fileURLToPath(import.meta.url)), '../..'); | ||
| ||
export const ASK = `Add an Install section to the README of knotless://alice.test/${REPO} (cargo install --path .) and open a pull for it.`; | ||
| ||
/// Claude Code, given only the knotless tools as `who`, doing `ask`: what it | ||
/// said and the tools it called, in order. | ||
async function claude(world, who, ask) { | ||
const dir = join(world.dir, who, 'claude'); | ||
mkdirSync(dir, { recursive: true }); | ||
const config = join(dir, 'mcp.json'); | ||
writeFileSync( | ||
config, | ||
JSON.stringify({ | ||
mcpServers: { | ||
knotless: { | ||
type: 'stdio', | ||
command: join(root, 'target/debug/knotless-mcp'), | ||
args: [], | ||
env: environment(world, world.dir, who) | ||
} | ||
} | ||
}) | ||
); | ||
const args = [ | ||
'-p', | ||
ask, | ||
'--mcp-config', | ||
config, | ||
'--strict-mcp-config', | ||
'--tools', | ||
'', | ||
'--allowedTools', | ||
'mcp__knotless', | ||
'--output-format', | ||
'stream-json', | ||
'--verbose', | ||
'--no-session-persistence', | ||
'--setting-sources', | ||
'' | ||
]; | ||
const said = []; | ||
const child = spawn('claude', args, { cwd: dir, stdio: ['ignore', 'pipe', 'inherit'] }); | ||
let buf = ''; | ||
child.stdout.on('data', (d) => { | ||
buf += d; | ||
for (let i = buf.indexOf('\n'); i >= 0; i = buf.indexOf('\n')) { | ||
const line = buf.slice(0, i); | ||
buf = buf.slice(i + 1); | ||
if (!line.trim()) continue; | ||
const m = JSON.parse(line); | ||
if (m.type !== 'assistant') continue; | ||
for (const c of m.message.content ?? []) { | ||
if (c.type === 'text' && c.text.trim()) said.push({ kind: 'text', text: c.text.trim() }); | ||
if (c.type === 'tool_use') | ||
said.push({ kind: 'tool', text: c.name.replace(/^mcp__knotless__/, '') }); | ||
} | ||
} | ||
}); | ||
const code = await new Promise((r) => child.on('close', (c) => r(c ?? 1))); | ||
if (code !== 0) throw new Error(`claude exited ${code}`); | ||
return said; | ||
} | ||
| ||
export default { | ||
as: 'alice', | ||
start: () => `/alice.test/${REPO}`, | ||
prepare: async ({ world }) => { | ||
const said = await claude(world, 'alice', ASK); | ||
mkdirSync(join(here, 'public/agent'), { recursive: true }); | ||
writeFileSync( | ||
join(here, 'public/agent/agent.json'), | ||
JSON.stringify({ ask: ASK, said }, null, 1) | ||
); | ||
console.log(` agent: ${said.length} steps`); | ||
}, | ||
film: async ({ page, world, size, take }) => { | ||
const shot = await film(take, page, size); | ||
try { | ||
await shot.open(`${world.base}/alice.test/${REPO}/pulls`); | ||
await shot.hold(900); | ||
shot.mark('pulls'); | ||
await shot.click(page.locator('a[href*="/pull/"]').first()); | ||
await page.waitForURL(/\/pull\//); | ||
shot.mark('pull'); | ||
await shot.hold(2200); | ||
} finally { | ||
await shot.cut(); | ||
} | ||
} | ||
}; | ||
marketing/video/shoot/scenes/checks.mjsnew+39−0
| @@ -0,0 +1,39 @@ | ||
// The pull's checks, as alice's runner reports them while she watches: each | ||
// running, then passing, the page following as each lands. Reported here | ||
// as a runner would, since the world runs no runner. | ||
| ||
import { check } from '../act.mjs'; | ||
import { film } from '../camera.mjs'; | ||
import { REPO } from './push.mjs'; | ||
| ||
const WORKFLOWS = ['build', 'test', 'lint']; | ||
| ||
export default { | ||
as: 'alice', | ||
start: (world, cast) => cast.pull, | ||
film: async ({ page, world, size, take, cast }) => { | ||
const shot = await film(take, page, size); | ||
const report = (workflow, status, started) => | ||
check(world, 'alice', REPO, 'celsius', cast.tip, workflow, status, started); | ||
try { | ||
await shot.open(world.base + cast.pull); | ||
await shot.hold(800); | ||
const started = new Date().toISOString(); | ||
for (const w of WORKFLOWS) await report(w, 'running', started); | ||
const box = page.getByText(/still running/i).first(); | ||
await box.waitFor({ timeout: 20_000 }); | ||
await shot.reveal(box); | ||
shot.mark('running'); | ||
await shot.hold(1500); | ||
for (const w of WORKFLOWS) { | ||
await report(w, 'success', started); | ||
await shot.hold(900); | ||
} | ||
await page.getByText('All checks have passed').waitFor({ timeout: 20_000 }); | ||
shot.mark('passed'); | ||
await shot.hold(1800); | ||
} finally { | ||
await shot.cut(); | ||
} | ||
} | ||
}; | ||
marketing/video/shoot/scenes/login.mjsnew+45−0
| @@ -0,0 +1,45 @@ | ||
// alice comes to her new repository and logs in with the account she | ||
// already has: her handle, then her own PDS's page to sign in and approve. | ||
| ||
import { film, ready } from '../camera.mjs'; | ||
import { REPO } from './push.mjs'; | ||
| ||
export default { | ||
as: 'alice', | ||
login: false, | ||
start: () => `/alice.test/${REPO}`, | ||
film: async ({ page, world, size, take }) => { | ||
const alice = world.accounts.find((a) => a.handle === 'alice.test'); | ||
const shot = await film(take, page, size); | ||
try { | ||
await shot.open(`${world.base}/alice.test/${REPO}`); | ||
await shot.hold(1200); | ||
shot.mark('repo'); | ||
await shot.click(page.getByRole('button', { name: 'Log in' })); | ||
const dialog = page.getByRole('dialog', { name: 'Log in with your Atmosphere account' }); | ||
await shot.type(dialog.getByLabel('Handle'), alice.handle, { delay: 70 }); | ||
shot.mark('asked'); | ||
await shot.click(dialog.getByRole('button', { name: 'Log in' })); | ||
await page.waitForURL((u) => u.origin === new URL(world.pds).origin); | ||
await page.getByRole('textbox', { name: 'Password' }).waitFor(); | ||
shot.mark('pds'); | ||
await shot.hold(500); | ||
await shot.type(page.getByRole('textbox', { name: 'Password' }), alice.password, { | ||
delay: 45 | ||
}); | ||
await shot.click(page.getByRole('button', { name: 'Sign in' })); | ||
const yes = page.getByRole('button', { name: /^(accept|authorize|allow)$/i }); | ||
await yes.waitFor(); | ||
await shot.hold(500); | ||
shot.mark('approve'); | ||
await shot.click(yes); | ||
await page.waitForURL((u) => u.pathname === `/alice.test/${REPO}`); | ||
await ready(page); | ||
await page.getByLabel('Your account').waitFor(); | ||
shot.mark('in'); | ||
await shot.hold(1500); | ||
} finally { | ||
await shot.cut(); | ||
} | ||
} | ||
}; | ||
marketing/video/shoot/scenes/merge.mjsnew+27−0
| @@ -0,0 +1,27 @@ | ||
// alice merges her approved pull, its checks passed, from its page. | ||
| ||
import { film } from '../camera.mjs'; | ||
| ||
export default { | ||
as: 'alice', | ||
start: (world, cast) => cast.pull, | ||
film: async ({ page, world, size, take, cast }) => { | ||
const shot = await film(take, page, size); | ||
try { | ||
await shot.open(world.base + cast.pull); | ||
await shot.hold(700); | ||
const box = page.locator('.merge-box'); | ||
await box.scrollIntoViewIfNeeded(); | ||
await shot.hold(900); | ||
shot.mark('merge'); | ||
await shot.click(box.getByRole('button', { name: 'Merge pull' })); | ||
const merged = page.locator('.event', { hasText: 'Merged as' }); | ||
await merged.waitFor({ timeout: 30_000 }); | ||
await merged.scrollIntoViewIfNeeded(); | ||
shot.mark('merged'); | ||
await shot.hold(2000); | ||
} finally { | ||
await shot.cut(); | ||
} | ||
} | ||
}; | ||
marketing/video/shoot/scenes/pull.mjsnew+38−0
| @@ -0,0 +1,38 @@ | ||
// alice opens a pull of her celsius branch from the repository's pages. | ||
| ||
import { film } from '../camera.mjs'; | ||
import { REPO } from './push.mjs'; | ||
| ||
export default { | ||
as: 'alice', | ||
start: () => `/alice.test/${REPO}`, | ||
film: async ({ page, world, size, take, cast }) => { | ||
const shot = await film(take, page, size); | ||
try { | ||
await shot.open(`${world.base}/alice.test/${REPO}/pulls/new`); | ||
await shot.hold(900); | ||
shot.mark('new'); | ||
const branch = page.getByLabel('Branch'); | ||
await shot.click(branch); | ||
await branch.selectOption('celsius'); | ||
const ready = page.locator('.compare-ready'); | ||
await ready.waitFor(); | ||
shot.mark('chose'); | ||
await shot.hold(900); | ||
await shot.click(ready.getByRole('button', { name: 'Open pull' })); | ||
const title = page.getByRole('textbox', { name: 'Title' }); | ||
await title.fill(''); | ||
await shot.type(title, 'Show temperatures in Celsius', { delay: 50 }); | ||
await shot.hold(300); | ||
shot.mark('open'); | ||
await shot.click(page.getByRole('button', { name: 'Open pull', exact: true })); | ||
await page.waitForURL(/\/pull\//); | ||
await page.getByRole('heading', { name: 'Show temperatures in Celsius' }).waitFor(); | ||
shot.mark('opened'); | ||
await shot.hold(1800); | ||
cast.pull = new URL(page.url()).pathname; | ||
} finally { | ||
await shot.cut(); | ||
} | ||
} | ||
}; | ||
marketing/video/shoot/scenes/push.mjsnew+94−0
| @@ -0,0 +1,94 @@ | ||
// alice makes a repository and pushes to it with plain git, from her own | ||
// terminal: the commands run for real, and what they print is what the | ||
// video's terminal shows. Off camera, a branch for the next scenes' pull. | ||
| ||
import { mkdirSync, writeFileSync } from 'node:fs'; | ||
import { join } from 'node:path'; | ||
import { run, terminal } from '../act.mjs'; | ||
| ||
export const REPO = 'forecast'; | ||
| ||
const README = `# forecast | ||
| ||
The weather tomorrow, in your terminal. | ||
| ||
\`\`\` | ||
$ forecast lisbon | ||
lisbon tomorrow: high 71°F, low 55°F | ||
\`\`\` | ||
`; | ||
| ||
const MAIN = `use std::env; | ||
| ||
fn main() { | ||
let city = env::args().nth(1).unwrap_or_else(|| "lisbon".into()); | ||
let (high, low) = forecast(&city); | ||
println!("{city} tomorrow: high {high}°F, low {low}°F"); | ||
} | ||
| ||
fn forecast(city: &str) -> (i32, i32) { | ||
let seed = city.bytes().map(i32::from).sum::<i32>(); | ||
(60 + seed % 25, 45 + seed % 15) | ||
} | ||
`; | ||
| ||
const CELSIUS = `use std::env; | ||
| ||
fn main() { | ||
let args: Vec<String> = env::args().skip(1).collect(); | ||
let celsius = args.iter().any(|a| a == "--celsius"); | ||
let city = args | ||
.iter() | ||
.find(|a| !a.starts_with("--")) | ||
.cloned() | ||
.unwrap_or_else(|| "lisbon".into()); | ||
let (high, low) = forecast(&city); | ||
if celsius { | ||
println!("{city} tomorrow: high {}°C, low {}°C", to_celsius(high), to_celsius(low)); | ||
} else { | ||
println!("{city} tomorrow: high {high}°F, low {low}°F"); | ||
} | ||
} | ||
| ||
fn to_celsius(f: i32) -> i32 { | ||
(f - 32) * 5 / 9 | ||
} | ||
| ||
fn forecast(city: &str) -> (i32, i32) { | ||
let seed = city.bytes().map(i32::from).sum::<i32>(); | ||
(60 + seed % 25, 45 + seed % 15) | ||
} | ||
`; | ||
| ||
export default { | ||
prepare: async ({ world, cast }) => { | ||
const dir = join(world.dir, 'alice', REPO); | ||
mkdirSync(join(dir, 'src'), { recursive: true }); | ||
writeFileSync(join(dir, 'README.md'), README); | ||
writeFileSync(join(dir, 'src/main.rs'), MAIN); | ||
const git = (...args) => run(world, 'alice', dir, 'git', args); | ||
git('init', '-q', '-b', 'main'); | ||
git('add', '-A'); | ||
git('commit', '-q', '-m', 'Tomorrow, in your terminal'); | ||
const url = `knotless://alice.test/${REPO}`; | ||
const shell = terminal(world, 'alice', dir); | ||
shell.run('knotless', [ | ||
'repo', | ||
'create', | ||
url, | ||
'--description', | ||
'The weather tomorrow, in your terminal' | ||
]); | ||
git('remote', 'add', 'origin', url); | ||
shell.run('git', ['push', '-u', 'origin', 'main']); | ||
shell.save('push'); | ||
// Her runner reports its checks; the branch the pull brings in. | ||
run(world, 'alice', dir, 'knotless', ['runner', 'add', url, 'alice.test']); | ||
git('switch', '-q', '-c', 'celsius'); | ||
writeFileSync(join(dir, 'src/main.rs'), CELSIUS); | ||
git('commit', '-q', '-am', 'Show temperatures in Celsius with --celsius'); | ||
git('push', '-q', 'origin', 'celsius'); | ||
cast.dir = dir; | ||
cast.tip = git('rev-parse', 'HEAD'); | ||
} | ||
}; | ||
marketing/video/shoot/scenes/review.mjs+20−31
| @@ -1,58 +1,47 @@ | ||
// alice reviews bob's pull into tally: a comment on a line it adds, the | ||
// review approved, then the pull merged from its page, squashed into one | ||
// commit on her protected main. | ||
// bob reviews alice's pull: a comment on the line that reads --celsius, | ||
// then the review approved. | ||
| ||
import { film } from '../camera.mjs'; | ||
| ||
export default { | ||
as: 'alice', | ||
start: (world) => `${world.views['pull-open']}/files`, | ||
film: async ({ page, world, size, take }) => { | ||
const pull = world.base + world.views['pull-open']; | ||
as: 'bob', | ||
start: (world, cast) => `${cast.pull}/files`, | ||
film: async ({ page, world, size, take, cast }) => { | ||
const shot = await film(take, page, size); | ||
try { | ||
await shot.open(`${pull}/files`); | ||
await shot.open(`${world.base}${cast.pull}/files`); | ||
await shot.hold(900); | ||
shot.mark('files'); | ||
// The line the pull adds that reads `--json`, with the button that comments on it. | ||
const row = page | ||
.locator('tr', { has: page.locator('button[aria-label^="Comment on line"]') }) | ||
.filter({ hasText: '"--json"' }) | ||
.filter({ hasText: '"--celsius"' }) | ||
.first(); | ||
shot.mark('line'); | ||
await shot.point(row); | ||
await shot.hold(300); | ||
await shot.click(row.locator('button[aria-label^="Comment on line"]')); | ||
const box = page.getByRole('textbox', { name: /^Comment on line/ }); | ||
await shot.type(box, 'Could this say `--format json`, to leave room for others?', { | ||
delay: 40 | ||
}); | ||
await shot.hold(300); | ||
await shot.type( | ||
page.getByRole('textbox', { name: /^Comment on line/ }), | ||
'Could -c work too?', | ||
{ delay: 55 } | ||
); | ||
await shot.hold(250); | ||
await shot.click(page.getByRole('button', { name: 'Start a review' })); | ||
await page.locator('.pending-line').waitFor(); | ||
shot.mark('commented'); | ||
await shot.hold(700); | ||
await shot.hold(600); | ||
await shot.click(page.locator('.review-menu > summary')); | ||
shot.mark('finish'); | ||
const menu = page.locator('.review-menu .menu'); | ||
await shot.type(menu.getByPlaceholder('Leave a comment'), 'Looks good with that.', { | ||
delay: 45 | ||
await shot.type(menu.getByPlaceholder('Leave a comment'), 'Nice one, ship it.', { | ||
delay: 50 | ||
}); | ||
await shot.click(menu.getByText('Approve', { exact: true })); | ||
await shot.hold(250); | ||
await shot.hold(200); | ||
await shot.click(menu.getByRole('button', { name: 'Approve', exact: true })); | ||
await page.locator('.review-menu .menu').waitFor({ state: 'hidden' }); | ||
await menu.waitFor({ state: 'hidden' }); | ||
shot.mark('approved'); | ||
await shot.hold(900); | ||
await shot.open(pull); | ||
await shot.hold(700); | ||
const box2 = page.locator('.merge-box'); | ||
await box2.scrollIntoViewIfNeeded(); | ||
shot.mark('merge'); | ||
await shot.click(box2.getByRole('button', { name: 'Merge pull' })); | ||
await page.locator('.event', { hasText: 'Merged as' }).waitFor({ timeout: 30_000 }); | ||
await page.locator('.event', { hasText: 'Merged as' }).scrollIntoViewIfNeeded(); | ||
shot.mark('merged'); | ||
await shot.hold(2200); | ||
await shot.hold(1200); | ||
} finally { | ||
await shot.cut(); | ||
} | ||
marketing/video/shoot/scenes/star.mjsdeleted+0−22
| @@ -1,22 +0,0 @@ | ||
// bob finds alice's notes and stars it: the count goes up as he clicks. | ||
| ||
import { film } from '../camera.mjs'; | ||
| ||
export default { | ||
as: 'bob', | ||
start: () => '/alice.test/notes', | ||
film: async ({ page, world, size, take }) => { | ||
const shot = await film(take, page, size); | ||
try { | ||
await shot.open(`${world.base}/alice.test/notes`); | ||
await shot.hold(1000); | ||
shot.mark('star'); | ||
await shot.click(page.getByRole('button', { name: 'Star', exact: true })); | ||
await page.getByRole('button', { name: 'Starred' }).waitFor(); | ||
shot.mark('starred'); | ||
await shot.hold(1800); | ||
} finally { | ||
await shot.cut(); | ||
} | ||
} | ||
}; | ||
marketing/video/shoot/act.mjsnew+101−0
marketing/video/shoot/camera.mjs+28−10
marketing/video/shoot/shoot.mjs+25−21
marketing/video/video/change/Checks.tsxdeleted+0−153
marketing/video/video/change/Deploy.tsxdeleted+0−100
marketing/video/video/change/Diff.tsxdeleted+0−158
marketing/video/video/change/Land.tsxdeleted+0−90
marketing/video/video/change/Review.tsxdeleted+0−93
marketing/video/video/change/Terminal.tsxdeleted+0−72
marketing/video/video/Agent.tsxnew+133−0
marketing/video/video/Change.tsxdeleted+0−329
marketing/video/video/Feature.tsxdeleted+0−166
marketing/video/video/Footage.tsx+60−32
marketing/video/video/kit.tsx+12−164
marketing/video/video/look.ts+0−5
marketing/video/video/Promo.tsxnew+323−0
marketing/video/video/Root.tsx+7−24
marketing/video/video/Sounds.tsxnew+39−0
marketing/video/video/story.tsdeleted+0−65
marketing/video/video/Terminal.tsxnew+89−0
marketing/video/.DS_Storenew+0−0
A binary file, not shown.