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
1,263 changes: 82 additions & 1,181 deletions README.md

Large diffs are not rendered by default.

1,217 changes: 1,217 additions & 0 deletions docs/guide.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions site/docs/docs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.manual { font-size: 15px; }
.manual .wrap { max-width: 86ch; }
.manual header { padding-block: 28px; border-bottom: 1px solid var(--rule); }
.manual nav ul { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px 24px; padding-left: 20px; }
.manual nav[aria-label="Site"] { display: flex; flex-wrap: wrap; gap: 12px 24px; }
.manual main { padding-block: 32px 80px; overflow-wrap: anywhere; }
.manual h1 { font-size: clamp(44px, 9vw, 80px); }
.manual h2 { font-size: clamp(30px, 5vw, 48px); margin-top: 56px; }
.manual h3 { font-size: 28px; margin-top: 32px; }
.manual h4 { font-size: 18px; margin-top: 28px; }
.manual [id] { scroll-margin-top: 24px; }
.manual pre { padding: 16px; background: var(--k2); border: 1px solid var(--rule); white-space: pre-wrap; overflow-wrap: anywhere; }
.manual li { margin-block: 8px; }
.manual table { display: block; overflow-x: auto; border-collapse: collapse; }
.manual th, .manual td { border: 1px solid var(--rule); padding: 8px; text-align: left; }
.skip-manual { position: absolute; top: -100px; padding: 12px; background: var(--paper); color: #000; }
.skip-manual:focus { top: 12px; }
922 changes: 922 additions & 0 deletions site/docs/index.html

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions site/generate-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// docs/guide.md is the source; deployment serves the checked-in HTML unchanged.
import { readFile, writeFile } from "node:fs/promises";
import { Marked } from "marked";
import GithubSlugger from "github-slugger";

const source = new URL("../docs/guide.md", import.meta.url);
const output = new URL("docs/index.html", import.meta.url);
const slugger = new GithubSlugger();
const sections = [];
const markdown = new Marked({
renderer: {
heading({ tokens, depth, text }) {
const id = slugger.slug(text);
const content = this.parser.parseInline(tokens);
if (depth === 2) sections.push(`<li><a href="#${id}">${content}</a></li>`);
return `<h${depth} id="${id}">${content}</h${depth}>\n`;
},
},
walkTokens(token) {
if (token.type !== "link" && token.type !== "image") return;
// Fragment and external links stay intact. Repository paths are relative
// to docs/guide.md, so resolve them there before linking back to GitHub.
if (/^(?:[a-z][a-z\d+.-]*:|#|\/\/)/i.test(token.href)) return;
token.href = new URL(token.href, "https://github.com/JacobStephens2/thirdshift/blob/main/docs/").href;
},
});
const content = markdown.parse(await readFile(source, "utf8"));
const page = `<!doctype html>
<!-- Generated from docs/guide.md by site/generate-docs.mjs. Run: cd site && npm run docs -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Documentation · thirdshift</title>
<meta name="description" content="The thirdshift user manual: installation, commands, configuration, workflows, logs and scheduling.">
<link rel="canonical" href="https://thirdshift.app/docs/">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/docs/docs.css">
</head>
<body class="manual">
<a class="skip-manual" href="#thirdshift">Skip to the manual</a>
<header class="wrap">
<nav aria-label="Site"><a href="/">thirdshift / home</a><a href="/how-it-works/">How thirdshift works</a><a href="/prompts/">Prompts and skills</a><a href="https://github.com/JacobStephens2/thirdshift/blob/main/docs/guide.md">Markdown source</a></nav>
<p>This manual is published from the same Markdown you can read in the repository.</p>
<nav aria-label="Manual sections"><ul>${sections.join("\n")}</ul></nav>
</header>
<main class="wrap">${content}</main>
</body>
</html>
`;

if (process.argv.slice(2).some(arg => arg !== "--check")) {
throw new Error("Usage: node generate-docs.mjs [--check]");
}
if (process.argv.includes("--check")) {
if (await readFile(output, "utf8") !== page) {
throw new Error("Documentation is stale. Run: cd site && npm run docs");
}
} else {
await writeFile(output, page);
}
10 changes: 5 additions & 5 deletions site/how-it-works/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ <h1>How thirdshift works</h1>
<p class="guide-connector">Shared Git history and remotes · separate checked-out files</p>
</figure>
<p>The work happens in the Worktree rather than switching your Launch directory to the Issue branch. If an existing Issue branch has no pull request or an open one, thirdshift continues it instead of starting over. An open pull request supplies its Base branch. Once a branch's pull request is closed or merged, later work uses a new numbered Issue branch.</p>
<aside class="guide-callout"><strong>A checkout is not a sandbox.</strong> Sessions run unattended as your user. The Harness has access to that account's files, credentials and tools; the separate Worktree keeps edits apart, but does not confine the process. <a href="https://github.com/JacobStephens2/thirdshift/blob/main/README.md#unattended-sessions">Read the permissions and cleanup details</a>.</aside>
<aside class="guide-callout"><strong>A checkout is not a sandbox.</strong> Sessions run unattended as your user. The Harness has access to that account's files, credentials and tools; the separate Worktree keeps edits apart, but does not confine the process. <a href="/docs/#unattended-sessions">Read the permissions and cleanup details</a>.</aside>
</div>
</section>

Expand Down Expand Up @@ -111,7 +111,7 @@ <h1>How thirdshift works</h1>
</figure>
<p>Repair and upstream-move budgets keep the loop bounded. If a CI-fix Repair leaves the same failing head, that head gets one <strong>Check re-run</strong>; if its own checks stay red, the Run fails rather than spending more Repairs on unchanged code.</p>
<p>An <strong>Inherited failure</strong> is a check that also failed under the same name on the Base branch commit last merged in. thirdshift does not ask a Repair to fix that on this Issue branch. It fails the Run, or, when allowed, starts a separate <strong>Base fix</strong> and waits before trying again.</p>
<p>An optional <strong>Security review</strong> runs after the opening session (or after the Spec review for a Spec PR). It fixes reproduced vulnerabilities introduced by this change; an introduced finding left unfixed holds Self-merge. <a href="https://github.com/JacobStephens2/thirdshift/blob/main/README.md#security-review">Security review details</a>.</p>
<p>An optional <strong>Security review</strong> runs after the opening session (or after the Spec review for a Spec PR). It fixes reproduced vulnerabilities introduced by this change; an introduced finding left unfixed holds Self-merge. <a href="/docs/#security-review">Security review details</a>.</p>
</div>
</section>

Expand All @@ -128,7 +128,7 @@ <h1>How thirdshift works</h1>
<pre class="guide-command"><code>thirdshift status
thirdshift show https://github.com/acme/widgets/issues/7
thirdshift logs https://github.com/acme/widgets/issues/7 --follow</code></pre>
<p>The <strong>Command log</strong> records the whole command; <strong>Session logs</strong> record the individual agent sessions, and the <strong>Activity log</strong> records starts and outcomes per repository. Optional email notifications report how the Run ended. <a href="https://github.com/JacobStephens2/thirdshift/blob/main/README.md#logs">Inspecting logs</a>.</p>
<p>The <strong>Command log</strong> records the whole command; <strong>Session logs</strong> record the individual agent sessions, and the <strong>Activity log</strong> records starts and outcomes per repository. Optional email notifications report how the Run ended. <a href="/docs/#logs">Inspecting logs</a>.</p>
</div>
</section>

Expand All @@ -149,7 +149,7 @@ <h1>How thirdshift works</h1>
</ol>
</figure>
<p>Each Ticket is a Merge run into the <strong>Spec branch</strong>, not directly into <code>main</code>. The <strong>Spec PR</strong> gathers the result into the Base branch. A Ticket closing means its work landed on the Spec branch; the whole Spec has not shipped yet.</p>
<p>A failed Ticket stops only its dependents; other reachable Tickets can still run. Tickets marked for human work or triage also hold their dependents. An incomplete Spec leaves a draft Spec PR with a Tickets checklist. Rerunning continues existing branches. <a href="https://github.com/JacobStephens2/thirdshift/blob/main/README.md#spec-runs">Spec run controls and limits</a>.</p>
<p>A failed Ticket stops only its dependents; other reachable Tickets can still run. Tickets marked for human work or triage also hold their dependents. An incomplete Spec leaves a draft Spec PR with a Tickets checklist. Rerunning continues existing branches. <a href="/docs/#spec-runs">Spec run controls and limits</a>.</p>
</div>
</section>

Expand All @@ -167,7 +167,7 @@ <h1>How thirdshift works</h1>
<p class="guide-connector">Eligible work ↓ Run or Spec run ↓ reviewed pull request</p>
</figure>
<p>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.</p>
<p>Next: <a href="/prompts/">inspect every prompt and skill</a>, see <a href="/#how-a-run-works">a real Run's recorded output</a>, or use the <a href="https://github.com/JacobStephens2/thirdshift/blob/main/README.md">README for installation, commands and configuration</a>.</p>
<p>Next: <a href="/prompts/">inspect every prompt and skill</a>, see <a href="/#how-a-run-works">a real Run's recorded output</a>, or use the <a href="/docs/">manual for installation, commands and configuration</a>.</p>
</div>
</section>
</main>
Expand Down
3 changes: 2 additions & 1 deletion site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,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="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.</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>
<div>
Expand Down Expand Up @@ -516,6 +516,7 @@ <h3><a href="https://github.com/JacobStephens2/thirdshift/pull/21">Numbered Issu
<a href="https://github.com/JacobStephens2/thirdshift">GitHub</a>
<a href="https://github.com/JacobStephens2/thirdshift/releases">Releases</a>
<a href="/how-it-works/">How thirdshift works</a>
<a href="/docs/">Documentation</a>
<a href="/prompts/">Prompts and skills</a>
<a href="https://github.com/JacobStephens2/thirdshift/blob/main/LICENSE">MIT License</a>
<span>Factory skills adapted from <a href="https://github.com/mattpocock/skills">Matt Pocock's skills</a></span>
Expand Down
22 changes: 22 additions & 0 deletions site/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion site/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"private": true,
"scripts": {
"test": "node --test tests/*.test.cjs"
"docs": "node generate-docs.mjs",
"docs:check": "node generate-docs.mjs --check",
"test": "npm run docs:check && node --test tests/*.test.cjs"
},
"devDependencies": {
"github-slugger": "2.0.0",
"marked": "18.1.1",
"playwright": "1.64.0"
}
}
8 changes: 7 additions & 1 deletion site/tests/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Browser tests

The site still serves static files with no build step. To run its browser tests:
The site serves checked-in static files; deployment needs no build step. To run its browser tests:

```sh
cd site
Expand All @@ -11,3 +11,9 @@ 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 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
`npm run docs:check` to reject stale output. The browser tests check navigation,
section anchors, command escaping and reading at mobile widths without JavaScript.
40 changes: 40 additions & 0 deletions site/tests/docs.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { readFile, stat } = require("node:fs/promises");
const { resolve, dirname } = require("node:path");

test("the README and manual's repository links reach files and section anchors", async () => {
const { marked } = await import("marked");
const { default: GithubSlugger } = await import("github-slugger");
const root = resolve(__dirname, "../..");
async function document(path) {
const source = await readFile(path, "utf8");
const tokens = marked.lexer(source);
const slugger = new GithubSlugger();
const anchors = new Set([...source.matchAll(/<a name="([^"]+)"/g)].map(match => match[1]));
marked.walkTokens(tokens, token => {
if (token.type === "heading") anchors.add(slugger.slug(token.text));
});
return { tokens, anchors };
}
for (const file of ["README.md", "docs/guide.md"]) {
const path = resolve(root, file);
const { tokens } = await document(path);
const links = [];
marked.walkTokens(tokens, token => {
if (token.type === "link" && !/^(?:[a-z][a-z\d+.-]*:|\/)/i.test(token.href)) links.push(token.href);
});
for (const link of links) {
const [target, fragment] = link.split("#");
const destination = target ? resolve(dirname(path), target) : path;
assert.ok(await stat(destination), `${file}: ${link}`);
if (fragment) assert.ok((await document(destination)).anchors.has(fragment), `${file}: ${link}`);
}
}

// These README URLs were linked from the site before the manual moved.
const { anchors } = await document(resolve(root, "README.md"));
for (const anchor of ["unattended-sessions", "security-review", "logs", "spec-runs"]) {
assert.ok(anchors.has(anchor), `existing README bookmark #${anchor}`);
}
});
32 changes: 31 additions & 1 deletion site/tests/pages.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ async function pageForTest(t, options) {
return page;
}

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}/`);
await page.getByRole("link", { name: "Documentation", exact: true }).first().click({ timeout: 2000 });
assert.equal(new URL(page.url()).pathname, "/docs/");
assert.equal(await page.getByRole("heading", { name: "thirdshift", exact: true, level: 1 }).isVisible(), true);
await page.getByRole("navigation", { name: "Manual sections" }).getByRole("link", { name: "Install", exact: true }).click();
assert.equal(new URL(page.url()).hash, "#install");
assert.equal(await page.locator("#install + pre code").textContent(),
"curl -LsSf https://github.com/JacobStephens2/thirdshift/releases/latest/download/thirdshift-installer.sh | sh\n");
assert.equal(await page.getByRole("link", { name: "Markdown source", exact: true }).getAttribute("href"),
"https://github.com/JacobStephens2/thirdshift/blob/main/docs/guide.md");
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
const fragments = await page.locator('main a[href^="#"]').evaluateAll(nodes => nodes.map(node => node.hash.slice(1)));
for (const fragment of fragments) {
assert.equal(await page.locator(`[id="${fragment}"]`).count(), 1, fragment);
}
assert.ok((await page.locator("main code").allTextContents()).some(text => text.includes("<Issue URL>")), "command placeholders remain text");
assert.equal(await page.getByRole("link", { name: "ADR-0001", exact: true }).first().getAttribute("href"),
"https://github.com/JacobStephens2/thirdshift/blob/main/docs/adr/0001-rust-binary-with-embedded-skills.md");
for (const path of ["/styles.css", "/docs/docs.css"]) {
assert.equal((await page.request.get(`${origin}${path}`)).status(), 200, path);
}
});

test("the homepage leads to a guide that teaches the process in order", async (t) => {
const page = await pageForTest(t);
await page.goto(`${origin}/`);
Expand Down Expand Up @@ -73,6 +98,11 @@ test("guide navigation and Repair loop links reach visible explanations without

test("the guide's local links and styles resolve, including prompt deep links", async (t) => {
const page = await pageForTest(t);
// Exercise navigation before the deferred fragment-reveal script finishes loading.
await page.route("**/main.js", async route => {
await new Promise(resolve => setTimeout(resolve, 200));
await route.continue();
});
const errors = [];
page.on("pageerror", error => errors.push(error.message));
await page.goto(`${origin}/how-it-works/`);
Expand All @@ -82,7 +112,7 @@ test("the guide's local links and styles resolve, including prompt deep links",
assert.equal(response.status(), 200, path);
}
await page.getByRole("link", { name: "the review procedure" }).click();
assert.equal(await page.locator("#skill-thirdshift-code-review .job-body").isVisible(), true);
await page.locator("#skill-thirdshift-code-review .job-body").waitFor({ state: "visible", timeout: 5000 });
assert.deepEqual(errors, []);
});

Expand Down
Loading