diff --git a/site/fonts/OFL-big-shoulders-display.txt b/site/fonts/OFL-big-shoulders-display.txt new file mode 100644 index 00000000..b5a4e0df --- /dev/null +++ b/site/fonts/OFL-big-shoulders-display.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Big Shoulders Project Authors (https://github.com/xotypeco/big_shoulders) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/fonts/OFL-jetbrains-mono.txt b/site/fonts/OFL-jetbrains-mono.txt new file mode 100644 index 00000000..821a3dac --- /dev/null +++ b/site/fonts/OFL-jetbrains-mono.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/fonts/big-shoulders-display-800-latin.woff2 b/site/fonts/big-shoulders-display-800-latin.woff2 new file mode 100644 index 00000000..47d2fe7d Binary files /dev/null and b/site/fonts/big-shoulders-display-800-latin.woff2 differ diff --git a/site/fonts/fonts.css b/site/fonts/fonts.css new file mode 100644 index 00000000..98862a07 --- /dev/null +++ b/site/fonts/fonts.css @@ -0,0 +1,17 @@ +/* The site's faces, served from thirdshift.app rather than Google Fonts, for pages that must + request nothing from Google. Latin subsets as Google Fonts serves them; both are under the + SIL Open Font License 1.1 (the OFL-*.txt files beside them). JetBrains Mono is variable. */ +@font-face { + font-family: "Big Shoulders Display"; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url("/fonts/big-shoulders-display-800-latin.woff2") format("woff2"); +} +@font-face { + font-family: "JetBrains Mono"; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url("/fonts/jetbrains-mono-latin.woff2") format("woff2"); +} diff --git a/site/fonts/jetbrains-mono-latin.woff2 b/site/fonts/jetbrains-mono-latin.woff2 new file mode 100644 index 00000000..4d09cda4 Binary files /dev/null and b/site/fonts/jetbrains-mono-latin.woff2 differ diff --git a/site/how-it-works/index.html b/site/how-it-works/index.html index 39cb847b..6e892934 100644 --- a/site/how-it-works/index.html +++ b/site/how-it-works/index.html @@ -31,7 +31,7 @@
- +

An engineer's guide · from issue to pull request

How thirdshift works

You describe the work. thirdshift runs an unattended coding agent in its own Git checkout, asks it to implement and review the change, then checks the pull request against the repository's latest state. By default, you review before merging. A Merge run merges it when asked.

@@ -193,7 +193,7 @@

How thirdshift works

Eligible work ↓ Run or Spec run ↓ reviewed pull request

Pickup respects issue readiness, open blockers and the Claim limit. Architect and Security runs yield to ready human-shaped work. Gates can skip a pass without failure, and unresolved plans, ideas or findings can leave the next decision with the Day shift. This is scheduled CLI work on your machine, using your authenticated tools.

-

Next: inspect every prompt and skill, see a real Run's recorded output, or use the manual for installation, commands and configuration.

+

Next: inspect every prompt and skill, see a real Run's recorded output, use the manual for installation, commands and configuration, or watch the explainer videos.

@@ -201,6 +201,7 @@

How thirdshift works

diff --git a/site/index.html b/site/index.html index 36d960fc..a5938383 100644 --- a/site/index.html +++ b/site/index.html @@ -63,7 +63,7 @@

Your issues go to press overnight.

The humans are the day shift: you talk the idea through, write the spec and cut it into tickets. Coding agents work the third shift: give thirdshift a GitHub issue URL and it implements the issue, reviews its own work and delivers a pull request ready for review, or merges it when asked. Start a Run whenever you like, or let your operating system start passes on a schedule.

-

How thirdshift works: an engineer's guide from issue to pull request, through Worktrees, agent review and Delivery. Documentation covers installation, commands and configuration.

+

How thirdshift works: an engineer's guide from issue to pull request, through Worktrees, agent review and Delivery. Documentation covers installation, commands and configuration. Videos explain it in under four minutes.

Built on Matt Pocock's skills: aihero.dev/skills · mattpocock/skills. Read every prompt and skill thirdshift sends.

@@ -527,6 +527,7 @@

Numbered Issu How thirdshift works Documentation Prompts and skills + Videos MIT License Factory skills adapted from Matt Pocock's skills diff --git a/site/main.js b/site/main.js index d039bbff..93847b4b 100644 --- a/site/main.js +++ b/site/main.js @@ -42,6 +42,35 @@ for (const button of document.querySelectorAll(".install button.copy")) { }); } +// The videos page: each thumbnail links to the video on YouTube, so it works without JavaScript. +// Playing one swaps in YouTube's no-cookie player, so nothing is requested from YouTube or Google +// until the reader asks for a video. A modified click still opens the link as usual. +for (const link of document.querySelectorAll("a.video-play")) { + link.addEventListener("click", event => { + if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + event.preventDefault(); + playVideo(link); + }); + // Space activates buttons, not links; a reader can't tell this link from a play button. + link.addEventListener("keydown", event => { + if (event.key !== " ") return; + event.preventDefault(); + playVideo(link); + }); +} + +function playVideo(link) { + const id = new URL(link.href).pathname.slice(1); + const player = Object.assign(document.createElement("iframe"), { + src: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}?autoplay=1`, + title: link.closest(".video").querySelector("h2").textContent, + allow: "autoplay; encrypted-media; picture-in-picture; fullscreen", + allowFullscreen: true, + }); + link.replaceWith(player); + player.focus(); +} + // The Prompts and skills page's commit label: the commit of main the deploy // published, from the commit.txt it writes. Without one (e.g. served locally), // the label keeps its generic text. diff --git a/site/sitemap.xml b/site/sitemap.xml index d8d1f997..84efab0e 100644 --- a/site/sitemap.xml +++ b/site/sitemap.xml @@ -4,4 +4,5 @@ https://thirdshift.app/docs/ https://thirdshift.app/how-it-works/ https://thirdshift.app/prompts/ + https://thirdshift.app/videos/ diff --git a/site/tests/pages.test.cjs b/site/tests/pages.test.cjs index 19beb179..16395422 100644 --- a/site/tests/pages.test.cjs +++ b/site/tests/pages.test.cjs @@ -5,7 +5,7 @@ const { readFile } = require("node:fs/promises"); const { join } = require("node:path"); const { chromium } = require("playwright"); -const publicPaths = ["/", "/docs/", "/how-it-works/", "/prompts/"]; +const publicPaths = ["/", "/docs/", "/how-it-works/", "/prompts/", "/videos/"]; let server, browser, origin; before(async () => { @@ -14,7 +14,8 @@ before(async () => { const file = path.endsWith("/") ? `${path.slice(1)}index.html` : path.slice(1); try { const content = await readFile(join(__dirname, "..", file)); - const type = file.endsWith(".js") ? "text/javascript" : file.endsWith(".css") ? "text/css" : "text/html"; + const types = { js: "text/javascript", css: "text/css", jpg: "image/jpeg", woff2: "font/woff2" }; + const type = types[file.split(".").pop()] ?? "text/html"; response.writeHead(200, { "Content-Type": type }).end(content); } catch { response.writeHead(404).end(); @@ -37,6 +38,14 @@ async function pageForTest(t, options) { return page; } +// Nothing in the selected containers, or the page, scrolls sideways. +async function assertFits(page, selector, message) { + const overflow = await page.locator(selector).evaluateAll(nodes => + nodes.filter(node => node.scrollWidth > node.clientWidth + 1).map(node => node.textContent.trim().slice(0, 80))); + assert.deepEqual(overflow, [], message); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, "no horizontal page scrolling"); +} + async function checkSectionToggles(page, contentSelector, closeKey) { for (const heading of await page.locator("main h2").all()) { assert.equal(await heading.isVisible(), true); @@ -114,8 +123,8 @@ test("every public page identifies its canonical URL, purpose and share metadata assert.equal(await page.locator("h1").count(), 1, path); assert.ok((await page.locator("body").textContent()).includes("GitHub"), path); } - assert.equal(titles.size, 4, "page titles distinguish the four search results"); - assert.equal(descriptions.size, 4, "each page has its own search description"); + assert.equal(titles.size, publicPaths.length, "page titles distinguish the search results"); + assert.equal(descriptions.size, publicPaths.length, "each page has its own search description"); await page.goto(`${origin}/`); assert.match(await page.title(), /GitHub issues.*pull requests/); }); @@ -250,10 +259,7 @@ for (const width of [320, 390, 768, 1280]) { assert.equal(await diagram.isVisible(), true); assert.ok(await diagram.locator("figcaption").textContent()); } - const overflow = await page.locator("main, header, .guide-diagram, .guide-node, .guide-flow > li, pre").evaluateAll(nodes => - nodes.filter(node => node.scrollWidth > node.clientWidth + 1).map(node => node.textContent.trim().slice(0, 80))); - assert.deepEqual(overflow, [], "text and diagrams fit their containers"); - assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, "no horizontal page scrolling"); + await assertFits(page, "main, header, .guide-diagram, .guide-node, .guide-flow > li, pre", "text and diagrams fit their containers"); }); } @@ -282,6 +288,121 @@ for (const javaScriptEnabled of [false, true]) { }); } +// The videos page's entries, in order. +const videos = [ + { title: "thirdshift in 31 seconds", audience: "Anyone", length: "0:32", id: "eRB6gGOQdJA" }, + { title: "thirdshift, briefly", audience: "Engineering leaders", length: "2:02", id: "yynbSGpHXGc" }, + { title: "How thirdshift works", audience: "Engineers", length: "2:38", id: "oL-5pi1AFC4" }, + { title: "How thirdshift works, step by step", audience: "Engineers, first look", length: "3:22", id: "gehONSA0viQ" }, + { title: "How to use thirdshift", audience: "New users", length: "3:21", id: "pNhNyW9JgqA" }, +]; +const playerUrl = id => `https://www.youtube-nocookie.com/embed/${id}?autoplay=1`; + +// A stand-in for YouTube's player, so tests that play a video never leave the machine. +async function stubPlayer(page) { + await page.route("https://www.youtube-nocookie.com/embed/**", route => + route.fulfill({ contentType: "text/html", body: "player" })); +} + +test("the homepage and the guide link to the videos page without JavaScript", async (t) => { + const page = await pageForTest(t, { javaScriptEnabled: false }); + for (const path of ["/", "/how-it-works/"]) { + await page.goto(`${origin}${path}`); + await page.getByRole("link", { name: "Videos", exact: true }).first().click(); + assert.equal(new URL(page.url()).pathname, "/videos/", path); + } +}); + +test("the videos page lists every video in order, each a thumbnail linking to YouTube without JavaScript", async (t) => { + const page = await pageForTest(t, { javaScriptEnabled: false }); + await page.goto(`${origin}/videos/`); + assert.deepEqual(await page.locator("main .video").evaluateAll(nodes => nodes.map(node => [ + node.querySelector("h2").textContent, + node.querySelector(".video-audience").textContent, + node.querySelector(".video-length").textContent, + node.querySelector("a.video-play").getAttribute("href"), + ])), videos.map(({ title, audience, length, id }) => [title, audience, length, `https://youtu.be/${id}`])); + for (const summary of await page.locator("main .video-text > p:last-child").allTextContents()) { + assert.equal(summary.match(/[.!?](\s|$)/g)?.length, 1, `one sentence on what the video covers: ${summary}`); + } + for (const [index, { title, id }] of videos.entries()) { + const thumbnail = page.locator("main .video").nth(index).locator("a.video-play img"); + assert.equal(await thumbnail.getAttribute("src"), `/videos/${id}.jpg`); + assert.equal(await thumbnail.getAttribute("width"), "1280"); + assert.equal(await thumbnail.getAttribute("height"), "720"); + assert.ok((await thumbnail.getAttribute("alt")).startsWith(`Title card: ${title}.`), title); + const response = await page.request.get(`${origin}/videos/${id}.jpg`); + assert.equal(response.status(), 200, id); + assert.equal(response.headers()["content-type"], "image/jpeg"); + } + await page.route("https://youtu.be/**", route => route.fulfill({ contentType: "text/html", body: "YouTube" })); + await page.locator("a.video-play").first().click(); + assert.equal(page.url(), `https://youtu.be/${videos[0].id}`); +}); + +test("the videos page requests nothing from YouTube or Google until a video plays, then embeds only that video", async (t) => { + const page = await pageForTest(t); + await stubPlayer(page); + const external = []; + const failed = []; + page.on("request", request => { if (!request.url().startsWith(`${origin}/`)) external.push(request.url()); }); + page.on("response", response => { if (response.url().startsWith(`${origin}/`) && response.status() !== 200) failed.push(response.url()); }); + await page.goto(`${origin}/videos/`, { waitUntil: "networkidle" }); + // Bring the lazily loaded thumbnails in too. + await page.locator("main .video").last().scrollIntoViewIfNeeded(); + await page.waitForLoadState("networkidle"); + assert.equal(await page.locator("main .video img").evaluateAll(nodes => nodes.every(node => node.complete && node.naturalWidth > 0)), true); + assert.deepEqual(external, [], "fonts, thumbnails and scripts are all served by thirdshift.app"); + assert.deepEqual(failed, []); + assert.equal(await page.locator("iframe").count(), 0); + + const { title, id } = videos[2]; + const video = page.locator("main .video").nth(2); + await video.locator("a.video-play").click(); + const player = video.locator("iframe"); + await player.waitFor(); + await page.waitForLoadState("networkidle"); + assert.equal(await page.locator("iframe").count(), 1); + assert.equal(await video.locator("a.video-play").count(), 0, "the player replaces the thumbnail in place"); + assert.equal(await player.getAttribute("src"), playerUrl(id)); + assert.equal(await player.getAttribute("title"), title); + assert.deepEqual(external, [playerUrl(id)]); + const box = await player.boundingBox(); + assert.ok(Math.abs(box.width / box.height - 16 / 9) < 0.02, `16:9, not ${box.width}x${box.height}`); +}); + +test("each video plays from the keyboard, with a visible focus style", async (t) => { + const page = await pageForTest(t); + await stubPlayer(page); + await page.goto(`${origin}/videos/`); + const focusedPlay = () => page.evaluate(() => document.activeElement.matches("a.video-play")); + for (let presses = 0; presses < 20 && !await focusedPlay(); presses++) await page.keyboard.press("Tab"); + assert.equal(await focusedPlay(), true, "Tab reaches the first video"); + assert.notEqual(await page.evaluate(() => getComputedStyle(document.activeElement).outlineStyle), "none"); + await page.keyboard.press("Enter"); + // Space, which scrolls the page from a link, plays the next one instead. + await page.locator("a.video-play").first().focus(); + const scrollY = await page.evaluate(() => scrollY); + await page.keyboard.press("Space"); + assert.equal(await page.evaluate(() => scrollY), scrollY); + const players = page.locator("main .video iframe"); + assert.deepEqual(await players.evaluateAll(nodes => nodes.map(node => [node.title, node.src])), + videos.slice(0, 2).map(({ title, id }) => [title, playerUrl(id)])); + assert.equal(await page.evaluate(() => document.activeElement.title), videos[1].title, "focus moves to the player"); +}); + +for (const width of [320, 390, 1280]) { + for (const javaScriptEnabled of [false, true]) { + test(`the videos page fits ${width}px without horizontal scrolling (JavaScript: ${javaScriptEnabled})`, async (t) => { + const page = await pageForTest(t, { javaScriptEnabled, viewport: { width, height: 900 } }); + await stubPlayer(page); + await page.goto(`${origin}/videos/`); + if (javaScriptEnabled) await page.locator("a.video-play").first().click(); + await assertFits(page, "main, header, .video, .video-frame", "text and videos fit their containers"); + }); + } +} + // A skill's own SKILL.md starts open, so it shows as soon as its skill does. const skillMdIds = async page => new Set(await page.locator("main h4").evaluateAll(nodes => nodes.filter(node => /^[^/]+\/SKILL\.md$/.test(node.textContent)).map(node => node.id))); diff --git a/site/videos/eRB6gGOQdJA.jpg b/site/videos/eRB6gGOQdJA.jpg new file mode 100644 index 00000000..57aeb1d9 Binary files /dev/null and b/site/videos/eRB6gGOQdJA.jpg differ diff --git a/site/videos/gehONSA0viQ.jpg b/site/videos/gehONSA0viQ.jpg new file mode 100644 index 00000000..895afa14 Binary files /dev/null and b/site/videos/gehONSA0viQ.jpg differ diff --git a/site/videos/index.html b/site/videos/index.html new file mode 100644 index 00000000..59fe9c44 --- /dev/null +++ b/site/videos/index.html @@ -0,0 +1,105 @@ + + + + + +Videos · thirdshift + + + + + + + + + + + + + + + + + + + + + + + +Skip to the videos +
+
+
+ +

Explainer videos · from half a minute to under four

+

thirdshift on video

+

Five short videos, each for a different reader: a first glance, an overview for engineering leaders, two walkthroughs of a Run for engineers, and a getting-started tutorial. Pick the one for you.

+

A video loads from YouTube only when you play it. Every video has captions; turn them on with the player's CC button.

+
+
+
+ +
+
    +
  1. +
    +

    Anyone ·

    +

    thirdshift in 31 seconds

    +

    Give thirdshift a GitHub issue and get back a pull request ready for your review, shown with a real overnight Run in which issue #663 became merged pull request #665.

    +
    +
    + Title card: thirdshift in 31 seconds. Issue in. Reviewed pull request out. Beside it, a terminal from an overnight Run on 12 October 2026 takes issue #663 and reports pull request #665 merged.Play +
    +
  2. +
  3. +
    +

    Engineering leaders ·

    +

    thirdshift, briefly

    +

    A two-minute overview of thirdshift as a deterministic program wrapped around AI agent sessions, with a real overnight Run, what happens when a Run fails, and the one limit to know: sessions run as you, not in a sandbox.

    +
    +
    + Title card: thirdshift, briefly. For engineering leaders, in two minutes. Beside it, a slide lists what helps get it right: test-first, two independent reviews, reported bugs reproduced then fixed or set aside, and unfixed findings listed in the pull request with a reason.Play +
    +
  4. +
  5. +
    +

    Engineers ·

    +

    How thirdshift works

    +

    thirdshift claims the issue, gives the Run its own Git Worktree and starts an agent session that implements the change, gets two independent reviews and opens a pull request, then its deterministic code merges in the Base branch, watches CI and starts a Repair when needed.

    +
    +
    + Title card: How thirdshift works. From GitHub issue to reviewed pull request. Beside it, the Delivery loop: merge in the Base branch, push and watch CI, start a Repair session on a conflict or CI failure, and exit with a checked pull request.Play +
    +
  6. +
  7. +
    +

    Engineers, first look ·

    +

    How thirdshift works, step by step

    +

    Key terms first: who's involved, the one rule (the agent writes the code and opens the pull request; everything else that touches GitHub is thirdshift's deterministic code), and the six steps of a Run, laid out by who does each one.

    +
    +
    + Title card: How thirdshift works, step by step. Who does what in a Run. Beside it, a table of a Run's six steps, split between thirdshift's deterministic code, the AI agent and GitHub.Play +
    +
  8. +
  9. +
    +

    New users ·

    +

    How to use thirdshift

    +

    Getting started: install, setup, your first Run, watching it, when a Run fails, merging and running on a schedule, where every terminal line shown is real thirdshift output.

    +
    +
    + Title card: How to use thirdshift. Install, first Run, merge, schedule. Beside it, a terminal shows a Run's Delivery step: merging origin/main, pushing and waiting for CI until it passes.Play +
    +
  10. +
+
+ + + diff --git a/site/videos/oL-5pi1AFC4.jpg b/site/videos/oL-5pi1AFC4.jpg new file mode 100644 index 00000000..c1bafea3 Binary files /dev/null and b/site/videos/oL-5pi1AFC4.jpg differ diff --git a/site/videos/pNhNyW9JgqA.jpg b/site/videos/pNhNyW9JgqA.jpg new file mode 100644 index 00000000..dc1b2dcc Binary files /dev/null and b/site/videos/pNhNyW9JgqA.jpg differ diff --git a/site/videos/videos.css b/site/videos/videos.css new file mode 100644 index 00000000..3d37a56a --- /dev/null +++ b/site/videos/videos.css @@ -0,0 +1,19 @@ +/* The videos page: each video's text beside its player, stacking on narrow screens. */ +.video-list { list-style: none; margin: 0; padding: 0 0 72px; display: grid; gap: 64px; } +.video { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 20px 40px; align-items: center; } +.video h2 { font-size: clamp(34px, 4.5vw, 54px); margin-block: 14px; } +.video-text > p:last-child { margin: 0; max-width: 62ch; color: var(--dim); } +.video-text .eyebrow { color: var(--c); } +/* The thumbnail and the player it becomes share one 16:9 box, so nothing shifts. */ +.video-frame { position: relative; aspect-ratio: 16 / 9; border: 1px solid var(--rule); background: #000; } +.video-frame > a, .video-frame > iframe { display: block; width: 100%; height: 100%; border: 0; } +.video-frame img { display: block; width: 100%; height: auto; } +.video-play-label { position: absolute; left: 16px; bottom: 16px; display: flex; align-items: center; gap: 10px; padding: 10px 14px; + background: var(--k); color: var(--paper); border: 1px solid var(--y); font: 700 13px/1 var(--f-mono); letter-spacing: .14em; text-transform: uppercase; } +.video-play-label::before { content: ""; border-block: 6px solid transparent; border-left: 10px solid var(--y); } +.video-play:hover .video-play-label, .video-play:focus-visible .video-play-label { background: var(--y); color: #000; } +.video-play:hover .video-play-label::before, .video-play:focus-visible .video-play-label::before { border-left-color: #000; } +.video-play:focus-visible { outline-offset: 4px; } +@media (max-width: 850px) { + .video { grid-template-columns: 1fr; } +} diff --git a/site/videos/yynbSGpHXGc.jpg b/site/videos/yynbSGpHXGc.jpg new file mode 100644 index 00000000..11a2ed60 Binary files /dev/null and b/site/videos/yynbSGpHXGc.jpg differ