mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
* proposal: add devin desktop support
* feat(adapters): add devin desktop command adapter
- Create new Devin Desktop adapter for .devin/workflows/opsx-<id>.md
- Register adapter in CommandAdapterRegistry
- Export adapter from adapters index
- Update docs/supported-tools.md with Devin Desktop entry
- Add 'devin' to available tool IDs list
Devin Desktop uses the same Cascade workflow system as Windsurf,
making it a natural migration path for existing users.
* fix(config): add devin desktop to AI_TOOLS
Add Devin Desktop entry to AI_TOOLS configuration so that:
- getToolsWithSkillsDir() includes 'devin' as a valid tool ID
- getWorkspaceSkillToolIds() returns 'devin' in the list
- parseWorkspaceSkillToolsValue() accepts 'devin' as valid input
- openspec init --tools devin works correctly
This fixes validation failures where 'devin' was documented in
docs/supported-tools.md but not recognized by validation functions
that derive valid IDs from AI_TOOLS.
* fix(devin-adapter): escape implicit YAML scalars in frontmatter
Update escapeYamlValue to detect and quote implicit YAML scalars that
would be coerced by parsers:
- Booleans: true, false, yes, no, on, off
- Null variants: null, ~
- Numbers: integers, floats, exponentials, hex (0x), octal (0o)
- Edge cases: standalone dash (-) and dot (.)
This ensures values like 'true', '123', 'null' remain strings in YAML
frontmatter instead of being interpreted as booleans, numbers, or nulls.
Preserves existing escaping logic for special characters and newlines.
* test(devin-adapter): add comprehensive tests for Devin Desktop adapter
Add test coverage for the Devin Desktop adapter including:
- Command reference transformation from colon to hyphen syntax
- YAML frontmatter escaping for special characters and implicit scalars
- File path generation for workflows
- Integration with available tools detection
- Init and update command workflows
* Add cross-platform testcase.
* fix(devin): refresh deltas against canonical specs and point skills at skills
Addresses the two release blockers on this PR.
Archive: the change's MODIFIED blocks were written against an older
canonical `cli-init`, so `openspec archive add-devin-desktop-support`
aborted rather than merging. The deltas are regenerated from the current
canonical specs (cli-init `Skill Generation` + `Slash Command
Generation`, cli-update `Slash Command Updates`, and a new
`ai-tool-paths` delta for the `.devin` skillsDir), each restating every
existing scenario so archive is purely additive.
Invocation syntax: only Devin Desktop reads `.devin/workflows/`, so a
`/opsx-*` workflow reference is dead text on Devin Local, which supports
skills only. Devin now takes the skill-reference transformer, so skill
bodies and the getting-started hint say `/openspec-*`. Workflow bodies
keep hyphen references, applied by devinAdapter itself.
The adapter also drops its private copy of escapeYamlValue /
formatTagsArray in favor of the shared helpers main centralized in
#1447, which quote unconditionally and escape control characters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(devin): correct commands-only hint, fill doc gaps, cover both surfaces
Follow-up from adversarial review of the previous commit.
The devin special case in getTransformerForTool was unconditional, so
under commands-only delivery — where `.devin/skills/` is deleted — the
getting-started hint named `/openspec-propose`, a skill that is not on
disk. Devin now takes the skill transformer only when skills are
generated, and the hyphen form otherwise. The cli-init delta records the
fallback, and a unit test pins all three delivery modes.
Docs: `devin` was missing from the `--tools` list in docs/cli.md (which
mirrors the list supported-tools.md already had) and from the
command-syntax tables in docs/commands.md and docs/how-commands-work.md.
The supported-tools row gains a footnote citing Cognition's docs for the
`.windsurf/` -> `.devin/` move and the Devin Local workflow gap.
Tests: init and update now assert both surfaces — workflows carry
`/opsx-*`, skills carry `/openspec-*`, neither carries `/opsx:` — and
update checks the seeded skill was actually refreshed. Adds the negative
detection case. Drops three devin-only YAML assertions that duplicated,
less rigorously, the registry-derived escaping matrix that now enrolls
devin automatically.
Also reverts an unrelated zcode export and lingma reorder that a merge
resolution had pulled into adapters/index.ts. zcodeAdapter is registered
but missing from that barrel on main; that is a pre-existing gap and
belongs in its own change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(devin): name the right command in the profile migration notice
The profile-migration notice printed by both `init` and `update` hardcoded
`/opsx:propose` for every adapter-backed tool. Devin registers no such
command on any surface — its workflows answer to `/opsx-propose` and its
skills to `/openspec-propose` — so an upgrading Devin user was told to run
something that does not exist:
Migrated: custom profile with 6 workflows
New in this version: /opsx:propose.
The reference now goes through getTransformerForTool, the same call
init.ts already makes for the getting-started hint. Devin prints
`/openspec-propose`; opencode and the other filename-invoked tools are
corrected to `/opsx-propose` as a side effect; claude is unchanged.
Also corrects two inherited false claims in the cli-update delta — Devin
workflows carry no OpenSpec markers, and update writes every profile
workflow rather than only refreshing files that already exist, which the
PR's own test demonstrates. Qualifies the supported-tools footnote for
commands-only delivery, and strips trailing whitespace.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(devin): keep the cli-update delta in step with the canonical spec
The delta restates the whole 'Slash Command Updates' requirement, and its
copy of the OpenCode scenario predated #1471 — archiving it would have
quietly reverted the spec to calling the hyphen rewrite an OpenCode special
case, the hand-maintained framing #1471 removed. Archive on a scratch copy
is now purely additive.
Also point tasks.md at the generator rather than the deleted
transformToHyphenCommands, and enroll devin in the pure-formatter tripwire —
it is the one adapter whose private body transform was just removed, so it
is the likeliest to have it re-added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(adapters): follow the Windsurf rename to Devin Desktop, with migration
Windsurf was rebranded to Devin Desktop on 2026-06-02 and its config
directory moved: `.devin/` is the preferred read+write location, `.windsurf/`
a legacy read-only fallback. Devin Local does not read `.windsurf/` at all,
so an existing Windsurf user's OpenSpec files are invisible to it.
Carrying `devin` as a second tool id alongside `windsurf` would list one
product twice and leave upgraders with two parallel installs — `openspec
update` even told them to create the second one ("Detected new tool: Devin
Desktop"). This follows the rename instead, as the repo already did for
Kimi CLI -> Kimi Code:
- `windsurf` is retired as a tool id; `devin` takes its place, with
`detectionPaths: ['.devin', '.windsurf']` so pre-rebrand projects are
still recognized. The Windsurf adapter is replaced, not duplicated.
- `TOOL_ID_ALIASES` keeps `--tools windsurf` resolving, so existing setup
scripts and CI keep working; they now configure `.devin/`.
- OpenSpec-managed skills (`openspec-*`) and command files (`opsx-*`) under
`.windsurf/` move to `.devin/`. The kimi migration handled skills only;
command files now move too, deriving the legacy path from the adapter's
own getFilePath rather than hard-coding a layout.
- The move is offered, not taken: nothing on disk distinguishes a user who
took the rebrand from one still on a pre-rebrand Windsurf build that reads
only `.windsurf/`. `openspec update` explains the rename and asks; --force
and non-interactive runs migrate; declining leaves every file untouched and
says what that costs. Files the user wrote are never moved.
Also gives Devin its own row in the authoritative invocation table — the
catch-all row claimed `/opsx-<id>` for both agents, which is wrong for Devin
Local.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(devin): stop the migration from deleting anything it does not own
An adversarial pass found two ways the move destroyed files.
Symlinked roots wiped the install. `ln -s .devin .windsurf` is a realistic
way to straddle the rebrand, and it makes source and destination the same
file — so the "destination exists, drop the legacy copy" branch deleted the
only copy. Twelve generated files, gone, and not regenerated: the wipe
happens before tool detection, so update then reported no configured tools.
Both roots are now realpath'd and a self-move is skipped.
User content inside an OpenSpec-managed path was deleted. The same branch
rm -rf'd the whole legacy skill directory, taking a hand-written
reference.md beside SKILL.md with it, and deleted a legacy command file even
when the user had edited it. Now only SKILL.md is removed from a skill
directory, and a command file is removed only when byte-identical to the
one that survives — an edit is left where it is.
Also: declining the move stranded the user. `update` then printed "No
configured tools found. Run openspec init", which is wrong — the project is
configured, just in the directory OpenSpec no longer writes. It now says so
and how to resume. A closed stdin during the prompt aborted the whole
update; it is treated as a decline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(devin): add a changeset for the Windsurf rename and migration
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(devin): move only SKILL.md, never the skill directory around it
alfred caught a data-loss path the earlier fix missed. When the destination
did not yet exist, migration renamed the whole legacy skill directory into
`.devin/` — carrying any file the user kept beside `SKILL.md` with it. That
destination is a directory OpenSpec owns and removes on its own: under
commands-only delivery, or for a workflow outside the active profile. So the
move handed the user's file to a later rm and it vanished.
Reproduced on `d94af8b`: with `delivery: commands`, a `reference.md` beside a
legacy `SKILL.md` was gone after `openspec update`.
Only `SKILL.md` crosses now, in both branches; anything else stays under the
legacy root, and the legacy directory is still removed when the move leaves
it empty. Regression tests cover the commands-only and deselected-workflow
cases and both fail against the previous code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(devin): treat an edited skill the way an edited command is already treated
A final adversarial pass found the two paths disagreeing. When both roots
held the same file with different content, the command path compared bytes
and kept the user's version; the skill path deleted it with no comparison —
so one `openspec update` destroyed an edited SKILL.md while preserving an
edited opsx-*.md in the same project.
Both now share one `classifyManagedFile` rule: move when the destination is
empty, drop the legacy copy only when byte-identical, otherwise leave it.
Anything left behind is reported, so a user who customized a file knows two
copies exist rather than discovering it later.
Note on the other finding from that pass: OpenSpec regenerating or pruning
the files it owns is long-standing behavior, not something this PR
introduces. Verified against main — an edited SKILL.md under a deselected
workflow, and an edited selected skill and command, are all destroyed by
`openspec update` on 9a937cb too. No regression, so left alone here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(devin): report divergent legacy files even when nothing is movable
collectLegacyToolMigrations only returned a result when something moved, so
a project where EVERY legacy file differs from its counterpart produced no
output at all — two divergent copies and not a word about them. That is the
one case where the report matters most, since it is entirely made of files
the migration deliberately refused to touch.
Kept-only results are retained now. Callers gate on hasMovableContent(), so
a kept-only result reports what was left without offering to move nothing
and without claiming a migration that did not happen.
Also reworded the notice. A legacy file can differ because the user edited
it or simply because an older OpenSpec generated it, so it no longer asserts
an edit — it states that nothing was overwritten and leaves the user to
compare the two copies.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(devin): stop matching the unrelated profile-migration line
The kept-only regression asserted no line matched /Migrated\s*:/, which also
matches OpenSpec's profile migration message, "Migrated: custom profile with
N workflows". That line only prints when the global config has no profile
yet — true on a fresh CI runner, false on a developer machine that has run
OpenSpec before — so the test passed locally and failed on all three CI
platforms.
Now matched on the directory arrow, ".windsurf → .devin", which is specific
to a migration report and unaffected by config state.
Reproduced both ways with an empty XDG_CONFIG_HOME: the old assertion fails
there, the new one passes, and the full suite is green under CI's
XDG_CONFIG_HOME + VITEST_MAX_WORKERS=4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
652 lines
22 KiB
TypeScript
652 lines
22 KiB
TypeScript
import Link from 'next/link';
|
|
import {
|
|
ArrowRight,
|
|
Boxes,
|
|
Check,
|
|
Clock,
|
|
Compass,
|
|
FileText,
|
|
GitBranch,
|
|
Hammer,
|
|
Archive,
|
|
Layers,
|
|
ListChecks,
|
|
Share2,
|
|
Sparkles,
|
|
} from 'lucide-react';
|
|
import { docsRoute, links } from '@/lib/shared';
|
|
|
|
export default function HomePage() {
|
|
return (
|
|
<main className="flex flex-col">
|
|
<Hero />
|
|
<Philosophy />
|
|
<ToolStrip />
|
|
<TwoFolders />
|
|
<Anatomy />
|
|
<FiveIdeas />
|
|
<TheLoop />
|
|
<Teams />
|
|
<Why />
|
|
<Comparison />
|
|
<FinalCta />
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function Hero() {
|
|
return (
|
|
<section className="relative overflow-hidden border-b border-fd-border">
|
|
<div
|
|
className="absolute inset-0 -z-10"
|
|
style={{
|
|
background:
|
|
'radial-gradient(ellipse at top, color-mix(in oklab, var(--color-fd-primary) 9%, transparent), transparent 60%)',
|
|
}}
|
|
/>
|
|
<div className="mx-auto flex max-w-5xl flex-col items-center px-4 py-20 text-center sm:py-28">
|
|
<span className="mb-5 inline-flex items-center gap-2 rounded-full border border-fd-border bg-fd-card px-3 py-1 text-xs font-medium text-fd-muted-foreground">
|
|
<Sparkles className="size-3.5 text-fd-primary" />
|
|
The lightweight spec layer for AI coding
|
|
</span>
|
|
<h1 className="max-w-3xl text-balance text-4xl font-bold tracking-tight sm:text-6xl">
|
|
Agree first.
|
|
<br />
|
|
Then build confidently.
|
|
</h1>
|
|
<p className="mt-6 max-w-2xl text-balance text-lg text-fd-muted-foreground">
|
|
OpenSpec is a tiny agreement layer between you and your AI. You write
|
|
down what a change should do, the AI drafts the details, you both look
|
|
at the same plan, and <em>only then</em> does code get written. No more
|
|
discovering halfway through that it built the wrong thing.
|
|
</p>
|
|
<div className="mt-9 flex flex-col gap-3 sm:flex-row">
|
|
<Link
|
|
href={`${docsRoute}/getting-started`}
|
|
className="inline-flex items-center justify-center gap-2 rounded-lg bg-fd-primary px-5 py-2.5 text-sm font-semibold text-fd-primary-foreground transition-opacity hover:opacity-90"
|
|
>
|
|
Get started <ArrowRight className="size-4" />
|
|
</Link>
|
|
<Link
|
|
href={links.github}
|
|
className="inline-flex items-center justify-center gap-2 rounded-lg border border-fd-border bg-fd-card px-5 py-2.5 text-sm font-semibold transition-colors hover:bg-fd-accent"
|
|
>
|
|
<GitBranch className="size-4" /> Star on GitHub
|
|
</Link>
|
|
</div>
|
|
<Terminal />
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function Terminal() {
|
|
return (
|
|
<div className="mt-14 w-full max-w-2xl text-left">
|
|
<div className="overflow-hidden rounded-xl border border-fd-border bg-fd-card shadow-sm">
|
|
<div className="flex items-center gap-1.5 border-b border-fd-border px-4 py-3">
|
|
<span className="size-3 rounded-full bg-red-400/80" />
|
|
<span className="size-3 rounded-full bg-yellow-400/80" />
|
|
<span className="size-3 rounded-full bg-green-400/80" />
|
|
<span className="ml-3 text-xs text-fd-muted-foreground">
|
|
your-project — AI chat
|
|
</span>
|
|
</div>
|
|
<pre className="overflow-x-auto p-4 text-sm leading-relaxed">
|
|
<code>
|
|
<span className="text-fd-primary">/opsx:propose</span> add-dark-mode
|
|
{'\n'}
|
|
<span className="text-fd-muted-foreground">
|
|
{' '}✓ proposal.md — why we are doing this, what changes{'\n'}
|
|
{' '}✓ specs/ — requirements and scenarios{'\n'}
|
|
{' '}✓ design.md — technical approach{'\n'}
|
|
{' '}✓ tasks.md — implementation checklist{'\n'}
|
|
</span>
|
|
{'\n'}
|
|
<span className="text-fd-primary">/opsx:apply</span>
|
|
{'\n'}
|
|
<span className="text-fd-muted-foreground">
|
|
{' '}✓ working through tasks, checking each one off…{'\n'}
|
|
</span>
|
|
{'\n'}
|
|
<span className="text-fd-primary">/opsx:archive</span>
|
|
{'\n'}
|
|
<span className="text-fd-muted-foreground">
|
|
{' '}✓ specs updated · change filed away · ready for the next one
|
|
</span>
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const PHILOSOPHY = [
|
|
['fluid', 'not rigid'],
|
|
['iterative', 'not waterfall'],
|
|
['easy', 'not complex'],
|
|
['brownfield', 'not just greenfield'],
|
|
];
|
|
|
|
function Philosophy() {
|
|
return (
|
|
<section className="border-b border-fd-border bg-fd-card/30">
|
|
<div className="mx-auto grid max-w-5xl grid-cols-2 gap-px px-4 py-3 sm:grid-cols-4">
|
|
{PHILOSOPHY.map(([a, b]) => (
|
|
<div key={a} className="px-4 py-4 text-center">
|
|
<div className="text-lg font-semibold tracking-tight">{a}</div>
|
|
<div className="text-sm text-fd-muted-foreground">{b}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function TwoFolders() {
|
|
return (
|
|
<section className="mx-auto max-w-5xl px-4 py-20">
|
|
<div className="mx-auto max-w-2xl text-center">
|
|
<h2 className="text-3xl font-bold tracking-tight">
|
|
The whole idea, in two folders
|
|
</h2>
|
|
<p className="mt-4 text-fd-muted-foreground">
|
|
OpenSpec lives in one <code className="text-fd-primary">openspec/</code>{' '}
|
|
directory in your repo. Two folders inside it carry the entire mental
|
|
model.
|
|
</p>
|
|
</div>
|
|
<div className="mt-12 grid gap-6 md:grid-cols-2">
|
|
<div className="rounded-xl border border-fd-border bg-fd-card p-6">
|
|
<div className="mb-3 inline-flex size-10 items-center justify-center rounded-lg bg-fd-primary/10 text-fd-primary">
|
|
<FileText className="size-5" />
|
|
</div>
|
|
<h3 className="text-lg font-semibold">
|
|
<code>specs/</code> — what is true
|
|
</h3>
|
|
<p className="mt-2 text-sm text-fd-muted-foreground">
|
|
The source of truth. Plain-language requirements and scenarios that
|
|
describe how your system behaves <em>right now</em>, organized by
|
|
domain. This is the agreed-upon answer to “what does this
|
|
software do?”
|
|
</p>
|
|
</div>
|
|
<div className="rounded-xl border border-fd-border bg-fd-card p-6">
|
|
<div className="mb-3 inline-flex size-10 items-center justify-center rounded-lg bg-fd-primary/10 text-fd-primary">
|
|
<GitBranch className="size-5" />
|
|
</div>
|
|
<h3 className="text-lg font-semibold">
|
|
<code>changes/</code> — what you are proposing
|
|
</h3>
|
|
<p className="mt-2 text-sm text-fd-muted-foreground">
|
|
One folder per change. Each holds a proposal, a design, a task list,
|
|
and a small spec delta. When the work is done, you archive it and the
|
|
delta folds into the truth. The cycle closes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const IDEAS = [
|
|
{
|
|
icon: FileText,
|
|
title: 'Specs are the truth',
|
|
body: 'Requirements and scenarios describe how your system behaves today. One agreed-upon answer, in your repo, readable by humans and AI alike.',
|
|
},
|
|
{
|
|
icon: GitBranch,
|
|
title: 'A change is one unit of work',
|
|
body: 'One feature, one folder. Proposal, design, tasks, and spec edits all live together. Easy to review, easy to reason about.',
|
|
},
|
|
{
|
|
icon: Layers,
|
|
title: 'Deltas, not rewrites',
|
|
body: 'You describe what is changing — ADDED, MODIFIED, REMOVED — not the whole world. That is the trick that makes OpenSpec great at brownfield code.',
|
|
},
|
|
{
|
|
icon: Compass,
|
|
title: 'Enablers, not gates',
|
|
body: 'Artifacts build on each other in a natural order, but nothing locks. Learn something mid-build? Edit the plan and keep going.',
|
|
},
|
|
];
|
|
|
|
function FiveIdeas() {
|
|
return (
|
|
<section className="border-y border-fd-border bg-fd-card/30">
|
|
<div className="mx-auto max-w-5xl px-4 py-20">
|
|
<div className="mx-auto max-w-2xl text-center">
|
|
<h2 className="text-3xl font-bold tracking-tight">
|
|
Learn four ideas, and the rest is detail
|
|
</h2>
|
|
<p className="mt-4 text-fd-muted-foreground">
|
|
Everything in OpenSpec is built from a handful of simple concepts.
|
|
</p>
|
|
</div>
|
|
<div className="mt-12 grid gap-6 sm:grid-cols-2">
|
|
{IDEAS.map(({ icon: Icon, title, body }) => (
|
|
<div
|
|
key={title}
|
|
className="rounded-xl border border-fd-border bg-fd-card p-6"
|
|
>
|
|
<Icon className="size-5 text-fd-primary" />
|
|
<h3 className="mt-3 font-semibold">{title}</h3>
|
|
<p className="mt-2 text-sm text-fd-muted-foreground">{body}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const STEPS = [
|
|
{
|
|
icon: Compass,
|
|
cmd: '/opsx:explore',
|
|
label: 'optional',
|
|
body: 'A no-stakes thinking partner. It reads your code, weighs options, and turns a fuzzy idea into a concrete plan.',
|
|
},
|
|
{
|
|
icon: FileText,
|
|
cmd: '/opsx:propose',
|
|
body: 'The AI drafts the proposal, spec deltas, design, and a task list. You read it and adjust before any code is written.',
|
|
},
|
|
{
|
|
icon: Hammer,
|
|
cmd: '/opsx:apply',
|
|
body: 'The AI builds it, working through the tasks and checking each one off as it goes.',
|
|
},
|
|
{
|
|
icon: Archive,
|
|
cmd: '/opsx:archive',
|
|
body: 'Spec deltas merge into the truth and the change is filed away with a date stamp. Ready for the next one.',
|
|
},
|
|
];
|
|
|
|
function TheLoop() {
|
|
return (
|
|
<section className="mx-auto max-w-5xl px-4 py-20">
|
|
<div className="mx-auto max-w-2xl text-center">
|
|
<h2 className="text-3xl font-bold tracking-tight">The loop you run</h2>
|
|
<p className="mt-4 text-fd-muted-foreground">
|
|
Two terminal commands to set up. After that, you live in your AI chat.
|
|
</p>
|
|
</div>
|
|
<ol className="mt-12 grid gap-4 md:grid-cols-4">
|
|
{STEPS.map(({ icon: Icon, cmd, label, body }, i) => (
|
|
<li
|
|
key={cmd}
|
|
className="relative rounded-xl border border-fd-border bg-fd-card p-5"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<Icon className="size-5 text-fd-primary" />
|
|
<span className="text-xs font-medium text-fd-muted-foreground">
|
|
{label ?? `step ${i + 1}`}
|
|
</span>
|
|
</div>
|
|
<code className="mt-3 block text-sm font-semibold text-fd-primary">
|
|
{cmd}
|
|
</code>
|
|
<p className="mt-2 text-sm text-fd-muted-foreground">{body}</p>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function Why() {
|
|
return (
|
|
<section className="border-y border-fd-border bg-fd-card/30">
|
|
<div className="mx-auto max-w-5xl px-4 py-20">
|
|
<div className="mx-auto max-w-2xl text-center">
|
|
<h2 className="text-3xl font-bold tracking-tight">
|
|
Why bother with the extra step?
|
|
</h2>
|
|
<p className="mt-4 text-fd-muted-foreground">
|
|
OpenSpec adds one small step — a short plan before building. Here is
|
|
what you get for it.
|
|
</p>
|
|
</div>
|
|
<div className="mx-auto mt-12 grid max-w-3xl gap-5 sm:grid-cols-2">
|
|
{[
|
|
[
|
|
'Catch wrong turns early',
|
|
'Fixing a misunderstanding in a one-paragraph proposal is free. Fixing it after 400 lines of code is not.',
|
|
],
|
|
[
|
|
'The plan lives with the code',
|
|
'Six months later, the spec tells you and the next AI session why the system works the way it does.',
|
|
],
|
|
[
|
|
'Changes are reviewable',
|
|
'A change folder is a tidy package: read the proposal, skim the deltas, check the tasks. No chat archaeology.',
|
|
],
|
|
[
|
|
'It fits existing codebases',
|
|
'Deltas mean you can specify a change to a 50,000-line app without first documenting the whole thing.',
|
|
],
|
|
].map(([title, body]) => (
|
|
<div key={title} className="flex gap-3">
|
|
<ArrowRight className="mt-1 size-4 shrink-0 text-fd-primary" />
|
|
<div>
|
|
<div className="font-semibold">{title}</div>
|
|
<p className="mt-1 text-sm text-fd-muted-foreground">{body}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const TEAM_SCENARIOS = [
|
|
{
|
|
icon: Share2,
|
|
title: 'Cross-repo features',
|
|
body: 'One change, one plan — even when the code lands in the API server, the web app, and a shared library. No more "whose openspec/ folder does this live in?"',
|
|
},
|
|
{
|
|
icon: Boxes,
|
|
title: 'Shared requirements',
|
|
body: 'A platform team owns the specs; product teams reference them read-only, right where their coding agent can read them. No more drifting wiki.',
|
|
},
|
|
{
|
|
icon: Clock,
|
|
title: 'Plan before code',
|
|
body: 'Capture the plan in the store now, while it is just an idea. The code repos catch up later — the thinking is already recorded and reviewed.',
|
|
},
|
|
];
|
|
|
|
function Teams() {
|
|
return (
|
|
<section className="border-y border-fd-border bg-fd-primary/5">
|
|
<div className="mx-auto max-w-5xl px-4 py-20">
|
|
<div className="mx-auto max-w-2xl text-center">
|
|
<p className="text-sm font-medium uppercase tracking-wide text-fd-primary">
|
|
For teams
|
|
</p>
|
|
<h2 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl">
|
|
Why teams adopt OpenSpec
|
|
</h2>
|
|
<p className="mt-4 text-fd-muted-foreground">
|
|
Solo, OpenSpec keeps you and your AI honest on one repo. On a team,
|
|
the hard part moves: work spans repos, requirements cross team lines,
|
|
and planning starts before code exists. OpenSpec{' '}
|
|
<Link href={`${docsRoute}/stores`} className="font-medium text-fd-primary underline">
|
|
stores
|
|
</Link>{' '}
|
|
put planning in a repo of its own — one source of truth your whole
|
|
team and every coding agent can read, shared by{' '}
|
|
<code>git push</code> like anything else.
|
|
</p>
|
|
</div>
|
|
<div className="mt-12 grid gap-5 md:grid-cols-3">
|
|
{TEAM_SCENARIOS.map(({ icon: Icon, title, body }) => (
|
|
<div
|
|
key={title}
|
|
className="rounded-xl border border-fd-border bg-fd-card p-6"
|
|
>
|
|
<div className="mb-3 inline-flex size-10 items-center justify-center rounded-lg bg-fd-primary/10 text-fd-primary">
|
|
<Icon className="size-5" />
|
|
</div>
|
|
<h3 className="font-semibold">{title}</h3>
|
|
<p className="mt-2 text-sm text-fd-muted-foreground">{body}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="mt-10 text-center">
|
|
<Link
|
|
href={`${docsRoute}/stores`}
|
|
className="inline-flex items-center justify-center gap-2 rounded-lg bg-fd-primary px-5 py-2.5 text-sm font-semibold text-fd-primary-foreground transition-opacity hover:opacity-90"
|
|
>
|
|
Explore stores <ArrowRight className="size-4" />
|
|
</Link>
|
|
<span className="ml-3 rounded-full border border-fd-border bg-fd-card px-2.5 py-1 text-xs font-medium text-fd-muted-foreground">
|
|
Beta
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const TOOLS = [
|
|
'Claude Code',
|
|
'Cursor',
|
|
'Codex',
|
|
'Devin Desktop',
|
|
'Gemini CLI',
|
|
'GitHub Copilot',
|
|
'Cline',
|
|
'Zoo Code',
|
|
'Kilo Code',
|
|
'Amazon Q',
|
|
'OpenCode',
|
|
'Qwen Code',
|
|
'Kiro',
|
|
'Continue',
|
|
'Factory Droid',
|
|
];
|
|
|
|
function ToolStrip() {
|
|
return (
|
|
<section className="mx-auto max-w-5xl px-4 py-16 text-center">
|
|
<p className="text-sm font-medium uppercase tracking-wide text-fd-muted-foreground">
|
|
Works with the tools you already use
|
|
</p>
|
|
<div className="mt-6 flex flex-wrap items-center justify-center gap-2.5">
|
|
{TOOLS.map((t) => (
|
|
<span
|
|
key={t}
|
|
className="rounded-full border border-fd-border bg-fd-card px-3.5 py-1.5 text-sm text-fd-foreground/80"
|
|
>
|
|
{t}
|
|
</span>
|
|
))}
|
|
<span className="rounded-full px-3.5 py-1.5 text-sm font-medium text-fd-primary">
|
|
+ 15 more
|
|
</span>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const ARTIFACTS = [
|
|
{
|
|
icon: FileText,
|
|
file: 'proposal.md',
|
|
caption: 'The why and what',
|
|
code: `# Proposal: Add Dark Mode
|
|
|
|
## Intent
|
|
Reduce eye strain at night and
|
|
match the user's system theme.
|
|
|
|
## Scope
|
|
- Theme toggle in settings
|
|
- System-preference detection
|
|
- Persist the choice`,
|
|
},
|
|
{
|
|
icon: Layers,
|
|
file: 'specs/ui/spec.md',
|
|
caption: 'The delta — what changes',
|
|
code: `# Delta for UI
|
|
|
|
## ADDED Requirements
|
|
|
|
### Requirement: Theme Selection
|
|
The system SHALL let users choose
|
|
light or dark.
|
|
|
|
#### Scenario: Manual toggle
|
|
- WHEN the toggle is clicked
|
|
- THEN the theme switches at once`,
|
|
},
|
|
{
|
|
icon: ListChecks,
|
|
file: 'tasks.md',
|
|
caption: 'The checklist',
|
|
code: `# Tasks
|
|
|
|
## 1. Theme Infrastructure
|
|
- [ ] 1.1 ThemeContext + state
|
|
- [ ] 1.2 CSS custom properties
|
|
- [ ] 1.3 localStorage persistence
|
|
|
|
## 2. UI
|
|
- [ ] 2.1 ThemeToggle component`,
|
|
},
|
|
];
|
|
|
|
function Anatomy() {
|
|
return (
|
|
<section className="mx-auto max-w-5xl px-4 py-20">
|
|
<div className="mx-auto max-w-2xl text-center">
|
|
<h2 className="text-3xl font-bold tracking-tight">
|
|
What a change actually looks like
|
|
</h2>
|
|
<p className="mt-4 text-fd-muted-foreground">
|
|
Plain Markdown files your AI drafts and you review. No new formats to
|
|
learn, nothing you cannot read at a glance.
|
|
</p>
|
|
</div>
|
|
<div className="mt-12 grid gap-5 md:grid-cols-3">
|
|
{ARTIFACTS.map(({ icon: Icon, file, caption, code }) => (
|
|
<div
|
|
key={file}
|
|
className="overflow-hidden rounded-xl border border-fd-border bg-fd-card"
|
|
>
|
|
<div className="flex items-center gap-2 border-b border-fd-border px-4 py-2.5">
|
|
<Icon className="size-4 text-fd-primary" />
|
|
<code className="text-xs font-medium">{file}</code>
|
|
</div>
|
|
<pre className="overflow-x-auto p-4 text-xs leading-relaxed text-fd-muted-foreground">
|
|
<code>{code}</code>
|
|
</pre>
|
|
<div className="border-t border-fd-border px-4 py-2 text-xs text-fd-muted-foreground">
|
|
{caption}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const ROWS = [
|
|
{
|
|
name: 'Spec Kit',
|
|
by: 'GitHub',
|
|
good: 'Thorough and structured',
|
|
catch: 'Rigid phase gates, lots of Markdown, Python setup',
|
|
us: false,
|
|
},
|
|
{
|
|
name: 'Kiro',
|
|
by: 'AWS',
|
|
good: 'Powerful and integrated',
|
|
catch: 'Locked into their IDE and a limited set of models',
|
|
us: false,
|
|
},
|
|
{
|
|
name: 'No specs',
|
|
by: 'the default',
|
|
good: 'Zero overhead',
|
|
catch: 'Vague prompts, unpredictable results, no record of why',
|
|
us: false,
|
|
},
|
|
{
|
|
name: 'OpenSpec',
|
|
by: '',
|
|
good: 'Lightweight, fluid, lives in your repo',
|
|
catch: 'Adds one small step — worth it whenever agreement matters',
|
|
us: true,
|
|
},
|
|
];
|
|
|
|
function Comparison() {
|
|
return (
|
|
<section className="mx-auto max-w-5xl px-4 py-20">
|
|
<div className="mx-auto max-w-2xl text-center">
|
|
<h2 className="text-3xl font-bold tracking-tight">The honest middle</h2>
|
|
<p className="mt-4 text-fd-muted-foreground">
|
|
Heavier tools exist. So does doing nothing. OpenSpec aims for the
|
|
spot where the value clearly beats the cost.
|
|
</p>
|
|
</div>
|
|
<div className="mx-auto mt-12 max-w-3xl divide-y divide-fd-border overflow-hidden rounded-xl border border-fd-border">
|
|
{ROWS.map((r) => (
|
|
<div
|
|
key={r.name}
|
|
className={
|
|
'grid grid-cols-1 gap-1 px-5 py-4 sm:grid-cols-[10rem_1fr] ' +
|
|
(r.us ? 'bg-fd-primary/5' : 'bg-fd-card')
|
|
}
|
|
>
|
|
<div className="flex items-center gap-2 font-semibold">
|
|
{r.us && <Check className="size-4 text-fd-primary" />}
|
|
<span className={r.us ? 'text-fd-primary' : ''}>{r.name}</span>
|
|
{r.by && (
|
|
<span className="text-xs font-normal text-fd-muted-foreground">
|
|
{r.by}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="text-sm">
|
|
<span className="text-fd-foreground/90">{r.good}.</span>{' '}
|
|
<span className="text-fd-muted-foreground">{r.catch}.</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function FinalCta() {
|
|
return (
|
|
<section className="mx-auto max-w-5xl px-4 py-24 text-center">
|
|
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
|
Ship your first change in five minutes
|
|
</h2>
|
|
<p className="mx-auto mt-4 max-w-xl text-fd-muted-foreground">
|
|
Works with 30+ AI assistants — Claude Code, Cursor, Codex, Devin Desktop,
|
|
Gemini CLI, and more.
|
|
</p>
|
|
<div className="mt-8 inline-flex flex-col gap-1 rounded-lg border border-fd-border bg-fd-card px-4 py-3 text-left font-mono text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-fd-muted-foreground">$</span>
|
|
npm install -g @fission-ai/openspec@latest
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-fd-muted-foreground">$</span>
|
|
cd your-project && openspec init
|
|
</div>
|
|
</div>
|
|
<p className="mt-4 text-sm text-fd-muted-foreground">
|
|
Or{' '}
|
|
<Link
|
|
href={`${docsRoute}/installation#install-with-your-ai-assistant`}
|
|
className="underline underline-offset-4 hover:text-fd-foreground"
|
|
>
|
|
let your AI assistant install it for you
|
|
</Link>
|
|
.
|
|
</p>
|
|
<div className="mt-8">
|
|
<Link
|
|
href={`${docsRoute}/getting-started`}
|
|
className="inline-flex items-center justify-center gap-2 rounded-lg bg-fd-primary px-6 py-3 text-sm font-semibold text-fd-primary-foreground transition-opacity hover:opacity-90"
|
|
>
|
|
Read the getting-started guide <ArrowRight className="size-4" />
|
|
</Link>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|