Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/titled-artifact-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fission-ai/openspec": patch
---

Start generated proposal, spec, design, and tasks files with a top-level heading, so artifacts are complete markdown documents instead of files whose first line is a section header. Editors that run markdownlint no longer flag every OpenSpec artifact with MD041. `openspec schema init` scaffolds custom templates the same way.

`openspec show --json` and `openspec change list --json` keep naming a change by its id when its proposal opens with the template's bare `# Proposal` title.
2 changes: 1 addition & 1 deletion docs-lab/customize/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ The scaffold is bare. Artifacts come from the built-in four ids only, and the ge

A fork has two kinds of files to edit:

- **templates/** change the skeleton of each document. Add a section to the tasks template and every new tasks.md starts with it.
- **templates/** change the skeleton of each document. Add a section to the tasks template and every new tasks.md starts with it. Keep the `#` title on the first line: the artifact inherits it, so every generated file opens as a titled document.
- **schema.yaml** changes the workflow itself: which artifacts exist, what each one requires first, and the instruction the agent gets when creating it.

For example, to drop the design document for a leaner flow:
Expand Down
32 changes: 28 additions & 4 deletions docs-lab/reference/schemas/spec-driven/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Establishes why the change is needed.
The template the agent receives as the output format ([templates/proposal.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/proposal.md)):

```md
# Proposal

## Why

<!-- Explain the motivation for this change. What problem does this solve? Why now? -->
Expand Down Expand Up @@ -101,7 +103,19 @@ Sections:
- **Impact**: Affected code, APIs, dependencies, or systems.

IMPORTANT: The Capabilities section is critical. It creates the contract between
proposal and specs phases. Research existing specs before filling this in.
proposal and specs phases. Research existing specs before filling this in:
run `openspec list --specs` for the project's capability inventory, then
`openspec show "<spec-id>" --type spec --json --no-scenarios` for any that
look related - that returns a capability's purpose and requirement texts
without pulling whole spec files into context. Append `--store "<id>"` to
both commands only for a registered standalone store, and keep `--type
spec`: a change and a spec sharing a name is otherwise an ambiguous-item
error. `openspec list` without `--specs` lists in-flight changes, not
specs - it never shows what the project already covers. Reuse an existing
capability's exact path instead of introducing a near-duplicate name.
The filtered read is only an overview. Before deciding what is already
covered or what should change, read each relevant spec in full, including
scenarios, with `openspec show "<spec-id>" --type spec` (same `--store` rule).
Each capability listed here will need a corresponding spec file.

Every change must either declare at least one capability (new or
Expand Down Expand Up @@ -129,6 +143,8 @@ Each delta spec is the `spec.md` inside its capability folder. `openspec validat
The template the agent receives as the output format ([templates/spec.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/spec.md)):

```md
# Spec Delta

## Purpose
<!-- New capabilities only: one or two sentences (50+ characters) on what this capability is for. Delete this section for an existing capability. -->

Expand Down Expand Up @@ -170,7 +186,7 @@ Create one spec file per capability listed in the proposal's Capabilities sectio
`<capability-path>` is the spec directory relative to `specs/` (for example,
`user-auth` or `identity/user-auth`). Preserve the full path:
- New capabilities: use the exact path from the proposal at `specs/<capability-path>/spec.md`. Any path segment newly introduced in the proposal must be kebab-case. Follow the project's existing organization; do not add a new domain level when the project uses a flat layout.
- Modified capabilities: use the exact existing path from `openspec/specs/<capability-path>/` when creating the delta at `specs/<capability-path>/spec.md`. Do not move or rename the capability.
- Modified capabilities: use the exact existing path from `openspec/specs/<capability-path>/` when creating the delta at `specs/<capability-path>/spec.md`. Run `openspec list --specs` to confirm that path before writing the delta, appending `--store "<id>"` only for a registered standalone store - a mistyped or invented path targets a capability that does not exist rather than the one you meant. Do not move or rename the capability.

There must be at least one spec file unless the change's `.openspec.yaml`
sets `skip_specs: true` (no spec-level behavior change) - `openspec validate`
Expand All @@ -190,7 +206,7 @@ Format requirements:
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
- Every requirement MUST have at least one scenario.

New capabilities only: start the delta spec with a `## Purpose` section -
New capabilities only: the delta spec's first section is `## Purpose` -
one or two sentences (50+ characters, or `openspec validate --strict`
reports it as too brief) describing what the capability is for. Archive
copies it into the main spec it creates; without it the new main spec is
Expand All @@ -209,8 +225,10 @@ MODIFIED requirements workflow:
Common pitfall: Using MODIFIED with partial content loses detail at archive time.
If adding new concerns without changing existing behavior, use ADDED instead.

Example (a new capability, so it opens with `## Purpose`):
Example (a new capability, so its first section is `## Purpose`):
```
# Spec Delta

## Purpose

Lets users take their data out of the product in a portable format.
Expand Down Expand Up @@ -243,6 +261,8 @@ Explains how to implement the change. Drafted only when the change needs one.
The template the agent receives as the output format ([templates/design.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/design.md)):

```md
# Design

## Context

<!-- Current state and constraints that shape the approach. See proposal.md for motivation - don't restate it -->
Expand Down Expand Up @@ -307,6 +327,8 @@ Breaks the implementation into checkable tasks. [apply](#apply) tracks progress
The template the agent receives as the output format ([templates/tasks.md](https://github.com/Fission-AI/OpenSpec/blob/main/schemas/spec-driven/templates/tasks.md)):

```md
# Tasks

## 1. <!-- Task Group Name -->

- [ ] 1.1 <!-- Task description -->
Expand Down Expand Up @@ -343,6 +365,8 @@ Guidelines:

Example:
```
# Tasks

## 1. Setup

- [ ] 1.1 Create new module structure
Expand Down
8 changes: 6 additions & 2 deletions schemas/spec-driven/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ artifacts:
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
- Every requirement MUST have at least one scenario.

New capabilities only: start the delta spec with a `## Purpose` section -
New capabilities only: the delta spec's first section is `## Purpose` -
one or two sentences (50+ characters, or `openspec validate --strict`
reports it as too brief) describing what the capability is for. Archive
copies it into the main spec it creates; without it the new main spec is
Expand All @@ -120,8 +120,10 @@ artifacts:
Common pitfall: Using MODIFIED with partial content loses detail at archive time.
If adding new concerns without changing existing behavior, use ADDED instead.

Example (a new capability, so it opens with `## Purpose`):
Example (a new capability, so its first section is `## Purpose`):
```
# Spec Delta

## Purpose

Lets users take their data out of the product in a portable format.
Expand Down Expand Up @@ -211,6 +213,8 @@ artifacts:

Example:
```
# Tasks

## 1. Setup

- [ ] 1.1 Create new module structure and verify expected files are present
Expand Down
2 changes: 2 additions & 0 deletions schemas/spec-driven/templates/design.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Design

## Context

<!-- Current state and constraints that shape the approach. See proposal.md for motivation - don't restate it -->
Expand Down
2 changes: 2 additions & 0 deletions schemas/spec-driven/templates/proposal.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Proposal

## Why

<!-- Explain the motivation for this change. What problem does this solve? Why now? -->
Expand Down
2 changes: 2 additions & 0 deletions schemas/spec-driven/templates/spec.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Spec Delta

## Purpose
<!-- New capabilities only: one or two sentences (50+ characters) on what this capability is for. Delete this section for an existing capability. -->

Expand Down
2 changes: 2 additions & 0 deletions schemas/spec-driven/templates/tasks.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Tasks

## 1. <!-- Task Group Name -->

- [ ] 1.1 <!-- Task description -->
Expand Down
8 changes: 8 additions & 0 deletions skills/openspec-onboard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ Here's a draft proposal:

---

# Proposal

## Why

[1-2 sentences explaining the problem/opportunity]
Expand Down Expand Up @@ -298,6 +300,8 @@ Here's the spec:

---

# Spec Delta

## ADDED Requirements

### Requirement: <Name>
Expand Down Expand Up @@ -337,6 +341,8 @@ Here's the design:

---

# Design

## Context

[Brief context about the current state]
Expand Down Expand Up @@ -382,6 +388,8 @@ Here are the implementation tasks:

---

# Tasks

## 1. [Category or file]

- [ ] 1.1 [Specific task] — verify: [test, command, observable behavior, or delivered artifact]
Expand Down
2 changes: 2 additions & 0 deletions skills/openspec-sync-specs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ In both branches, never create the root as a side effect: do not run `openspec i
**Delta Spec Format Reference**

```markdown
# Spec Delta

## Purpose

Only on a delta that introduces a brand-new capability. Seeds the new main spec.
Expand Down
5 changes: 4 additions & 1 deletion src/commands/change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,10 @@ export class ChangeCommand {

private extractTitle(content: string, changeName: string): string {
const match = content.match(/^#\s+(?:Change:\s+)?(.+)$/im);
return match ? match[1].trim() : changeName;
const title = match?.[1].trim();
// The packaged template opens every proposal with a bare `# Proposal`,
// which names the document rather than the change.
return title && title.toLowerCase() !== 'proposal' ? title : changeName;
}

private printNextSteps(issues: Array<{ message: string }> = []): void {
Expand Down
22 changes: 17 additions & 5 deletions src/commands/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1422,11 +1422,17 @@ export function registerSchemaCommand(program: Command): void {

/**
* Create default template content for an artifact.
*
* Every template opens with a top-level heading so the artifact it produces is
* a well-formed markdown document rather than a file whose first line is a
* section header (markdownlint MD041, #1138).
*/
function createDefaultTemplate(artifactId: string): string {
switch (artifactId) {
case 'proposal':
return `## Why
return `# Proposal

## Why

<!-- Describe the motivation for this change -->

Expand All @@ -1448,7 +1454,9 @@ function createDefaultTemplate(artifactId: string): string {
`;

case 'specs':
return `## ADDED Requirements
return `# Spec Delta

## ADDED Requirements

### Requirement: Example requirement

Expand All @@ -1460,7 +1468,9 @@ Description of the requirement.
`;

case 'design':
return `## Context
return `# Design

## Context

<!-- Background and context -->

Expand All @@ -1487,15 +1497,17 @@ Description and rationale.
`;

case 'tasks':
return `## Implementation Tasks
return `# Tasks

## Implementation Tasks

- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
`;

default:
return `## ${artifactId}
return `# ${artifactId}

<!-- Add content here -->
`;
Expand Down
8 changes: 8 additions & 0 deletions src/core/templates/workflows/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ Here's a draft proposal:

---

# Proposal

## Why

[1-2 sentences explaining the problem/opportunity]
Expand Down Expand Up @@ -362,6 +364,8 @@ Here's the spec:

---

# Spec Delta

## ADDED Requirements

### Requirement: <Name>
Expand Down Expand Up @@ -401,6 +405,8 @@ Here's the design:

---

# Design

## Context

[Brief context about the current state]
Expand Down Expand Up @@ -446,6 +452,8 @@ Here are the implementation tasks:

---

# Tasks

## 1. [Category or file]

- [ ] 1.1 [Specific task] — verify: [test, command, observable behavior, or delivered artifact]
Expand Down
4 changes: 4 additions & 0 deletions src/core/templates/workflows/sync-specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ ${PROJECT_ROOT_GUARD}
**Delta Spec Format Reference**

\`\`\`markdown
# Spec Delta

## Purpose

Only on a delta that introduces a brand-new capability. Seeds the new main spec.
Expand Down Expand Up @@ -465,6 +467,8 @@ ${PROJECT_ROOT_GUARD}
**Delta Spec Format Reference**

\`\`\`markdown
# Spec Delta

## Purpose

Only on a delta that introduces a brand-new capability. Seeds the new main spec.
Expand Down
49 changes: 49 additions & 0 deletions test/commands/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,55 @@ artifacts:
).toContain('schema: my-workflow');
});

it('scaffolds every template with a top-level heading', async () => {
const initialized = await runCLI(
[
'schema',
'init',
'lint-clean',
'--artifacts',
'proposal,specs,design,tasks',
'--json',
],
{ cwd: tempDir }
);
expect(initialized.exitCode).toBe(0);

const templatesDir = path.join(
tempDir,
'openspec',
'schemas',
'lint-clean',
'templates'
);
const templates = fs
.readdirSync(templatesDir, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => path.join(path.relative(templatesDir, entry.parentPath), entry.name))
.sort();
expect(templates).toEqual([
'design.md',
'proposal.md',
path.join('specs', 'spec.md'),
'tasks.md',
].sort());

// The artifact a template produces is a document in its own right, so it
// opens with a title and a blank line, not a section header (#1138).
const headings: Record<string, string> = {
'design.md': '# Design',
'proposal.md': '# Proposal',
[path.join('specs', 'spec.md')]: '# Spec Delta',
'tasks.md': '# Tasks',
};
for (const template of templates) {
const content = fs.readFileSync(path.join(templatesDir, template), 'utf-8');
const [firstLine, secondLine] = content.replace(/\r\n?/g, '\n').split('\n');
expect(firstLine).toBe(headings[template]);
expect(secondLine).toBe('');
}
});

describe.each(failureModes)('$label with --default', ({ force }) => {
it('preserves the schema and invalid YAML config byte-for-byte', async () => {
const { schemaDir, before } = prepareSchemaForFailure(force);
Expand Down
Loading
Loading