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
36 changes: 36 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ on:
- reopened
- synchronize

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
reqstool-source: [pypi, main]
steps:
- uses: actions/checkout@v7
- name: Set up Java
Expand All @@ -22,3 +29,32 @@ jobs:
distribution: "temurin"
- name: Build
run: ./gradlew clean build
- name: Self-apply plugin to assemble dataset zip
# True self-application (publishing to mavenLocal and re-applying the plugin
# to its own sources from a separate project) is not yet wired up for Gradle.
# As an interim measure, merge the annotation processor's own main/test output
# (already produced by `./gradlew build` above) the same way the plugin's
# combineOutput() would, so reqstool status has real, non-fabricated data.
run: |
mkdir -p build/reqstool
{
echo "# yaml-language-server: \$schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/annotations.schema.json"
yq eval-all '. as $item ireduce ({}; . * $item )' \
build/generated/sources/annotationProcessor/java/main/resources/annotations.yml \
build/generated/sources/annotationProcessor/java/test/resources/annotations.yml | grep -v "^#"
} > build/reqstool/annotations.yml
- name: Install reqstool
uses: reqstool/.github/.github/actions/install-reqstool@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
with:
reqstool-source: ${{ matrix.reqstool-source }}
- name: Validate reqstool spec completeness
# not yet available in the latest PyPI release
if: matrix.reqstool-source == 'main'
uses: reqstool/.github/.github/actions/validate-reqstool@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
- name: Run reqstool status
uses: reqstool/.github/.github/actions/reqstool-status@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
with:
fail-if-incomplete: "true"

validate-openspec:
uses: reqstool/.github/.github/workflows/common-validate-openspec.yml@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
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-java-gradle-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 GRADLE_PLUGIN_NNN / SVC_GRADLE_PLUGIN_NNN ID convention.
plugin:
path: docs/reqstool
req_prefix: "GRADLE_PLUGIN_"
svc_prefix: "SVC_GRADLE_PLUGIN_"
10 changes: 8 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ dependencies {
// Jackson for YAML processing
implementation 'com.fasterxml.jackson.core:jackson-databind:2.22.0'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.22.0'

// SnakeYAML for reqstool_config.yml generation
implementation 'org.yaml:snakeyaml:2.6'


// reqstool dogfooding: @Requirements/@SVCs annotations + processor
implementation 'io.github.reqstool:reqstool-java-annotations:1.0.0'
annotationProcessor 'io.github.reqstool:reqstool-java-annotations:1.0.0'
testImplementation 'io.github.reqstool:reqstool-java-annotations:1.0.0'
testAnnotationProcessor 'io.github.reqstool:reqstool-java-annotations:1.0.0'

// Testing - JUnit BOM for version management
testImplementation platform('org.junit:junit-bom:6.1.0')
testImplementation 'org.junit.jupiter:junit-jupiter-api'
Expand Down
10 changes: 10 additions & 0 deletions docs/reqstool/reqstool_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/reqstool_config.schema.json

language: java
build: gradle
resources:
requirements: requirements.yml
software_verification_cases: software_verification_cases.yml
annotations: ../../build/reqstool/annotations.yml
test_results:
- ../../build/test-results/test/*.xml
41 changes: 41 additions & 0 deletions docs/reqstool/requirements.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/requirements.schema.json
---
metadata:
urn: reqstool-java-gradle-plugin
variant: microservice
title: Reqstool Java Gradle Plugin
url: https://github.com/reqstool/reqstool-java-gradle-plugin

requirements:
- id: GRADLE_PLUGIN_001
title: Annotation combination
significance: shall
description: >-
The plugin shall combine requirement implementation annotations and SVC test
annotations gathered from multiple source sets into a single annotations
dataset, merging test entries for SVC IDs that appear in more than one
source set.
categories:
- functional-suitability
revision: "0.1.0"

- id: GRADLE_PLUGIN_002
title: Zip artifact assembly
significance: shall
description: >-
The plugin shall assemble a reqstool dataset ZIP artifact containing the
requirements dataset files, the combined annotations file, matching test
result files, and a generated reqstool_config.yml.
categories:
- functional-suitability
revision: "0.1.0"

- id: GRADLE_PLUGIN_003
title: Skip flags
significance: shall
description: >-
The plugin shall support skip flags that bypass task execution entirely
or bypass only ZIP artifact assembly, while still combining annotations.
categories:
- functional-suitability
revision: "0.1.0"
20 changes: 20 additions & 0 deletions docs/reqstool/software_verification_cases.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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_GRADLE_PLUGIN_001
requirement_ids: ["GRADLE_PLUGIN_001"]
title: "Verify implementation and test annotations are combined and merged across source sets"
verification: automated-test
revision: "0.1.0"

- id: SVC_GRADLE_PLUGIN_002
requirement_ids: ["GRADLE_PLUGIN_002"]
title: "Verify the zip artifact is assembled with the mandatory and optional reqstool resources"
verification: automated-test
revision: "0.1.0"

- id: SVC_GRADLE_PLUGIN_003
requirement_ids: ["GRADLE_PLUGIN_003"]
title: "Verify skip and skipAssembleZipArtifact each bypass their respective step"
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-combination/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Annotation Combination 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: GRADLE_PLUGIN_001
The system SHALL implement GRADLE_PLUGIN_001.

#### Scenario: SVC_GRADLE_PLUGIN_001
The system SHALL pass SVC_GRADLE_PLUGIN_001.
15 changes: 15 additions & 0 deletions openspec/specs/skip-flags/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Skip Flags 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: GRADLE_PLUGIN_003
The system SHALL implement GRADLE_PLUGIN_003.

#### Scenario: SVC_GRADLE_PLUGIN_003
The system SHALL pass SVC_GRADLE_PLUGIN_003.
15 changes: 15 additions & 0 deletions openspec/specs/zip-assembly/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Zip Assembly 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: GRADLE_PLUGIN_002
The system SHALL implement GRADLE_PLUGIN_002.

#### Scenario: SVC_GRADLE_PLUGIN_002
The system SHALL pass SVC_GRADLE_PLUGIN_002.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import io.github.reqstool.annotations.Requirements;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.ConfigurableFileCollection;
Expand Down Expand Up @@ -263,6 +264,7 @@ public RegularFileProperty getZipFile() {
* annotations file, and optionally assembles a ZIP artifact containing requirements,
* annotations, and test results.
*/
@Requirements({ "GRADLE_PLUGIN_003" })
@TaskAction
public void execute() {
if (skip.get()) {
Expand Down Expand Up @@ -334,6 +336,7 @@ else if (reqAnnotFile != null) {
* @param testsNode node containing test cases
* @return combined requirement annotations node
*/
@Requirements({ "GRADLE_PLUGIN_001" })
static JsonNode combineOutput(JsonNode implementationsNode, JsonNode testsNode) {
ObjectNode requirementAnnotationsNode = yamlMapper.createObjectNode();
if (!implementationsNode.isEmpty()) {
Expand All @@ -355,6 +358,7 @@ static JsonNode combineOutput(JsonNode implementationsNode, JsonNode testsNode)
* @param target the node to merge into
* @param source the node to merge from
*/
@Requirements({ "GRADLE_PLUGIN_001" })
static void mergeTestNodes(ObjectNode target, JsonNode source) {
if (!source.isObject()) {
return;
Expand Down Expand Up @@ -396,6 +400,7 @@ private void writeCombinedOutputToFile(File outputFile, JsonNode combinedOutputN
* results.
* @throws IOException if ZIP creation or file reading fails
*/
@Requirements({ "GRADLE_PLUGIN_002" })
private void assembleZipArtifact() throws IOException {
String zipArtifactFilename = projectName.get() + "-" + projectVersion.get() + "-reqstool.zip";
String topLevelDir = projectName.get() + "-" + projectVersion.get() + "-reqstool";
Expand Down
Loading