-
Notifications
You must be signed in to change notification settings - Fork 0
feat(reqstool): dogfood OpenSpec ↔ reqstool traceability #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b006558
01d21a8
3a7d9c5
37dbd49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "mcpServers": { | ||
| "reqstool": { | ||
| "command": "reqstool", | ||
| "args": ["mcp"] | ||
| } | ||
| } | ||
| } |
| 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-poetry-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 POETRY_PLUGIN_NNN / SVC_POETRY_PLUGIN_NNN ID convention. | ||
| plugin: | ||
| path: docs/reqstool | ||
| req_prefix: "POETRY_PLUGIN_" | ||
| svc_prefix: "SVC_POETRY_PLUGIN_" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # 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_POETRY_PLUGIN_001 | ||
| requirement_ids: ["POETRY_PLUGIN_001"] | ||
| title: "Verify the built sdist contains the reqstool dataset, annotations, and config" | ||
| verification: automated-test | ||
| revision: "0.1.0" | ||
|
|
||
| - id: SVC_POETRY_PLUGIN_002 | ||
| requirement_ids: ["POETRY_PLUGIN_002"] | ||
| title: "Verify annotations.yml is generated from decorated source and bundled" | ||
| verification: automated-test | ||
| revision: "0.1.0" | ||
|
|
||
| - id: SVC_POETRY_PLUGIN_003 | ||
| requirement_ids: ["POETRY_PLUGIN_003"] | ||
| title: "Verify pyproject.toml's [tool.poetry.include] is updated with reqstool paths" | ||
| verification: automated-test | ||
| revision: "0.1.0" | ||
|
|
||
| - id: SVC_POETRY_PLUGIN_004 | ||
| requirement_ids: ["POETRY_PLUGIN_004"] | ||
| title: "Verify reqstool_config.yml is removed from the project root after build" | ||
| verification: automated-test | ||
| revision: "0.1.0" | ||
|
|
||
| - id: SVC_POETRY_PLUGIN_005 | ||
| requirement_ids: ["POETRY_PLUGIN_005"] | ||
| title: "Verify excess blank lines are stripped from pyproject.toml after install" | ||
| verification: automated-test | ||
| revision: "0.1.0" |
| 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> { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Low] #9 — MCP client has no process error/exit handling If Found by: |
||
| 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}` }], | ||
| }; | ||
| } | ||
| }; | ||
| 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: POETRY_PLUGIN_002 | ||
| The system SHALL implement POETRY_PLUGIN_002. | ||
|
|
||
| #### Scenario: SVC_POETRY_PLUGIN_002 | ||
| The system SHALL pass SVC_POETRY_PLUGIN_002. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Build Cleanup 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: POETRY_PLUGIN_004 | ||
| The system SHALL implement POETRY_PLUGIN_004. | ||
|
|
||
| #### Scenario: SVC_POETRY_PLUGIN_004 | ||
| The system SHALL pass SVC_POETRY_PLUGIN_004. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Install Cleanup 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: POETRY_PLUGIN_005 | ||
| The system SHALL implement POETRY_PLUGIN_005. | ||
|
|
||
| #### Scenario: SVC_POETRY_PLUGIN_005 | ||
| The system SHALL pass SVC_POETRY_PLUGIN_005. |
| 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: POETRY_PLUGIN_001 | ||
| The system SHALL implement POETRY_PLUGIN_001. | ||
|
|
||
| #### Scenario: SVC_POETRY_PLUGIN_001 | ||
| The system SHALL pass SVC_POETRY_PLUGIN_001. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Sdist Include Registration 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: POETRY_PLUGIN_003 | ||
| The system SHALL implement POETRY_PLUGIN_003. | ||
|
|
||
| #### Scenario: SVC_POETRY_PLUGIN_003 | ||
| The system SHALL pass SVC_POETRY_PLUGIN_003. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Low] #4 — Full build/test pipeline duplicated per matrix leg
The entire build+test+package pipeline (install deps, build wheel, install plugin, pytest, poetry build) runs twice even though only the 3 reqstool-specific steps differ by
reqstool-source. This roughly doubles CI time/cost for this job with no added coverage of the plugin itself.Leaving as-is for now: splitting the reqstool-specific steps into a separate downstream job (consuming a shared build artifact, matrixed only there) would avoid the duplication, but adds cross-job artifact-passing complexity to a CI pattern that's still being proven out across the org-wide dogfooding rollout. Worth revisiting once the
[pypi, main]matrix can collapse back to a single pypi-only leg (tracked via reqstool-client's release cadence).Found by:
code-review