Skip to content
Merged
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
14 changes: 13 additions & 1 deletion site/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Documentation · thirdshift</title>
<title>thirdshift documentation · Install, configure and run coding agents</title>
<meta name="description" content="The thirdshift user manual: installation, commands, configuration, workflows, logs and scheduling.">
<link rel="canonical" href="https://thirdshift.app/docs/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="thirdshift">
<meta property="og:url" content="https://thirdshift.app/docs/">
<meta property="og:title" content="thirdshift documentation · Install, configure and run coding agents">
<meta property="og:description" content="The thirdshift user manual: installation, commands, configuration, workflows, logs and scheduling.">
<meta property="og:image" content="https://thirdshift.app/share-card.png">
<meta property="og:image:alt" content="thirdshift: your issues go to press overnight.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="thirdshift documentation · Install, configure and run coding agents">
<meta name="twitter:description" content="The thirdshift user manual: installation, commands, configuration, workflows, logs and scheduling.">
<meta name="twitter:image" content="https://thirdshift.app/share-card.png">
<meta name="twitter:image:alt" content="thirdshift: your issues go to press overnight.">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/docs/docs.css">
Expand Down
14 changes: 13 additions & 1 deletion site/generate-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,21 @@ const page = `<!doctype html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Documentation · thirdshift</title>
<title>thirdshift documentation · Install, configure and run coding agents</title>
<meta name="description" content="The thirdshift user manual: installation, commands, configuration, workflows, logs and scheduling.">
<link rel="canonical" href="https://thirdshift.app/docs/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="thirdshift">
<meta property="og:url" content="https://thirdshift.app/docs/">
<meta property="og:title" content="thirdshift documentation · Install, configure and run coding agents">
<meta property="og:description" content="The thirdshift user manual: installation, commands, configuration, workflows, logs and scheduling.">
<meta property="og:image" content="https://thirdshift.app/share-card.png">
<meta property="og:image:alt" content="thirdshift: your issues go to press overnight.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="thirdshift documentation · Install, configure and run coding agents">
<meta name="twitter:description" content="The thirdshift user manual: installation, commands, configuration, workflows, logs and scheduling.">
<meta name="twitter:image" content="https://thirdshift.app/share-card.png">
<meta name="twitter:image:alt" content="thirdshift: your issues go to press overnight.">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/docs/docs.css">
Expand Down
12 changes: 12 additions & 0 deletions site/how-it-works/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
<title>How thirdshift works · thirdshift</title>
<meta name="description" content="An engineer's guide from GitHub issue to reviewed pull request: worktrees, agent sessions, Delivery, Repairs, Spec runs and scheduled work.">
<link rel="canonical" href="https://thirdshift.app/how-it-works/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="thirdshift">
<meta property="og:url" content="https://thirdshift.app/how-it-works/">
<meta property="og:title" content="How thirdshift works · thirdshift">
<meta property="og:description" content="An engineer's guide from GitHub issue to reviewed pull request: worktrees, agent sessions, Delivery, Repairs, Spec runs and scheduled work.">
<meta property="og:image" content="https://thirdshift.app/share-card.png">
<meta property="og:image:alt" content="thirdshift: your issues go to press overnight.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="How thirdshift works · thirdshift">
<meta name="twitter:description" content="An engineer's guide from GitHub issue to reviewed pull request: worktrees, agent sessions, Delivery, Repairs, Spec runs and scheduled work.">
<meta name="twitter:image" content="https://thirdshift.app/share-card.png">
<meta name="twitter:image:alt" content="thirdshift: your issues go to press overnight.">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Expand Down
23 changes: 16 additions & 7 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,31 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>thirdshift: your issues go to press overnight</title>
<meta name="description" content="thirdshift turns a GitHub issue into a pull request ready for review, or merges it when asked. You plan by day; unattended agents implement, review and open the pull request overnight.">
<title>thirdshift · GitHub issues to pull requests with coding agents</title>
<meta name="description" content="Turn GitHub issues into ready-for-review pull requests with unattended coding agents. thirdshift runs implementation, code review and CI in isolated Git worktrees.">
<link rel="canonical" href="https://thirdshift.app/">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "thirdshift",
"url": "https://thirdshift.app/"
}
</script>
<!-- The share card. docs/share-card/render.mjs renders the image from this page. -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="thirdshift">
<meta property="og:url" content="https://thirdshift.app/">
<meta property="og:title" content="thirdshift: your issues go to press overnight">
<meta property="og:description" content="thirdshift turns a GitHub issue into a pull request ready for review, or merges it when asked. You plan by day; unattended agents implement, review and open the pull request overnight.">
<meta property="og:title" content="thirdshift · GitHub issues to pull requests with coding agents">
<meta property="og:description" content="Turn GitHub issues into ready-for-review pull requests with unattended coding agents. thirdshift runs implementation, code review and CI in isolated Git worktrees.">
<meta property="og:image" content="https://thirdshift.app/share-card.png">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Your issues go to press overnight. The press line: issue #663 goes in at the feeder, passes through units 1 · C · Prepare, 2 · M · Implement, 3 · Y · Review and 4 · K · Finish, and comes out as pull request #665, merged.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="thirdshift: your issues go to press overnight">
<meta name="twitter:description" content="thirdshift turns a GitHub issue into a pull request ready for review, or merges it when asked. You plan by day; unattended agents implement, review and open the pull request overnight.">
<meta name="twitter:title" content="thirdshift · GitHub issues to pull requests with coding agents">
<meta name="twitter:description" content="Turn GitHub issues into ready-for-review pull requests with unattended coding agents. thirdshift runs implementation, code review and CI in isolated Git worktrees.">
<meta name="twitter:image" content="https://thirdshift.app/share-card.png">
<meta name="twitter:image:alt" content="Your issues go to press overnight. The press line: issue #663 goes in at the feeder, passes through units 1 · C · Prepare, 2 · M · Implement, 3 · Y · Review and 4 · K · Finish, and comes out as pull request #665, merged.">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
Expand Down Expand Up @@ -53,7 +62,7 @@
<h1 class="misreg">Your issues go to press overnight.</h1>
<div class="proof-row">
<div>
<p class="framing">The humans are the <b>day shift</b>: you talk the idea through, write the spec and cut it into tickets. The agents work the <b>third shift</b>: give thirdshift an 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.</p>
<p class="framing">The humans are the <b>day shift</b>: you talk the idea through, write the spec and cut it into tickets. Coding agents work the <b>third shift</b>: 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.</p>
<p class="credit"><a href="/how-it-works/">How thirdshift works</a>: an engineer's guide from issue to pull request, through Worktrees, agent review and Delivery. <a href="/docs/">Documentation</a> covers installation, commands and configuration.</p>
<p class="credit">Built on Matt Pocock's skills: <a href="https://www.aihero.dev/skills">aihero.dev/skills</a> · <a href="https://github.com/mattpocock/skills">mattpocock/skills</a>. Read <a href="/prompts/">every prompt and skill thirdshift sends</a>.</p>
</div>
Expand Down
15 changes: 14 additions & 1 deletion site/prompts/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Prompts and skills · thirdshift</title>
<meta name="description" content="Everything that steers thirdshift's agents: the claude command line, every prompt a Run sends, and the full text of every Factory skill, generated from the source.">
<meta name="description" content="Explore thirdshift's coding agent command lines, Session prompts and Factory skills for implementation, code review, security audits and pull request delivery.">
<link rel="canonical" href="https://thirdshift.app/prompts/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="thirdshift">
<meta property="og:url" content="https://thirdshift.app/prompts/">
<meta property="og:title" content="Prompts and skills · thirdshift">
<meta property="og:description" content="Explore thirdshift's coding agent command lines, Session prompts and Factory skills for implementation, code review, security audits and pull request delivery.">
<meta property="og:image" content="https://thirdshift.app/share-card.png">
<meta property="og:image:alt" content="thirdshift: your issues go to press overnight.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Prompts and skills · thirdshift">
<meta name="twitter:description" content="Explore thirdshift's coding agent command lines, Session prompts and Factory skills for implementation, code review, security audits and pull request delivery.">
<meta name="twitter:image" content="https://thirdshift.app/share-card.png">
<meta name="twitter:image:alt" content="thirdshift: your issues go to press overnight.">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Expand Down
2 changes: 2 additions & 0 deletions site/robots.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://thirdshift.app/sitemap.xml
7 changes: 7 additions & 0 deletions site/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://thirdshift.app/</loc></url>
<url><loc>https://thirdshift.app/docs/</loc></url>
<url><loc>https://thirdshift.app/how-it-works/</loc></url>
<url><loc>https://thirdshift.app/prompts/</loc></url>
</urlset>
5 changes: 5 additions & 0 deletions site/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ npm test
The tests start a local server on an unused port and close it and the browser
before exiting. CI runs them in the `site-test` job.

The page tests also consume the site without JavaScript to check canonical URLs,
distinct search metadata, homepage site-name structured data and sitemap discovery
through `robots.txt`. When adding a public page, give it a canonical URL and unique
title and description, then include it in `site/sitemap.xml` and the page tests.

The user manual's source is `docs/guide.md` at the repository root. After editing
it or `site/generate-docs.mjs`, run `cd site && npm run docs` and commit the generated
`site/docs/index.html` in the same pull request. `npm test` first runs
Expand Down
75 changes: 75 additions & 0 deletions site/tests/pages.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const { readFile } = require("node:fs/promises");
const { join } = require("node:path");
const { chromium } = require("playwright");

const publicPaths = ["/", "/docs/", "/how-it-works/", "/prompts/"];

let server, browser, origin;
before(async () => {
server = createServer(async (request, response) => {
Expand Down Expand Up @@ -35,6 +37,79 @@ async function pageForTest(t, options) {
return page;
}

test("every public page identifies its canonical URL, purpose and share metadata without JavaScript", async (t) => {
const page = await pageForTest(t, { javaScriptEnabled: false });
const titles = new Set();
const descriptions = new Set();
for (const path of publicPaths) {
assert.equal((await page.goto(`${origin}${path}`)).status(), 200);
assert.equal(await page.locator('link[rel="canonical"]').count(), 1, path);
assert.equal(await page.locator('link[rel="canonical"]').getAttribute("href"), `https://thirdshift.app${path}`);
const title = await page.title();
const description = await page.locator('meta[name="description"]').getAttribute("content");
assert.match(title, /thirdshift/);
assert.ok(description.length > 50, path);
titles.add(title);
descriptions.add(description);
for (const [selector, expected] of [
['meta[property="og:type"]', "website"],
['meta[property="og:site_name"]', "thirdshift"],
['meta[property="og:url"]', `https://thirdshift.app${path}`],
['meta[property="og:title"]', title],
['meta[property="og:description"]', description],
['meta[property="og:image"]', "https://thirdshift.app/share-card.png"],
['meta[name="twitter:card"]', "summary_large_image"],
['meta[name="twitter:title"]', title],
['meta[name="twitter:description"]', description],
['meta[name="twitter:image"]', "https://thirdshift.app/share-card.png"],
]) {
const tag = page.locator(`head ${selector}`);
assert.equal(await tag.count(), 1, `${path}: ${selector}`);
assert.equal(await tag.getAttribute("content"), expected, `${path}: ${selector}`);
}
assert.equal(await page.locator('meta[name="robots"][content*="noindex"]').count(), 0);
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");
await page.goto(`${origin}/`);
assert.match(await page.title(), /GitHub issues.*pull requests/);
});

test("robots advertises a valid sitemap of reachable canonical pages", async (t) => {
const page = await pageForTest(t, { javaScriptEnabled: false });
const robots = await page.request.get(`${origin}/robots.txt`);
assert.equal(robots.status(), 200);
assert.match(await robots.text(), /^Allow: \/$/m);
assert.match(await robots.text(), /^Sitemap: https:\/\/thirdshift\.app\/sitemap\.xml$/m);
const sitemap = await page.request.get(`${origin}/sitemap.xml`);
assert.equal(sitemap.status(), 200);
const urls = await page.evaluate(xml => {
const document = new DOMParser().parseFromString(xml, "application/xml");
if (document.querySelector("parsererror")) throw new Error("Invalid sitemap XML");
if (document.documentElement.namespaceURI !== "http://www.sitemaps.org/schemas/sitemap/0.9") {
throw new Error("Missing sitemap namespace");
}
return [...document.querySelectorAll("urlset > url > loc")].map(node => node.textContent);
}, await sitemap.text());
// The metadata test checks reachability and canonical URLs for this inventory.
assert.deepEqual(urls.sort(), publicPaths.map(path => `https://thirdshift.app${path}`));
});

test("the homepage declares the site name as static structured data", async (t) => {
const page = await pageForTest(t, { javaScriptEnabled: false });
await page.goto(`${origin}/`);
assert.equal(await page.locator('script[type="application/ld+json"]').count(), 1);
const site = JSON.parse(await page.locator('script[type="application/ld+json"]').textContent());
assert.equal(site["@context"], "https://schema.org");
assert.equal(site["@type"], "WebSite");
assert.equal(site.name, "thirdshift");
assert.equal(site.url, "https://thirdshift.app/");
assert.equal(site.url, await page.locator('link[rel="canonical"]').getAttribute("href"));
assert.equal(site.name, await page.locator('meta[property="og:site_name"]').getAttribute("content"));
});

test("the homepage links to the shared Markdown manual without JavaScript", async (t) => {
const page = await pageForTest(t, { javaScriptEnabled: false, viewport: { width: 320, height: 900 } });
await page.goto(`${origin}/`);
Expand Down
Loading