Repository navigation
Agent Experience
This page sets out what the website provides for coding agents that read the docs on a developer's behalf, how those files are built, and how to check that an agent reading only them writes correct code. Use it when building the agent features or changing a docs page.
Agents are readers here, like people who land from a search engine. The decision to ship llms.txt, llms-full.txt, a Markdown version of every page, "Copy page" and "Open in Claude / ChatGPT" is in the log on Design System. Items marked Proposed are not decided.
An agent fetches one or two URLs, reads them as text and writes code from them. It cannot run the demo, switch a tab or hover over a term. So the site gives it:
-
One file that lists everything (
llms.txt), to pick the right page. -
One file with everything (
llms-full.txt), for agents that load the whole docs at once. - Plain Markdown of every page, at a predictable URL.
- Pages that stand alone, written by the rules in Writing Style.
- Stable URLs, so links in an agent's context or a project's notes keep working.
llms.txt follows the llmstxt.org proposal: a Markdown file with an H1 (the only required part), a blockquote summary, optional paragraphs, then H2 sections listing links with a short note each. A section named "Optional" holds links an agent can skip. The file may sit at any path and covers the pages under it, so https://workclock.karthik.nz/llms.txt covers the whole site.
It is written by a route handler, website/app/llms.txt/route.ts. Fumadocs' llms().index() writes links as page.url (/docs/install: no host, pointing at HTML; fumadocs-core 16.17.8, source/llms.js), so the route renders its own list with absolute .md URLs.
Proposed: a fixed header before the list, carrying the facts agents most often get wrong:
# workclock
> Fast, correct business-time arithmetic (working hours, SLAs, shifts) for Rust, Python and JavaScript, and a maintained replacement for businesstimedelta, with a compatibility layer for existing code.
Status: pre-1.0; the API may still change in a minor release.
- Install: `cargo add workclock jiff` (Rust; the API uses jiff 0.2 types), `pip install workclock` or `uv add workclock` (Python), `npm i workclock` (JavaScript and TypeScript).
- Requires Rust 1.85+, CPython 3.9+, Node.js 22+.
- Inputs must be timezone-aware: Python `datetime` with `tzinfo`, JavaScript `Date` or ISO 8601 string with an offset, Rust `jiff::Zoned` or `jiff::Timestamp`. Time zones are IANA names such as `Europe/London`.
- JavaScript durations are milliseconds.
## Docs
- [Install](https://workclock.karthik.nz/docs/install.md): packages, requirements and time zone data
- [Quickstart: Rust](https://workclock.karthik.nz/docs/quickstart/rust.md), [Python](https://workclock.karthik.nz/docs/quickstart/python.md), [JavaScript](https://workclock.karthik.nz/docs/quickstart/javascript.md): the same support-desk example
- [Schedules](https://workclock.karthik.nz/docs/concepts/schedules.md): rules, working time and how rules combine
- [Time zones and DST](https://workclock.karthik.nz/docs/concepts/time-zones-and-dst.md): rule zones, gaps and overlaps
- [Errors](https://workclock.karthik.nz/docs/concepts/errors.md): every error class in each language and when it is raised
- [Migrating from businesstimedelta](https://workclock.karthik.nz/docs/guides/migrating-from-businesstimedelta.md): `workclock.compat` and every difference
## Reference
- [Rust API on docs.rs](https://docs.rs/workclock)
- [DESIGN.md](https://raw.githubusercontent.com/Karthik-beta/workclock/main/DESIGN.md): the specification, with an example and an ID for every edge case
## Optional
- [Benchmarks](https://workclock.karthik.nz/docs/benchmarks.md): medians and p95s against businesstimedelta 1.0.1
- [Changelog](https://raw.githubusercontent.com/Karthik-beta/workclock/main/CHANGELOG.md)DESIGN.md and CHANGELOG.md point at raw.githubusercontent.com so the agent gets Markdown, not GitHub's HTML. The "Status" line is the README's. The Playground and docs.rs are left out of llms-full.txt; docs.rs stays in llms.txt.
Every docs page's Markdown in sidebar order, from llms().full() in website/app/llms-full.txt/route.ts (Fumadocs: AI & LLMs). Each page starts with its title as an H1 and its URL on the next line, so an agent can cite it. It is not part of the llmstxt.org proposal but a common companion. Docs pages only: not the landing page or the wiki.
| Page | Markdown |
|---|---|
https://workclock.karthik.nz/docs/install/ |
https://workclock.karthik.nz/docs/install.md |
https://workclock.karthik.nz/docs/ |
https://workclock.karthik.nz/docs.md |
The rule: the page URL without its trailing slash, plus .md, as llmstxt.org suggests. With trailingSlash: true a page is docs/install/index.html, so docs/install.md sits beside its folder without a clash.
How it is built under a static export:
- Fumadocs serves
/docs/<slug>.mdthrough a Next.js rewrite to its Markdown route. A static export supports no rewrites, redirects or headers (Next.js: unsupported features), and GitHub Pages has none of its own. - Instead, a
GETroute handler withexport const dynamic = 'force-static'runs at build time and its response is written as a file (Next.js: route handlers).website/app/llms.mdx/docs/[[...slug]]/route.ts, withgenerateStaticParams, writesout/llms.mdx/docs/<slug>/content.mdfor each page, frompage.data.getText('processed')(needsincludeProcessedMarkdown: true). Fumadocs' static example does the same (examples/next-static/app). -
Proposed:
website/scripts/copy-markdown.mjs, using only Node'sfs, runs afternext buildand copies eachcontent.mdtoout/docs/<slug>.md. The alternative is to publish the longer/llms.mdx/docs/<slug>/content.mdURLs as they are, with no script. The owner chooses; every.mdURL on this site assumes the first. - The Pages deploy uploads
out/as an artifact (Website Tech Stack), so.mdfiles are served as they are. After the first deploy, check thatcurl -I https://workclock.karthik.nz/docs/install.mdreturns200with a text content type. -
Proposed: each page's
<head>names its Markdown version:alternates: { types: { 'text/markdown': '<absolute .md URL>' } }ingenerateMetadata.
What a Markdown version contains:
- The title as an H1, then the page's first paragraph.
- Code tabs as one fenced block per language, each under a label naming the language ("Rust", "Python", "JavaScript"). Unlabelled blocks are a bug.
- Callouts as blockquotes starting with their kind:
> **Note:**,> **Warning:**. - Absolute links (
https://workclock.karthik.nz/...), because the file is read outside the site. - The text equivalents of interactive content, which every page already has.
Proposed: a landing page Markdown version at https://workclock.karthik.nz/index.md, from the same data as sections 2 to 6 of Landing Page.
Under every docs page's description: Copy page, and an Open menu with View as Markdown, Open in Claude and Open in ChatGPT. Built as a custom component on Fumadocs' MarkdownCopyButton; why it does not use Fumadocs' ViewOptionsPopover, and the visual spec, are on Components. No amber, no service logos.
The prompt passed to Claude and ChatGPT, with the page URL filled in:
Read https://workclock.karthik.nz/docs/install.md, a page of the workclock documentation, so I can ask you questions about it. For anything it does not cover, use https://workclock.karthik.nz/llms.txt.
Link formats, as fumadocs-ui 16.17.8 builds them (layouts/shared/page-actions.js):
- Claude:
https://claude.ai/new?q=<URL-encoded prompt> - ChatGPT:
https://chatgpt.com/?prompt=<URL-encoded prompt>&hints=search
Neither service documents these, so test both before launch. Links open in a new tab with rel="noopener". The site sends nothing: the visitor's browser opens the other service with the prompt in its address, and the visitor sees it before sending.
The writing rules are on Writing Style and the URL rules on Docs Site Structure. Two follow from agents in particular:
-
The
titleand one-sentencedescriptionin front matter are what an agent sees inllms.txt, so each says exactly what the page covers. -
A published URL never stops working. If a page must move, the old path gets a small HTML page with
<meta http-equiv="refresh" content="0; url=...">and<link rel="canonical">to the new one (GitHub Pages has no server redirects), and the old.mdpath keeps a copy of the new Markdown (Proposed mechanism).llms.txt,llms-full.txtand every.mdpath are as stable as the pages. If versioned docs come later, the unversioned URLs keep pointing at the latest release.
At the end of the Install page, a copyable block titled "Prompt for your coding agent". The developer pastes it into their agent before asking it to use workclock. Proposed text:
Add the workclock library to this project to compute working time: working hours, SLAs and shifts, with breaks, holidays and time zones.
Install:
- Rust: cargo add workclock jiff (the API takes and returns jiff 0.2 types)
- Python: pip install workclock, or uv add workclock
- JavaScript or TypeScript: npm i workclock
Requirements: Rust 1.85 or later, CPython 3.9 or later, Node.js 22 or later.
How it works:
- Build one Schedule from rules: WorkDay (working hours on given weekdays), Break (time off on given weekdays) and Holidays (dates removed from 00:00 to the next 00:00).
- Give the schedule an IANA time zone name, such as "Europe/London". Rules without their own zone use the schedule's, holidays included.
- WorkDay defaults to Monday to Friday. An end time at or before the start time is an overnight shift.
- between(a, b) returns the working time between two instants. It is negative when b is before a.
- add(t, d) returns the earliest instant with exactly d of working time after t; sub(t, d) goes backwards. add(t, 0) returns t unchanged.
- next_period and previous_period (nextPeriod and previousPeriod in JavaScript) return a stretch of working time.
Language rules:
- Python: from workclock import Schedule, WorkDay, Break, Holidays. Pass datetimes with tzinfo (zoneinfo.ZoneInfo); naive datetimes raise ValueError. Weekdays are integers, 0 is Monday. Durations are timedelta, or add(t, hours=8). For many inputs, use between_many, add_many and sub_many.
- JavaScript: import { Schedule } from "workclock". new Schedule({ timeZone, rules: [{ type: "workday", start: "09:00", end: "17:00" }, ...] }). Instants are Date objects or ISO 8601 strings with an offset. Durations are numbers of milliseconds. Weekdays are "mon" to "sun". For many inputs, use betweenMany, addMany and subMany. In a browser without a bundler, import init from "workclock/web" and await init() first.
- Rust: use workclock::{Schedule, WorkDay, Break, Holidays, Weekdays}; build with Schedule::builder().time_zone("...").rule(...).build()?. Instants are jiff::Zoned or jiff::Timestamp; durations are jiff::SignedDuration.
Errors: a schedule with no working time fails when it is built (NoWorkingTimeError in Python and JavaScript, workclock::Error::NoWorkingTime in Rust). Every search is bounded, so no call hangs. In Python and JavaScript the errors subclass WorkclockError.
Existing businesstimedelta code: replace its import with "import workclock.compat as businesstimedelta". Most code runs unchanged.
Check your code with this example before using it: a support desk in Europe/London works 09:00 to 17:00, Monday to Friday, with a break from 12:00 to 13:00, and is closed on 2026-12-25 and 2026-12-28. Adding 8 working hours to 2026-12-24 16:30 London time gives 2026-12-30 09:30 London time.
Full documentation for agents: https://workclock.karthik.nz/llms-full.txt
Every statement comes from the README, DESIGN.md or the package's type definitions; when one changes, the prompt changes in the same PR. Before 0.1.0 the block starts with "workclock is not published yet; these install commands work from version 0.1.0."
The site is static files, so it cannot run an MCP server. llms.txt, llms-full.txt and the .md pages give agents the same docs without one. Proposed, for later and only with the owner's approval: a separate package that runs an MCP server on the developer's machine, exposing the docs and between, add and sub as tools; never part of the library packages or the website.
The goal: an agent given only llms.txt and the pages it links writes code that runs and gives the right answer.
Proposed, run in the website CI job with Node built-ins only:
-
out/llms.txtexists, starts with# workclock, has a blockquote first after the H1, and every list item under an H2 is a link. - Every site link in
llms.txtresolves to a file inout/. (The lychee link check in CI also covers this.) - Every docs page has its
.mdfile, andllms-full.txtcontains every page title. - No
.mdfile contains a relative link (one starting with/or./). - Every fenced code block in the
.mdfiles has a language tag. - The docs' code samples run in CI and match their expected-output comments, as the README's already do (
test_readme.py,readme.test.ts).
Proposed: a manual check by the owner before each release and after large docs changes.
- Start a fresh coding-agent session with web access to the docs site only: no web search, no repository, no other files.
- Give it
https://workclock.karthik.nz/llms.txtand one task below, in the language the task names. - Run its code as written in an empty project with workclock installed.
- Pass if it runs and prints the expected answer. Note which page the agent read.
- Record the date, docs commit, agent, and pass or fail per task in an issue labelled
type:docs.
A failure is fixed in the docs, never by making the task easier. If a task fails twice in a row after a fix, open an issue with the agent's code and the page it read.
| ID | Task | Expected answer | Source |
|---|---|---|---|
| T1 | In Python: a support desk in London works 09:00–17:00 Monday to Friday with a 12:00–13:00 break and is closed on 25 and 28 December 2026. A ticket opens at 16:30 on 24 December 2026 with an 8-hour SLA. When is it due? | 2026-12-30 09:30, London time | README |
| T2 | In JavaScript, same desk and ticket: how much working time has elapsed by 11:00 London time on 29 December 2026? | 9000000 ms (2 hours 30 minutes) | README |
| T3 | In Rust: a night shift runs 22:00–06:00 Monday to Friday in UTC. Working time between Monday 2026-01-05 22:00 and Tuesday 06:00, and the instant 4 working hours after Monday 22:00? | 8 hours; 2026-01-06T02:00:00Z | DESIGN.md O1, O2 |
| T4 | In Python: 09:00–17:00 Monday to Friday in UTC, holiday on Wednesday 2026-01-07. Add 2 working hours to Tuesday 2026-01-06 16:00. | Thursday 2026-01-08 10:00 UTC | DESIGN.md H2 |
| T5 | In Python, same schedule without holidays: working time from Monday 2026-01-05 12:00 to Monday 10:00. | timedelta(hours=-2) |
DESIGN.md B4 |
| T6 | In Python: 09:00–17:00 Monday to Friday in Asia/Kolkata. Working hours between 10:00 and 12:00 local time on Monday 2026-01-05? | 2 hours, from timezone-aware datetimes | DESIGN.md N1 |
| T7 | In America/New_York, a shift runs 22:00–06:00 every day. Working time between Saturday 2026-03-07 22:00 and Sunday 2026-03-08 06:00 local time? | 7 hours (clocks go forward that night) | DESIGN.md D1 |
| T8 | In Python: build a schedule from a 09:00–17:00 WorkDay with no working days. What happens? |
workclock.NoWorkingTimeError when built |
DESIGN.md E2 |
| T9 | Here is businesstimedelta 1.0.1 code (a short snippet with WorkDayRule, HolidayRule and Rules). Switch it to workclock with the fewest changes. |
The import becomes import workclock.compat as businesstimedelta; the rest is unchanged |
MIGRATING.md |
-
Proposed: the
llms.txtheader;copy-markdown.mjsand the/docs/<slug>.mdURLs (versus thellms.mdxURLs); thetext/markdownalternate link; the landingindex.md; the redirect-page mechanism; the agent prompt; the build checks; the agent task check; a local MCP server package. - After each build, compare
llms-full.txt's size with the guides; if the reference pages dominate, move them to "Optional" inllms.txtand out ofllms-full.txt(Proposed). - Test both "Open in" link formats before launch.
workclock is MIT licensed. The repository documentation is authoritative; this wiki summarises it.