From fcd5d23139bb173db02877126ecbb0ff91351016 Mon Sep 17 00:00:00 2001 From: arturict Date: Fri, 24 Jul 2026 20:44:38 +0200 Subject: [PATCH] release work-sdk v0.5 --- README.md | 9 +- apps/video/.gitignore | 8 + apps/video/.prettierrc | 5 + apps/video/README.md | 11 + apps/video/eslint.config.mjs | 3 + apps/video/package.json | 33 + apps/video/public/brands/SOURCES.md | 11 + apps/video/public/brands/atlassian.svg | 1 + apps/video/public/brands/azure.svg | 1 + apps/video/public/brands/github.svg | 1 + apps/video/public/brands/gitlab.svg | 6 + apps/video/public/brands/linear.svg | 1 + apps/video/remotion.config.ts | 11 + apps/video/src/Composition.tsx | 356 +++ apps/video/src/Root.tsx | 10 + apps/video/src/index.css | 9 + apps/video/src/index.ts | 4 + apps/video/tsconfig.json | 15 + apps/web/app/docs/providers/github/page.tsx | 46 + apps/web/app/docs/providers/jira/page.tsx | 47 + apps/web/app/docs/providers/linear/page.tsx | 40 + apps/web/app/docs/providers/page.tsx | 6 +- apps/web/app/docs/reference/client/page.tsx | 17 +- apps/web/app/docs/reference/errors/page.tsx | 2 +- apps/web/app/globals.css | 94 +- apps/web/app/layout.tsx | 7 +- apps/web/app/page.tsx | 46 +- apps/web/app/sitemap.ts | 3 + apps/web/components/docs-navigation.tsx | 27 + apps/web/components/docs-search.tsx | 77 + apps/web/components/docs-shell.tsx | 18 +- apps/web/components/site-shell.tsx | 2 +- apps/web/lib/machine-content.ts | 3 + apps/web/package.json | 2 +- apps/web/tests/site.test.mjs | 17 +- examples/approval-cli/CHANGELOG.md | 7 + examples/approval-cli/package.json | 2 +- examples/webhook-bot/CHANGELOG.md | 7 + examples/webhook-bot/package.json | 2 +- packages/work-sdk/CHANGELOG.md | 6 + packages/work-sdk/README.md | 6 + packages/work-sdk/package.json | 8 +- packages/work-sdk/src/client.ts | 50 +- packages/work-sdk/src/index.ts | 4 + packages/work-sdk/src/types.ts | 25 +- packages/work-sdk/test/client.test.ts | 27 + packages/work-sdk/tsconfig.type-tests.json | 8 + packages/work-sdk/type-tests/client.ts | 44 + pnpm-lock.yaml | 2751 ++++++++++++++++++- pnpm-workspace.yaml | 27 + 50 files changed, 3825 insertions(+), 98 deletions(-) create mode 100644 apps/video/.gitignore create mode 100644 apps/video/.prettierrc create mode 100644 apps/video/README.md create mode 100644 apps/video/eslint.config.mjs create mode 100644 apps/video/package.json create mode 100644 apps/video/public/brands/SOURCES.md create mode 100644 apps/video/public/brands/atlassian.svg create mode 100644 apps/video/public/brands/azure.svg create mode 100644 apps/video/public/brands/github.svg create mode 100644 apps/video/public/brands/gitlab.svg create mode 100644 apps/video/public/brands/linear.svg create mode 100644 apps/video/remotion.config.ts create mode 100644 apps/video/src/Composition.tsx create mode 100644 apps/video/src/Root.tsx create mode 100644 apps/video/src/index.css create mode 100644 apps/video/src/index.ts create mode 100644 apps/video/tsconfig.json create mode 100644 apps/web/app/docs/providers/github/page.tsx create mode 100644 apps/web/app/docs/providers/jira/page.tsx create mode 100644 apps/web/app/docs/providers/linear/page.tsx create mode 100644 apps/web/components/docs-navigation.tsx create mode 100644 apps/web/components/docs-search.tsx create mode 100644 packages/work-sdk/tsconfig.type-tests.json create mode 100644 packages/work-sdk/type-tests/client.ts diff --git a/README.md b/README.md index fd78972..94ec93d 100644 --- a/README.md +++ b/README.md @@ -141,8 +141,11 @@ Credentials stay inside adapters and are never copied into prepared changes. - [Runnable example apps](https://work-sdk.vercel.app/docs/examples) - [Safe-write protocol](https://work-sdk.vercel.app/docs/concepts/safe-writes) - [Provider comparison](https://work-sdk.vercel.app/docs/providers) -- [Azure DevOps guide](https://work-sdk.vercel.app/docs/providers/azure-devops) +- [GitHub guide](https://work-sdk.vercel.app/docs/providers/github) - [GitLab guide](https://work-sdk.vercel.app/docs/providers/gitlab) +- [Linear guide](https://work-sdk.vercel.app/docs/providers/linear) +- [Jira Cloud guide](https://work-sdk.vercel.app/docs/providers/jira) +- [Azure DevOps guide](https://work-sdk.vercel.app/docs/providers/azure-devops) - [Client API reference](https://work-sdk.vercel.app/docs/reference/client) - [Normalized errors](https://work-sdk.vercel.app/docs/reference/errors) - [Agent integration](https://work-sdk.vercel.app/docs/guides/agents) @@ -168,6 +171,10 @@ const update = await work.prepareUpdate(issue.id, { const comment = await work.prepareComment(issue.id, { body: "Implemented in PR #481 and verified in staging.", }); + +const receipt = await work.commit(comment); +receipt.action; // "comment" +receipt.comment.body; // string — action-specific receipt typing in v0.5 ``` ## Idempotency diff --git a/apps/video/.gitignore b/apps/video/.gitignore new file mode 100644 index 0000000..3f14988 --- /dev/null +++ b/apps/video/.gitignore @@ -0,0 +1,8 @@ +node_modules +dist +build +.DS_Store +.env + +# Ignore the output video from Git but not videos you import into src/. +out diff --git a/apps/video/.prettierrc b/apps/video/.prettierrc new file mode 100644 index 0000000..37d5071 --- /dev/null +++ b/apps/video/.prettierrc @@ -0,0 +1,5 @@ +{ + "useTabs": false, + "bracketSpacing": true, + "tabWidth": 2 +} diff --git a/apps/video/README.md b/apps/video/README.md new file mode 100644 index 0000000..2a92c9a --- /dev/null +++ b/apps/video/README.md @@ -0,0 +1,11 @@ +# Work SDK v0.5 launch video + +An original 30-second, 1280×720 Remotion product teaser for Work SDK v0.5. + +```bash +pnpm --filter @work-sdk/video dev +pnpm --filter @work-sdk/video still +pnpm --filter @work-sdk/video render +``` + +The company marks under `public/brands` are the same locally cached SVGL assets used by the marketing site. Attribution links are recorded in `public/brands/SOURCES.md`. diff --git a/apps/video/eslint.config.mjs b/apps/video/eslint.config.mjs new file mode 100644 index 0000000..13b44a0 --- /dev/null +++ b/apps/video/eslint.config.mjs @@ -0,0 +1,3 @@ +import { config } from "@remotion/eslint-config-flat"; + +export default config; diff --git a/apps/video/package.json b/apps/video/package.json new file mode 100644 index 0000000..71d76aa --- /dev/null +++ b/apps/video/package.json @@ -0,0 +1,33 @@ +{ + "name": "@work-sdk/video", + "version": "0.5.0", + "description": "Work SDK v0.5 launch video", + "license": "UNLICENSED", + "private": true, + "dependencies": { + "@remotion/cli": "4.0.499", + "react": "19.2.3", + "react-dom": "19.2.3", + "remotion": "4.0.499" + }, + "devDependencies": { + "@remotion/eslint-config-flat": "4.0.499", + "@types/react": "19.2.7", + "@types/web": "0.0.166", + "eslint": "9.19.0", + "prettier": "3.8.1", + "typescript": "5.9.3" + }, + "scripts": { + "dev": "remotion studio", + "build": "remotion bundle", + "render": "remotion render WorkSdkV05 out/work-sdk-v0.5.mp4 --codec=h264 --crf=18", + "still": "remotion still WorkSdkV05 out/work-sdk-v0.5-poster.png --frame=850", + "upgrade": "remotion upgrade", + "check": "eslint src && tsc", + "lint": "eslint src && tsc" + }, + "sideEffects": [ + "*.css" + ] +} diff --git a/apps/video/public/brands/SOURCES.md b/apps/video/public/brands/SOURCES.md new file mode 100644 index 0000000..73657ef --- /dev/null +++ b/apps/video/public/brands/SOURCES.md @@ -0,0 +1,11 @@ +# Brand asset sources + +These SVGs are cached from the [SVGL registry](https://svgl.app/) so the marketing site does not depend on third-party requests at runtime. + +- `github.svg`: `https://svgl.app/library/github_light.svg` +- `gitlab.svg`: `https://svgl.app/library/gitlab.svg` +- `linear.svg`: `https://svgl.app/library/linear.svg` +- `atlassian.svg`: `https://svgl.app/library/atlassian.svg` (used for Jira, an Atlassian product) +- `azure.svg`: `https://svgl.app/library/azure.svg` (used for Azure DevOps) + +The respective companies retain all trademark rights. diff --git a/apps/video/public/brands/atlassian.svg b/apps/video/public/brands/atlassian.svg new file mode 100644 index 0000000..0f341d4 --- /dev/null +++ b/apps/video/public/brands/atlassian.svg @@ -0,0 +1 @@ + diff --git a/apps/video/public/brands/azure.svg b/apps/video/public/brands/azure.svg new file mode 100644 index 0000000..311e98f --- /dev/null +++ b/apps/video/public/brands/azure.svg @@ -0,0 +1 @@ + diff --git a/apps/video/public/brands/github.svg b/apps/video/public/brands/github.svg new file mode 100644 index 0000000..7c4a574 --- /dev/null +++ b/apps/video/public/brands/github.svg @@ -0,0 +1 @@ + diff --git a/apps/video/public/brands/gitlab.svg b/apps/video/public/brands/gitlab.svg new file mode 100644 index 0000000..529dcf0 --- /dev/null +++ b/apps/video/public/brands/gitlab.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/video/public/brands/linear.svg b/apps/video/public/brands/linear.svg new file mode 100644 index 0000000..75aadea --- /dev/null +++ b/apps/video/public/brands/linear.svg @@ -0,0 +1 @@ + diff --git a/apps/video/remotion.config.ts b/apps/video/remotion.config.ts new file mode 100644 index 0000000..6fd1b55 --- /dev/null +++ b/apps/video/remotion.config.ts @@ -0,0 +1,11 @@ +/** + * Note: When using the Node.JS APIs, the config file + * doesn't apply. Instead, pass options directly to the APIs. + * + * All configuration options: https://remotion.dev/docs/config + */ + +import { Config } from "@remotion/cli/config"; + +Config.setVideoImageFormat("jpeg"); +Config.setOverwriteOutput(true); diff --git a/apps/video/src/Composition.tsx b/apps/video/src/Composition.tsx new file mode 100644 index 0000000..66c900d --- /dev/null +++ b/apps/video/src/Composition.tsx @@ -0,0 +1,356 @@ +import type { CSSProperties, ReactNode } from "react"; +import { + AbsoluteFill, + Composition, + Easing, + Img, + interpolate, + Sequence, + staticFile, + useCurrentFrame, +} from "remotion"; + +const FPS = 30; +const SCENE = 150; +const DURATION = SCENE * 6; + +const palette = { + background: "#080a09", + surface: "#111512", + surfaceRaised: "#171c18", + line: "#303831", + ink: "#f3f5f3", + muted: "#9ca59e", + faint: "#707a72", + primary: "#8c98ff", + success: "#65d8a2", + warning: "#e0b660", +}; + +const clamp = { + extrapolateLeft: "clamp" as const, + extrapolateRight: "clamp" as const, +}; + +function fade(frame: number, duration = SCENE) { + const enter = interpolate(frame, [0, 18], [0, 1], { + ...clamp, + easing: Easing.bezier(0.16, 1, 0.3, 1), + }); + const exit = interpolate(frame, [duration - 18, duration], [1, 0], { + ...clamp, + easing: Easing.in(Easing.cubic), + }); + return enter * exit; +} + +function Background() { + const frame = useCurrentFrame(); + const drift = interpolate(frame, [0, DURATION], [-40, 40], clamp); + + return ( + +
+
+ +
+ + ); +} + +function Brand({ compact = false }: { compact?: boolean }) { + return ( +
+
W
+ + Work SDK + +
+ ); +} + +function SceneFrame({ + eyebrow, + title, + subtitle, + children, +}: { + eyebrow?: string; + title: ReactNode; + subtitle?: string; + children?: ReactNode; +}) { + const frame = useCurrentFrame(); + const visibility = fade(frame); + const lift = interpolate(frame, [0, 24], [30, 0], { + ...clamp, + easing: Easing.bezier(0.16, 1, 0.3, 1), + }); + + return ( + +
+
+ {eyebrow ? {eyebrow} : null} +
{title}
+ {subtitle ?
{subtitle}
: null} +
+ {children} +
+
+ ); +} + +const providers = [ + { name: "GitHub", asset: "brands/github.svg" }, + { name: "GitLab", asset: "brands/gitlab.svg" }, + { name: "Linear", asset: "brands/linear.svg" }, + { name: "Jira", asset: "brands/atlassian.svg" }, + { name: "Azure", asset: "brands/azure.svg" }, +] as const; + +function ProviderRow() { + const frame = useCurrentFrame(); + + return ( +
+ {providers.map((provider, index) => { + const enter = interpolate(frame, [20 + index * 6, 42 + index * 6], [0, 1], { + ...clamp, + easing: Easing.bezier(0.16, 1, 0.3, 1), + }); + return ( +
+ +
+ ); + })} +
+ ); +} + +const panel: CSSProperties = { + border: `1px solid ${palette.line}`, + borderRadius: 18, + background: "rgba(17,21,18,.94)", + boxShadow: "0 28px 70px rgba(0,0,0,.34)", +}; + +function PreparePanel() { + const frame = useCurrentFrame(); + const progress = interpolate(frame, [22, 50], [0, 1], { + ...clamp, + easing: Easing.bezier(0.16, 1, 0.3, 1), + }); + + return ( +
+
+
agent-update.ts
+
const change = await work.
+
{" "}prepareUpdate("ENG-42", {"{"}
+
{" "}state: "completed",
+
{" }"});
+
+
+
● prepared, not written
+
ENG-42
+
Current item read. Revision captured. No provider side effect yet.
+
+
+ ); +} + +function DiffPanel() { + const frame = useCurrentFrame(); + const arrow = interpolate(frame, [35, 70], [0, 1], { + ...clamp, + easing: Easing.bezier(0.16, 1, 0.3, 1), + }); + + return ( +
+
+ PROPOSED CHANGE1 field · 1 warning +
+
+ state + In progress + → + Done +
+
+ !Resolved “completed” to this team's Done state. +
+
+ ); +} + +function RetryPanel() { + const frame = useCurrentFrame(); + const line = interpolate(frame, [30, 78], [0, 1], { + ...clamp, + easing: Easing.inOut(Easing.cubic), + }); + const completed = frame > 72; + + return ( +
+
+
WORKER A
+
claim intent
+
{completed ? "committed" : "in flight…"}
+
+
+
+
deploy:ENG-42
+
+
+
WORKER B
+
same intent
+
{completed ? "replayed receipt" : "waiting…"}
+
+
+ ); +} + +function ReceiptPanel() { + const frame = useCurrentFrame(); + const check = interpolate(frame, [25, 55], [0, 1], { + ...clamp, + easing: Easing.bezier(0.34, 1.3, 0.64, 1), + }); + + return ( +
+
✓
+
+
Committed safely
+
+ actionupdate + replayedfalse + resultUpdateCommitResult +
+
+
+ ); +} + +function Intro() { + return Work across trackers.
Keep one safe API.} subtitle="A typed TypeScript SDK for work that agents can inspect before they commit.">
; +} + +function Providers() { + return Use the trackers
you already have.} subtitle="Switch adapters without rewriting your agent tool.">
; +} + +function Prepare() { + return ; +} + +function Inspect() { + return ; +} + +function Coordinate() { + return ; +} + +function Outro() { + return ( + 5 adapters.
One safe SDK.} subtitle="GitHub · GitLab · Linear · Jira · Azure DevOps"> +
+ +
+ +
+ + ); +} + +export function WorkSdkVideo() { + return ( + + + + + + + + + + ); +} + +export function MyComposition() { + return ( + + ); +} diff --git a/apps/video/src/Root.tsx b/apps/video/src/Root.tsx new file mode 100644 index 0000000..2be4c12 --- /dev/null +++ b/apps/video/src/Root.tsx @@ -0,0 +1,10 @@ +import "./index.css"; +import { MyComposition } from "./Composition"; + +export const RemotionRoot: React.FC = () => { + return ( + <> + + + ); +}; diff --git a/apps/video/src/index.css b/apps/video/src/index.css new file mode 100644 index 0000000..524ff4c --- /dev/null +++ b/apps/video/src/index.css @@ -0,0 +1,9 @@ +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + background: #080a09; +} diff --git a/apps/video/src/index.ts b/apps/video/src/index.ts new file mode 100644 index 0000000..f31c790 --- /dev/null +++ b/apps/video/src/index.ts @@ -0,0 +1,4 @@ +import { registerRoot } from "remotion"; +import { RemotionRoot } from "./Root"; + +registerRoot(RemotionRoot); diff --git a/apps/video/tsconfig.json b/apps/video/tsconfig.json new file mode 100644 index 0000000..4bdf271 --- /dev/null +++ b/apps/video/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "lib": ["es2015"], + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true + }, + "exclude": ["remotion.config.ts"] +} diff --git a/apps/web/app/docs/providers/github/page.tsx b/apps/web/app/docs/providers/github/page.tsx new file mode 100644 index 0000000..d450f13 --- /dev/null +++ b/apps/web/app/docs/providers/github/page.tsx @@ -0,0 +1,46 @@ +import type { Metadata } from "next"; + +import { CodeBlock, DocsCallout, DocsNext, DocsShell } from "@/components/docs-shell"; +import { createPageMetadata } from "@/lib/site"; + +export const metadata: Metadata = createPageMetadata({ + title: "GitHub Issues adapter", + description: "Configure the Work SDK GitHub Issues adapter, understand identifiers, permissions, state mappings, assignees, revisions, and safe writes.", + path: "/docs/providers/github", + keywords: ["GitHub Issues TypeScript SDK", "GitHub issue agent", "GitHub safe writes"], +}); + +const setup = `import { createWorkClient } from "work-sdk"; +import { github } from "work-sdk/github"; + +export const work = createWorkClient({ + adapter: github({ + owner: "octo-org", + repo: "product", + token: process.env.GITHUB_TOKEN!, + }), +});`; + +export default function GitHubProviderPage() { + return +

Configure

The token stays in your process and requests go directly to api.github.com. Inject fetch in tests or when your runtime requires a custom transport.

+

Permissions

Fine-grained personal access tokens need repository access plus read and write permission for Issues. GitHub App installation tokens are preferable for multi-tenant applications because each installation has an explicit repository boundary.

Never expose a GitHub token to a browser or to an untrusted model prompt. Keep the Work SDK client in a server process or isolated tool runtime.

+

Identifiers

idThe decimal issue number as a string, for example "481".
identifierA readable repository-qualified value such as octo-org/product#481.
revisionA deterministic readback fingerprint used for preflight conflict detection.
+

Field mapping

Work SDKGitHubNotes
stateopen / closedCompleted maps to closed; custom workflow states are not available.
assigneeIdsloginsMultiple assignees are supported when the repository allows them.
labelslabel namesUnknown labels are rejected instead of silently created.
kindissuePull requests are excluded from list results.
+

Safe write

0, +});`} label="close-issue.ts" />

The returned receipt is narrowed to UpdateCommitResult. GitHub cannot atomically test an issue revision during the write, so update concurrency is reported as preflight, not atomic.

+

Limits and escape hatches

  • GitHub Projects fields are outside the portable issue contract.
  • Issue types and organization-specific metadata remain available through raw for reads.
  • Repository rules and token permissions can reject an action even when the adapter capability is true.
+ +
; +} diff --git a/apps/web/app/docs/providers/jira/page.tsx b/apps/web/app/docs/providers/jira/page.tsx new file mode 100644 index 0000000..ec7f46d --- /dev/null +++ b/apps/web/app/docs/providers/jira/page.tsx @@ -0,0 +1,47 @@ +import type { Metadata } from "next"; + +import { CodeBlock, DocsCallout, DocsNext, DocsShell } from "@/components/docs-shell"; +import { createPageMetadata } from "@/lib/site"; + +export const metadata: Metadata = createPageMetadata({ + title: "Jira Cloud adapter", + description: "Configure the Work SDK Jira Cloud adapter, discover transitions, map ADF content, priorities, parents, and safe agent writes.", + path: "/docs/providers/jira", + keywords: ["Jira Cloud TypeScript SDK", "Jira agent tools", "Jira transition API"], +}); + +const setup = `import { createWorkClient } from "work-sdk"; +import { jira } from "work-sdk/jira"; + +export const work = createWorkClient({ + adapter: jira({ + baseUrl: "https://example.atlassian.net", + email: "agent@example.com", + apiToken: process.env.JIRA_API_TOKEN!, + projectKey: "ENG", + }), +});`; + +export default function JiraProviderPage() { + return +

Configure

baseUrl is your Jira Cloud tenant root without a trailing REST path. projectKey supplies the default project for creates and search.

+

Authentication

Basic authentication uses the Atlassian account email and an API token, not the account password. OAuth 2.0 applications can provide a bearer token through the adapter's supported auth configuration.

Jira API tokens often span more projects than an individual tool needs. Run the client in a server boundary and use a dedicated account with the narrowest practical project permissions.

+

Workflow transitions

Jira does not update status like an ordinary field. The adapter reads available transitions for the issue, resolves the requested normalized or display state, and then invokes the matching transition ID.

Requested completed→Available transitions→Done / transition 31

If no unambiguous transition exists, preparation or commit fails instead of choosing a similarly named state.

+

Content and fields

Work SDKJira CloudBehavior
descriptionADF documentPlain text is converted conservatively.
commentADF documentThe normalized body remains plain text.
prioritypriority name / IDResolved against tenant metadata.
parentIdparent issue keySupported when the issue type allows it.
kindissue typeMaps task, bug, story, epic, and subtask.
+

Safe write

The receipt is an UpdateCommitResult. The adapter re-reads the issue revision before writing, but Jira's multi-request transition flow still has a race window and is reported as preflight concurrency.

+

Limits

  • Required custom fields cannot be inferred across tenants.
  • Rich ADF structures beyond conservative text conversion remain provider-specific.
  • Workflow validators, conditions, and automation can reject an otherwise valid transition.
+ +
; +} diff --git a/apps/web/app/docs/providers/linear/page.tsx b/apps/web/app/docs/providers/linear/page.tsx new file mode 100644 index 0000000..3f48662 --- /dev/null +++ b/apps/web/app/docs/providers/linear/page.tsx @@ -0,0 +1,40 @@ +import type { Metadata } from "next"; + +import { CodeBlock, DocsCallout, DocsNext, DocsShell } from "@/components/docs-shell"; +import { createPageMetadata } from "@/lib/site"; + +export const metadata: Metadata = createPageMetadata({ + title: "Linear adapter", + description: "Configure the Work SDK Linear adapter with team-aware states, labels, priorities, pagination, and safe agent writes.", + path: "/docs/providers/linear", + keywords: ["Linear TypeScript SDK", "Linear issue agent", "Linear API adapter"], +}); + +const setup = `import { createWorkClient } from "work-sdk"; +import { linear } from "work-sdk/linear"; + +export const work = createWorkClient({ + adapter: linear({ + apiKey: process.env.LINEAR_API_KEY!, + teamId: "team_01JEXAMPLE", + }), +});`; + +export default function LinearProviderPage() { + return +

Configure

Use a personal API key for a single workspace or an OAuth access token for an installed application. teamId provides the default team for creates and state resolution.

+

Teams and identifiers

get accepts a Linear UUID or readable identifier such as ENG-123. Creates need a team from the adapter or input.project. List cursors are opaque GraphQL cursors and must be passed back unchanged.

State and label names are scoped by workspace and team. Resolve them through the adapter rather than hard-coding internal Linear IDs in an agent prompt.

+

States and priorities

Work SDKLinear
backlogWorkflow state type backlog
unstartedWorkflow state type unstarted
startedWorkflow state type started
completedWorkflow state type completed
urgent / high / medium / low / noneNative priority 1–4 or 0

The normalized state is portable. stateName preserves the workspace's exact display name, such as “In review”.

+

Safe write

Prepared changes include the current Linear issue and revision fingerprint. A final read blocks a stale plan before mutation; Linear does not expose an atomic compare-and-set for this adapter, so the guarantee remains preflight.

+

Limits

  • Linear supports one assignee in the portable contract.
  • Custom fields and project milestones remain provider-specific.
  • Workspace permissions and team membership still apply to every request.
+ +
; +} diff --git a/apps/web/app/docs/providers/page.tsx b/apps/web/app/docs/providers/page.tsx index a1ce154..40ec2a9 100644 --- a/apps/web/app/docs/providers/page.tsx +++ b/apps/web/app/docs/providers/page.tsx @@ -22,10 +22,10 @@ export default function ProvidersPage() {

Capability matrix

CapabilityGitHubGitLabLinearJiraAzure DevOps
Create / updateYesYesYesYesYes
CommentsMarkdownMarkdownMarkdownADFMarkdown
Custom statesNoNoYesYesYes
PrioritiesNo universal fieldNo universal fieldNativeNative1–4 mapping
Multiple assigneesYesOpt-inNoNoNo
Parent linksNoNoYesYesYes
SearchNot in adapterREST searchGraphQL filtersJQLWIQL
ConcurrencyReadback revisionReadback revisionReadback revisionReadback revisionNative /rev test
-

GitHub Issues

The adapter excludes pull requests from issue results, maps open/closed reasons, verifies fields through readback, and supports multiple assignees. GitHub Projects fields are intentionally outside the portable issue contract.

+

GitHub Issues

The adapter excludes pull requests from issue results, maps open/closed reasons, verifies fields through readback, and supports multiple assignees. GitHub Projects fields are intentionally outside the portable issue contract.

GitLab

The REST v4 adapter supports GitLab.com and Self-Managed. It validates labels before writes, requires explicit mappings for non-native issue kinds, and keeps tier-dependent multiple assignees opt-in.

-

Linear

Linear uses cursor pagination, one assignee, native priorities, and team-specific workflow states. The adapter resolves state and label names against provider metadata before a mutation.

-

Jira Cloud

Jira status changes are transitions, not ordinary field updates. The adapter discovers available transitions and converts conservative text content to Atlassian Document Format.

+

Linear

Linear uses cursor pagination, one assignee, native priorities, and team-specific workflow states. The adapter resolves state and label names against provider metadata before a mutation.

+

Jira Cloud

Jira status changes are transitions, not ordinary field updates. The adapter discovers available transitions and converts conservative text content to Atlassian Document Format.

Azure DevOps

Azure DevOps uses WIQL for discovery and JSON Patch for mutation. Custom processes can redefine state and work-item type names, so the adapter exposes explicit normalization maps.

Provider escape hatches

Every normalized item, user, project, and comment can retain raw. Use it for reads that are not portable. For provider-specific writes outside the adapter contract, build a narrow custom tool instead of mutating raw and pretending the operation is normalized.

Capability flags describe adapter-level support. Tenant workflow rules and user permissions can still reject an individual operation.

; diff --git a/apps/web/app/docs/reference/client/page.tsx b/apps/web/app/docs/reference/client/page.tsx index 33d40a1..1648faa 100644 --- a/apps/web/app/docs/reference/client/page.tsx +++ b/apps/web/app/docs/reference/client/page.tsx @@ -21,8 +21,21 @@ export default function ClientReferencePage() {

createWorkClient

adapter is required. The default idempotency store is process-local memory. now exists for deterministic testing and should normally be omitted.

Read methods

get(id, options?)

`} />

Returns one normalized item or throws WorkNotFoundError. IDs are adapter-native: a GitHub issue number, Linear ID or identifier, Jira key, or Azure work-item ID.

list(input?, options?)

>`} />

Returns an opaque provider cursor. Never construct or parse cursors; pass nextCursor back unchanged.

Prepare methods

prepareCreate(input)Plan a new item without writing it.prepareUpdate(id, input, options?)Read current state and calculate a changed-field diff.prepareComment(id, input, options?)Read the target and plan a comment.

Create plans do not have an expected revision. Update and comment plans capture the current item and opaque revision.

-

commit(change, options?)

`} />

Commit validates provider identity, fingerprint integrity, warning acknowledgement, idempotency state, and revision before calling the adapter.

If a mutation starts and the provider outcome becomes uncertain, Work SDK stores an ambiguous state and throws WorkAmbiguousCommitError. Reconcile the provider result before another write; never blind-retry that key.

-

Core types

TypePurpose
WorkItemNormalized item plus provider state name, opaque revision, URL, and optional raw payload.
WorkCommentNormalized body, author, timestamps, and raw payload.
PreparedWorkChangeSerializable, fingerprinted plan for one create, update, or comment.
CommitResultReceipt containing the resulting item, optional comment, replay flag, and commit time.
WorkPage<T>Items plus an optional opaque next cursor.
WorkWarningStructured provider limitation or lossy/ambiguous mapping notice.
+

commit(change, options?)

( + change: TChange, + options?: { + idempotencyKey?: string; + acceptWarnings?: boolean; + signal?: AbortSignal; + }, +): Promise>`} />

Commit validates provider identity, fingerprint integrity, warning acknowledgement, idempotency state, and revision before calling the adapter. The receipt follows the prepared action: create plans return CreateCommitResult, updates return UpdateCommitResult, and comments return CommentCommitResult with a required comment.

If a mutation starts and the provider outcome becomes uncertain, Work SDK stores an ambiguous state and throws WorkAmbiguousCommitError. Reconcile the provider result before another write; never blind-retry that key.

+

Core types

TypePurpose
WorkItemNormalized item plus provider state name, opaque revision, URL, and optional raw payload.
WorkCommentNormalized body, author, timestamps, and raw payload.
PreparedWorkChangeSerializable, fingerprinted plan for one create, update, or comment.
CommitResultDiscriminated union of create, update, and comment receipts with replay flag and commit time.
CommitResultFor<T>Maps a prepared change type to its exact action-specific receipt.
WorkPage<T>Items plus an optional opaque next cursor.
WorkWarningStructured provider limitation or lossy/ambiguous mapping notice.

Capabilities

work.capabilities is an immutable snapshot. Inspect it when building tools or UIs so unsupported actions are absent rather than offered and rejected later.

optimisticConcurrency remains as a deprecated compatibility Boolean. New code should use the action-specific concurrency object.

IdempotencyStore

;\n complete(key: string, leaseId: string, result: CommitResult): MaybePromise;\n abandon(key: string, leaseId: string, outcome: "retryable" | "ambiguous"): MaybePromise;\n}`} />

The client prefixes keys with the provider. acquire must be atomic across every worker using the store; use a transaction, compare-and-swap, unique insert, or conditional put. A durable get followed by set is not sufficient.

completedReplay the stored receipt without a provider call.
in-flightThrow WorkInFlightError; another worker owns the claim.
ambiguousThrow WorkAmbiguousCommitError; reconcile before retrying.
diff --git a/apps/web/app/docs/reference/errors/page.tsx b/apps/web/app/docs/reference/errors/page.tsx index 84f0126..6faa723 100644 --- a/apps/web/app/docs/reference/errors/page.tsx +++ b/apps/web/app/docs/reference/errors/page.tsx @@ -48,7 +48,7 @@ export default function ErrorsPage() {

Error hierarchy

ClassCodeTypical response
WorkAuthenticationErrorauthenticationRefresh or replace credentials; do not blind-retry.
WorkAuthorizationErrorauthorizationRequest scope/permission or remove the action.
WorkNotFoundErrornot_foundVerify target and visibility.
WorkRateLimitErrorrate_limitRetry after the supplied delay with jitter.
WorkConflictErrorconflictRe-read, prepare a new plan, request approval again.
WorkInFlightErrorin_flightWait for the current claim owner, then reuse the same key.
WorkAmbiguousCommitErrorambiguousReconcile provider state; never blind-retry.
WorkUnsupportedErrorunsupportedChange the tool schema or provider strategy.
WorkValidationErrorvalidationFix input; retrying unchanged input is wrong.
WorkErrorprovider / networkInspect cause and provider policy.

Shared fields

Typed handling

-

Retry policy

  • Rate limit: retry after retryAfterMs, add jitter, preserve the idempotency key.
  • In flight: wait for the claim owner and reuse the same key.
  • Ambiguous: do not retry; reconcile the provider and durable record.
  • Conflict: never retry the same plan; prepare and approve again.
  • Validation/unsupported: do not retry unchanged input.
  • Authentication: refresh credentials before another attempt.

A timeout cannot prove whether the remote provider committed a write. v0.4 records that uncertainty explicitly and blocks another mutation until application reconciliation resolves it.

+

Retry policy

  • Rate limit: retry after retryAfterMs, add jitter, preserve the idempotency key.
  • In flight: wait for the claim owner and reuse the same key.
  • Ambiguous: do not retry; reconcile the provider and durable record.
  • Conflict: never retry the same plan; prepare and approve again.
  • Validation/unsupported: do not retry unchanged input.
  • Authentication: refresh credentials before another attempt.

A timeout cannot prove whether the remote provider committed a write. Work SDK records that uncertainty explicitly and blocks another mutation until application reconciliation resolves it.

Logging and redaction

Log class, code, provider, status, retry delay, and a correlation ID. Treat details and cause as potentially sensitive because providers can echo field values or request context. Never log adapter options or authorization headers.

; diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 774f268..2f250d6 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1,31 +1,31 @@ :root { - --bg: #f7f7f4; - --surface: #ffffff; - --surface-raised: #fbfbf8; - --ink: #121413; - --muted: #636964; - --faint: #6f756f; - --line: #dfe2dd; - --line-strong: #c7cbc6; - --primary: #4d5ee8; - --primary-dark: #3344cf; - --primary-soft: #eceeff; - --success: #1a8f60; - --success-soft: #e2f8ed; - --warning: #9d6610; - --warning-soft: #fff4d8; + --bg: #080a09; + --surface: #0e1110; + --surface-raised: #131614; + --ink: #f3f5f3; + --muted: #a3aaa5; + --faint: #7f8881; + --line: #242925; + --line-strong: #39413b; + --primary: #8c98ff; + --primary-dark: #b2baff; + --primary-soft: #1d2246; + --success: #65d8a2; + --success-soft: #143429; + --warning: #e0b660; + --warning-soft: #342815; --danger: #c34444; --code-bg: #111413; --code-ink: #e7ece8; - --contrast-bg: #121413; + --contrast-bg: #111512; --contrast-ink: #f4f7f5; --contrast-muted: #aeb5b0; --contrast-line: #414743; - --primary-button-bg: #121413; - --primary-button-ink: #ffffff; - --primary-button-hover: #272b28; - --logo-cut: #ffffff; - --shadow-lg: 0 28px 80px rgba(26, 29, 27, 0.13), 0 2px 7px rgba(26, 29, 27, 0.08); + --primary-button-bg: #f1f4f2; + --primary-button-ink: #101311; + --primary-button-hover: #dfe5e1; + --logo-cut: #080a09; + --shadow-lg: 0 32px 100px rgba(0, 0, 0, .38), 0 2px 7px rgba(0, 0, 0, .34); --shell: 1220px; } @@ -35,7 +35,7 @@ body { margin: 0; overflow-x: clip; background: var(--bg); color: var(--ink); fo button, input { font: inherit; } a { color: inherit; text-decoration: none; } svg { display: block; } -::selection { background: #cdd3ff; color: #121413; } +::selection { background: #4651a7; color: white; } .shell { width: min(var(--shell), calc(100% - 48px)); margin-inline: auto; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } @@ -51,7 +51,7 @@ svg { display: block; } .nav-links { display: flex; align-items: center; gap: 26px; margin-left: auto; color: var(--muted); font-size: 13px; font-weight: 560; } .nav-links a, .footer-links a { transition: color .18s ease; } .nav-links a:hover, .footer-links a:hover { color: var(--ink); } -.install-pill { display: inline-flex; align-items: center; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); box-shadow: inset 0 1px 0 #fff; color: #363b37; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; padding: 8px 11px; } +.install-pill { display: inline-flex; align-items: center; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); color: #c7cdc8; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; padding: 8px 11px; } .prompt { color: var(--success); margin-right: 3px; } .hero { display: grid; grid-template-columns: minmax(0, .84fr) minmax(600px, 1.16fr); gap: 52px; min-height: 740px; align-items: center; padding-block: 84px 76px; } @@ -75,6 +75,25 @@ svg { display: block; } .hero-proof dt { font-weight: 690; } .hero-proof dd { margin: 0; color: var(--muted); } +.hero.hero-centered { position: relative; display: flex; min-height: 650px; align-items: flex-start; justify-content: center; overflow: hidden; padding-block: 95px 0; text-align: center; } +.hero-centered .hero-copy { position: relative; z-index: 2; max-width: 900px; } +.hero-centered .eyebrow { justify-content: center; } +.hero-centered h1 { max-width: none; margin-top: 25px; font-size: clamp(56px, 7vw, 94px); line-height: .94; } +.hero-centered h1 span { color: var(--ink); } +.hero-centered .hero-summary { max-width: 690px; margin-inline: auto; font-size: 16px; } +.hero-centered .hero-actions { justify-content: center; } +.hero-metrics { display: flex; justify-content: center; gap: 0; margin: 38px auto 0; } +.hero-metrics div { min-width: 126px; border-left: 1px solid var(--line); padding: 0 24px; } +.hero-metrics div:last-child { border-right: 1px solid var(--line); } +.hero-metrics dt { font-size: 18px; font-weight: 720; } +.hero-metrics dd { margin: 5px 0 0; color: var(--faint); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 9px; letter-spacing: .07em; text-transform: uppercase; } +.terrain { position: absolute; inset: auto 0 0; height: 310px; opacity: .72; pointer-events: none; } +.terrain svg { width: 100%; height: 100%; } +.terrain-fill { fill: url("#terrain-fill"); } +.terrain-glow { fill: #6572ff; filter: url("#terrain-glow"); opacity: .16; } +.terrain-line { fill: none; stroke: #8b96ff; stroke-width: 1.15; vector-effect: non-scaling-stroke; } +.terrain-line.subtle { stroke: #66706a; stroke-dasharray: 3 8; opacity: .45; } + .workbench { min-width: 0; overflow: hidden; border: 1px solid #2e3430; border-radius: 15px; background: var(--code-bg); box-shadow: var(--shadow-lg); color: var(--code-ink); } .workbench-topbar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; min-height: 47px; border-bottom: 1px solid #2a2f2c; padding: 0 14px; } .traffic-lights { display: flex; gap: 6px; } @@ -128,6 +147,10 @@ svg { display: block; } .provider-list .brand-logo { width: 18px; height: 18px; } .provider-note { justify-self: end; } +.demo-section { padding-top: 104px; } +.demo-section .section-heading { max-width: 760px; } +.demo-section .workbench { max-width: 1080px; margin: 56px auto 0; } + .section { padding-block: 116px; } .section-heading { max-width: 660px; } .section-heading.centered { margin-inline: auto; text-align: center; } @@ -257,12 +280,31 @@ tbody th { font-weight: 570; } /* Documentation */ .docs-layout { display: grid; grid-template-columns: 230px minmax(0, 740px) 1fr; gap: 55px; align-items: start; padding-block: 68px 110px; } -.docs-sidebar { position: sticky; top: 100px; } +.docs-sidebar { position: sticky; top: 100px; min-width: 0; } +.docs-search-trigger { display: grid; width: 100%; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; margin-bottom: 23px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--muted); cursor: pointer; padding: 9px 10px; text-align: left; } +.docs-search-trigger:hover { border-color: var(--line-strong); color: var(--ink); } +.docs-search-trigger kbd { border: 1px solid var(--line); border-radius: 4px; background: var(--bg); color: var(--faint); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 8px; padding: 3px 5px; } +.docs-search-backdrop { position: fixed; inset: 0; z-index: 100; display: grid; place-items: start center; background: rgba(0, 0, 0, .68); backdrop-filter: blur(8px); padding: min(14vh, 120px) 18px 18px; } +.docs-search-dialog { width: min(610px, 100%); overflow: hidden; border: 1px solid var(--line-strong); border-radius: 13px; background: #101311; box-shadow: 0 30px 100px rgba(0, 0, 0, .55); } +.docs-search-field { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; border-bottom: 1px solid var(--line); } +.docs-search-dialog input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--ink); padding: 20px; font-size: 15px; } +.docs-search-dialog input::placeholder { color: var(--faint); } +.docs-search-field button { margin-right: 13px; border: 1px solid var(--line-strong); border-radius: 5px; background: var(--surface); color: var(--faint); cursor: pointer; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 9px; padding: 5px 7px; } +.docs-search-field button:hover { color: var(--ink); } +.docs-search-results { max-height: min(56vh, 480px); overflow-y: auto; padding: 7px; } +.docs-search-results a { display: flex; align-items: center; justify-content: space-between; border-radius: 8px; padding: 12px 13px; } +.docs-search-results a:hover { background: var(--surface-raised); } +.docs-search-results small { color: var(--faint); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 9px; text-transform: uppercase; } +.docs-search-results p { color: var(--faint); padding: 14px; font-size: 13px; } .docs-sidebar p { margin: 24px 0 10px; color: var(--faint); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 9px; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } .docs-sidebar p:first-child { margin-top: 0; } .docs-sidebar a { display: block; border-radius: 6px; color: var(--muted); font-size: 12px; padding: 7px 9px; } .docs-sidebar a:hover { background: var(--surface); color: var(--ink); } +.docs-sidebar a[aria-current="page"] { background: var(--surface-raised); color: var(--ink); font-weight: 680; } +.docs-sidebar .docs-search-results a { display: flex; align-items: center; justify-content: space-between; color: var(--muted); font-size: 13px; padding: 12px 13px; } +.docs-sidebar .docs-search-results a:hover { background: var(--surface-raised); color: var(--ink); } .docs-nav-group { margin: 0; } +.docs-nav-scroll { min-width: 0; } .docs-content { min-width: 0; } .docs-content .breadcrumb { color: var(--faint); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 10px; } .docs-content h1 { margin: 22px 0 17px; font-size: 52px; letter-spacing: -.055em; } @@ -375,7 +417,9 @@ tbody th { font-weight: 570; } .article-rules li, .article-cta { grid-template-columns: 1fr; gap: 8px; } .article-body pre { padding: 20px; font-size: 11px; } .docs-layout { grid-template-columns: 1fr; padding-top: 34px; } - .docs-sidebar { position: static; display: flex; overflow-x: auto; gap: 4px; border-bottom: 1px solid var(--line); padding-bottom: 12px; } + .docs-sidebar { position: static; display: block; overflow: visible; border-bottom: 0; padding-bottom: 12px; } + .docs-search-trigger { margin-bottom: 12px; } + .docs-nav-scroll { display: flex; width: 100%; max-width: 100%; overflow-x: auto; gap: 4px; border-bottom: 1px solid var(--line); padding-bottom: 12px; scrollbar-width: thin; } .docs-nav-group { display: flex; flex: none; } .docs-sidebar p { display: none; } .docs-sidebar a { flex: none; } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 3c4dfbd..6cc790f 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -64,11 +64,8 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - colorScheme: "light dark", - themeColor: [ - { media: "(prefers-color-scheme: light)", color: "#f7f7f4" }, - { media: "(prefers-color-scheme: dark)", color: "#0b0d0c" }, - ], + colorScheme: "dark", + themeColor: "#080a09", }; export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) { diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index ab323f5..79d4479 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -95,24 +95,39 @@ export default function HomePage() {