Skip to content
Open
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
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extraKnownMarketplaces": {
"reqstool-ai": {
"source": {
"source": "github",
"repo": "reqstool/reqstool-ai"
},
"autoUpdate": true
}
},
"enabledPlugins": {
"reqstool@reqstool-ai": true,
"reqstool-openspec@reqstool-ai": true
}
}
32 changes: 32 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ on:
- reopened
- synchronize

permissions:
contents: read

jobs:
linting:
name: Reuse linting job
Expand All @@ -19,6 +22,10 @@ jobs:
build:
needs: linting
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
reqstool-source: [pypi, main]
steps:
- name: Check out source repository
uses: actions/checkout@v6
Expand All @@ -36,9 +43,34 @@ jobs:
run: hatch run dev:pytest --junitxml=build/junit.xml --cov=reqstool_python_hatch_plugin --cov-report=xml:build/coverage.xml
- name: Build project
run: hatch build
- name: Self-apply own decorators to own src/tests
run: hatch run dev:python scripts/generate_annotations.py
# NOTE: all reqstool/.github refs below are pinned to the same commit -- keep
# these 4 in sync when bumping (install-reqstool, validate-reqstool,
# reqstool-status, and the validate-openspec job's workflow ref further down).
- name: Install reqstool
uses: reqstool/.github/.github/actions/install-reqstool@74cc3ac55a476258898db82c69a78eeaabb2fcbc # main 2026-06-24
with:
reqstool-source: ${{ matrix.reqstool-source }}
- name: Validate reqstool spec completeness
# not yet available in the latest PyPI release; supplementary to (not a
# replacement for) the `reqstool status --fail-if-incomplete` gate below, which
# is the actual required check, run unconditionally on both matrix legs
if: matrix.reqstool-source == 'main'
uses: reqstool/.github/.github/actions/validate-reqstool@74cc3ac55a476258898db82c69a78eeaabb2fcbc # main 2026-06-24
- name: Run reqstool status
# Required gate on both matrix legs -- fails the build unless every
# requirement is implemented and verified.
uses: reqstool/.github/.github/actions/reqstool-status@74cc3ac55a476258898db82c69a78eeaabb2fcbc # main 2026-06-24
with:
fail-if-incomplete: "true"
# Upload artifacts for later use
- name: Upload Artifacts
if: matrix.reqstool-source == 'pypi'
uses: actions/upload-artifact@v7
with:
name: dist
path: dist/

validate-openspec:
uses: reqstool/.github/.github/workflows/common-validate-openspec.yml@74cc3ac55a476258898db82c69a78eeaabb2fcbc # main 2026-06-24
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ __pycache__/
# Distribution / packaging
.Python
build/
!tests/fixtures/test_project/build/
develop-eggs/
dist/
downloads/
Expand Down Expand Up @@ -283,4 +284,5 @@ pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python,intellij+all,visualstudiocode

# Claude Code
.claude/
.claude/*
!.claude/settings.json
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"reqstool": {
"command": "reqstool",
"args": ["mcp"]
}
}
}
32 changes: 32 additions & 0 deletions .reqstool-ai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# reqstool-ai configuration
#
# This file tells reqstool-ai skills where to find your reqstool files
# and how to generate IDs for new requirements and SVCs.
#
# Place this file at: .reqstool-ai.yaml (project root)

# Project URN — matches the urn in your reqstool YAML files
urn: reqstool-python-hatch-plugin

# Revision string for new requirements and SVCs
revision: "0.1.0"

# System-level reqstool directory (contains the SSOT requirements and SVCs)
system:
path: docs/reqstool

# Subproject modules — each module imports a subset of requirements/SVCs via filters
#
# Required fields per module:
# path — path to the module's reqstool directory (contains filter files)
# req_prefix — prefix for requirement IDs belonging to this module (e.g., CORE_)
# svc_prefix — prefix for SVC IDs belonging to this module (e.g., SVC_CORE_)
#
# Add as many modules as your project has. The module name (key) is used in
# commands like `/reqstool:status core` and `/reqstool:add-req core`.
modules:
# Matches the existing HATCH_PLUGIN_NNN / SVC_HATCH_PLUGIN_NNN ID convention.
plugin:
path: docs/reqstool
req_prefix: "HATCH_PLUGIN_"
svc_prefix: "SVC_HATCH_PLUGIN_"
25 changes: 25 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ For DCO sign-off, commit conventions, and code review process, see the organizat

- Python 3.13+
- [Hatch](https://hatch.pypa.io/) (`pip install hatch`)
- [reqstool](https://github.com/reqstool/reqstool-client) (`pipx install reqstool`)
- [OpenSpec](https://github.com/Fission-AI/OpenSpec) (`npm install -g @fission-ai/openspec`)

## Setup

Expand All @@ -17,9 +19,32 @@ cd reqstool-python-hatch-plugin
hatch env create
```

If using Claude Code, opening this repo will prompt you to confirm adding the `reqstool-ai`
marketplace and enabling the `reqstool`/`reqstool-openspec` plugins (configured in
`.claude/settings.json`) — accept the prompt.

Then regenerate the `opsx` slash commands and OpenSpec skills
(`.claude/commands/opsx/`, `.claude/skills/openspec-*`) — they're CLI-generated tool scaffolding,
not committed to the repo:

```bash
openspec update # or: openspec init --tools claude --force
```

## Build & Test

```bash
hatch build
hatch run test
```

## Self-applied traceability (`docs/reqstool/`)

This project dogfoods itself: `hatch run dev:python scripts/generate_annotations.py` scans its
own `src`/`tests/unit`/`tests/e2e` for `@Requirements`/`@SVCs` decorators and writes
`build/reqstool/annotations.yml`. Check status with:

```bash
hatch build --target wheel && hatch run dev:pytest && hatch run dev:python scripts/generate_annotations.py
reqstool status --check-all-reqs-met local -p docs/reqstool
```
11 changes: 11 additions & 0 deletions docs/reqstool/reqstool_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/reqstool_config.schema.json

language: python
build: hatch
resources:
requirements: requirements.yml
software_verification_cases: software_verification_cases.yml
manual_verification_results: manual_verification_results.yml
annotations: ../../build/reqstool/annotations.yml
test_results:
- ../../build/**/*.xml
31 changes: 31 additions & 0 deletions docs/reqstool/requirements.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/requirements.schema.json

metadata:
urn: reqstool-python-hatch-plugin
variant: microservice
title: Reqstool Python Hatch Plugin
url: https://github.com/reqstool/reqstool-python-hatch-plugin

requirements:
- id: HATCH_PLUGIN_001
title: Generate annotations.yml from reqstool decorators during build
significance: shall
description: >-
On `hatch build`, the plugin shall process the configured source
directories for `@Requirements`/`@SVCs` decorators and write the
result to annotations.yml in the configured output directory, before
any build artifact is produced.
categories: [functional-suitability]
revision: "0.1.0"
- id: HATCH_PLUGIN_002
title: Bundle reqstool dataset and annotations into the built sdist
significance: shall
description: >-
After a sdist artifact is built, the plugin shall append the
project's reqstool dataset (requirements.yml,
software_verification_cases.yml, manual_verification_results.yml,
when present), the generated annotations.yml, any configured test
result files, and a generated reqstool_config.yml directly into the
sdist tarball. Non-sdist artifacts (e.g. wheel) are left untouched.
categories: [functional-suitability]
revision: "0.1.0"
14 changes: 14 additions & 0 deletions docs/reqstool/software_verification_cases.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/software_verification_cases.schema.json

cases:
- id: SVC_HATCH_PLUGIN_001
requirement_ids: ["HATCH_PLUGIN_001"]
title: "Verify annotations.yml is generated from decorated source during hatch build"
verification: automated-test
revision: "0.1.0"

- id: SVC_HATCH_PLUGIN_002
requirement_ids: ["HATCH_PLUGIN_002"]
title: "Verify the built sdist tarball contains the reqstool dataset, annotations, and config"
verification: automated-test
revision: "0.1.0"
108 changes: 108 additions & 0 deletions openspec/openspecui.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// @reqstool-openspec-hooks: 0.1.1
import { spawn, ChildProcess } from "child_process";
import type { OnReadDocumentHookV1 } from "openspecui/hooks";

// Minimal MCP client over stdio (JSON-RPC 2.0, newline-delimited).
// Uses only Node.js built-ins — no npm packages required.
class McpStdioClient {
private proc: ChildProcess;
private buf = "";
private pending = new Map<
number,
{ resolve: (v: unknown) => void; reject: (e: Error) => void }
>();
private id = 1;
readonly ready: Promise<void>;

constructor(cwd: string) {
this.proc = spawn("reqstool", ["mcp"], {
cwd,
stdio: ["pipe", "pipe", "pipe"],
});
this.proc.stdout!.on("data", (chunk: Buffer) => {
this.buf += chunk.toString();
let nl: number;
while ((nl = this.buf.indexOf("\n")) !== -1) {
const line = this.buf.slice(0, nl).trim();
this.buf = this.buf.slice(nl + 1);
if (line) this.handle(line);
}
});
this.ready = this.init();
}

private handle(line: string) {
try {
const msg = JSON.parse(line) as { id?: number; result?: unknown; error?: { message: string } };
if (msg.id !== undefined) {
const p = this.pending.get(msg.id);
if (p) {
this.pending.delete(msg.id);
msg.error ? p.reject(new Error(msg.error.message)) : p.resolve(msg.result);
}
}
} catch (e) {
console.warn("[reqstool-openspec] Skipping non-JSON line from reqstool mcp:", e instanceof Error ? e.message : e);
}
}

private send(method: string, params: unknown, expectReply = true): Promise<unknown> {
if (!expectReply) {
this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
return Promise.resolve();
}
const id = this.id++;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
});
}

private async init(): Promise<void> {
await this.send("initialize", {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
clientInfo: { name: "openspecui", version: "1.0" },
});
this.send("notifications/initialized", {}, false);
}

async enrich(content: string, preset: string): Promise<string> {
await this.ready;
const result = (await this.send("tools/call", {
name: "enrich_document",
arguments: { content, preset },
})) as { content: { text: string }[] };
return result.content[0].text;
}

close() {
this.proc.stdin?.end();
this.proc.kill();
}
}

let client: McpStdioClient | null = null;

export const onReadDocument: OnReadDocumentHookV1 = async (ctx, read) => {
if (!client) {
client = new McpStdioClient(ctx.projectDir);
ctx.lifecycle.onDispose(() => {
client?.close();
client = null;
});
}

const result = await read();
const preset = `openspec:${ctx.document.kind}`;

try {
const enriched = await client.enrich(result.markdown, preset);
return { ...result, markdown: enriched, sourceLabel: `reqstool ${preset}` };
} catch (e) {
return {
...result,
diagnostics: [{ level: "warning", message: `reqstool enrich failed: ${e}` }],
};
}
};
15 changes: 15 additions & 0 deletions openspec/specs/annotation-generation/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Annotation Generation Specification

## Purpose

Requirement and SVC content is owned by reqstool (single source of truth). This spec references
reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via
`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`.

## Requirements

### Requirement: HATCH_PLUGIN_001
The system SHALL implement HATCH_PLUGIN_001.

#### Scenario: SVC_HATCH_PLUGIN_001
The system SHALL pass SVC_HATCH_PLUGIN_001.
15 changes: 15 additions & 0 deletions openspec/specs/sdist-bundling/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Sdist Bundling Specification

## Purpose

Requirement and SVC content is owned by reqstool (single source of truth). This spec references
reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via
`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`.

## Requirements

### Requirement: HATCH_PLUGIN_002
The system SHALL implement HATCH_PLUGIN_002.

#### Scenario: SVC_HATCH_PLUGIN_002
The system SHALL pass SVC_HATCH_PLUGIN_002.
17 changes: 17 additions & 0 deletions scripts/generate_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
# Copyright © LFV
"""Self-apply reqstool-python-decorators' processor to this repo's own src/tests,
producing build/reqstool/annotations.yml for `reqstool status` to consume."""

from reqstool_python_decorators.processors.decorator_processor import DecoratorProcessor

if __name__ == "__main__":
DecoratorProcessor().process_decorated_data(
# tests/fixtures holds a self-contained fixture project with its own decorated
# REQ_001/SVC_001, unrelated to this plugin's own requirements -- excluded so it
# doesn't pollute this repo's own traceability data.
# tests/integration is omitted entirely: it's currently just empty __init__.py
# stubs, add it back once real integration tests land there.
path_to_python_files=["src", "tests/unit", "tests/e2e"],
output_file="build/reqstool/annotations.yml",
)
Loading