opensddrag 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/package.json +1 -1
- package/src/commands/init.js +61 -46
- package/src/commands/status.js +7 -10
- package/src/config.js +53 -0
- package/src/templates/commands/index.js +190 -2
- package/src/templates/commands/opencode.js +139 -26
- package/src/templates/skill-md.js +180 -0
- package/src/templates/skills/index.js +208 -13
package/README.md
CHANGED
|
@@ -63,6 +63,9 @@ npx opensddrag init --server http://localhost:8000 --project my-app --yes
|
|
|
63
63
|
# Remote server with an API key
|
|
64
64
|
npx opensddrag init --server https://sdd.example.com --api-key sk-... --yes
|
|
65
65
|
|
|
66
|
+
# CI — configure via env vars, no flags needed
|
|
67
|
+
OPENSDDRAG_SERVER_URL=http://mcp.internal:8000 OPENSDDRAG_API_KEY=sk-... npx opensddrag init --project my-app --yes
|
|
68
|
+
|
|
66
69
|
# Configure only OpenCode (skip Claude Code)
|
|
67
70
|
npx opensddrag init --tools opencode
|
|
68
71
|
```
|
|
@@ -74,8 +77,11 @@ npx opensddrag init --tools opencode
|
|
|
74
77
|
| `.mcp.json` | Claude Code MCP server entry (`type: http`) |
|
|
75
78
|
| `opencode.json` | OpenCode MCP server entry (only with `--tools opencode`) |
|
|
76
79
|
| `.claude/skills/opensddrag-*/SKILL.md` | One skill file per SDD command |
|
|
80
|
+
| `.claude/skills/opensddrag-harness/SKILL.md` | Harness rule management skill |
|
|
77
81
|
| `.agents/skills/opensddrag-*/SKILL.md` | Same skills for agent-native tooling |
|
|
82
|
+
| `.agents/skills/opensddrag-harness/SKILL.md` | Same harness skill for agent-native tooling |
|
|
78
83
|
| `.opencode/skills/opensddrag-*/SKILL.md` | OpenCode-native skills (when selected) |
|
|
84
|
+
| `.opencode/skills/opensddrag-harness/SKILL.md` | OpenCode-native harness skill (when selected) |
|
|
79
85
|
| `.claude/commands/opsr/*.md` | Claude Code slash commands |
|
|
80
86
|
| `.opencode/commands/opsr/*.md` | OpenCode slash commands (when selected) |
|
|
81
87
|
| `CLAUDE.md` | OpenSddRag section appended (or file created) |
|
|
@@ -87,8 +93,14 @@ npx opensddrag init --tools opencode
|
|
|
87
93
|
|
|
88
94
|
Check whether the current project is correctly wired to the server.
|
|
89
95
|
|
|
96
|
+
```
|
|
97
|
+
Options:
|
|
98
|
+
--server <url> Override the server URL for this check
|
|
99
|
+
```
|
|
100
|
+
|
|
90
101
|
```bash
|
|
91
102
|
npx opensddrag status
|
|
103
|
+
npx opensddrag status --server http://mcp.internal:8000
|
|
92
104
|
```
|
|
93
105
|
|
|
94
106
|
Reports:
|
|
@@ -99,6 +111,32 @@ Reports:
|
|
|
99
111
|
- `CLAUDE.md` — whether the OpenSddRag section exists
|
|
100
112
|
- Server — live health check + project registration confirmation
|
|
101
113
|
|
|
114
|
+
## Environment Variables
|
|
115
|
+
|
|
116
|
+
Both `init` and `status` support configuration via environment variables. Precedence order (first match wins):
|
|
117
|
+
|
|
118
|
+
1. `--server` / `--api-key` CLI flags
|
|
119
|
+
2. Environment variables
|
|
120
|
+
3. `opensddrag.yaml` (`server_url` or `server` field)
|
|
121
|
+
4. Built-in default (`http://localhost:8000`)
|
|
122
|
+
|
|
123
|
+
| Variable | Purpose |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `OPENSDDRAG_SERVER_URL` | MCP server URL — used when no `--server` flag is passed |
|
|
126
|
+
| `OPENSDDRAG_API_KEY` | API key for authenticated servers — used when no `--api-key` flag is passed |
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Connect to a remote server without flags
|
|
130
|
+
export OPENSDDRAG_SERVER_URL=http://mcp.internal:8000
|
|
131
|
+
export OPENSDDRAG_API_KEY=sk-abc123
|
|
132
|
+
npx opensddrag init --project my-app --yes
|
|
133
|
+
npx opensddrag status
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`OPENSDDRAG_API_KEY` is honored for any server URL — including `localhost` — so local servers with `AUTH_ENABLED=true` work correctly.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
102
140
|
## SDD Slash Commands
|
|
103
141
|
|
|
104
142
|
After `init`, the following slash commands are available inside Claude Code (prefix `/opsr:`):
|
|
@@ -118,6 +156,55 @@ After `init`, the following slash commands are available inside Claude Code (pre
|
|
|
118
156
|
| `/opsr:status` | Show what is in progress and what is done |
|
|
119
157
|
| `/opsr:flow` | Run the full SDD flow end-to-end for a feature |
|
|
120
158
|
| `/opsr:search` | Semantic search over specs and past work |
|
|
159
|
+
| `/opsr:harness` | Add, list, or disable project rules enforced at SDD phase gates |
|
|
160
|
+
|
|
161
|
+
## Harness
|
|
162
|
+
|
|
163
|
+
The Harness is a rule-gate layer built on top of the SDD workflow. It lets you define persistent, per-project behavioral rules that are automatically injected into every agent session and enforced at specific SDD phase gates.
|
|
164
|
+
|
|
165
|
+
**How it works:**
|
|
166
|
+
- Rules are stored in the MCP server's database, scoped to your project.
|
|
167
|
+
- `always` rules are returned automatically by `get_working_context` at the start of every session.
|
|
168
|
+
- Phase-gate rules (`on_spec`, `on_apply`, `on_verify`, `on_archive`) are surfaced by `get_harness_checklist` when the corresponding SDD command runs its gate step.
|
|
169
|
+
- Agents check the checklist before completing each gate action and must satisfy all `error`-severity rules.
|
|
170
|
+
|
|
171
|
+
### Triggers
|
|
172
|
+
|
|
173
|
+
| Trigger | When it fires |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `always` | Every agent session — injected via `get_working_context` |
|
|
176
|
+
| `on_spec` | Before saving a spec artifact (`/opsr:spec`) |
|
|
177
|
+
| `on_apply` | Before marking a task complete (`/opsr:apply`) |
|
|
178
|
+
| `on_verify` | Before declaring verification done (`/opsr:verify`) |
|
|
179
|
+
| `on_archive` | Before archiving change artifacts (`/opsr:archive`) |
|
|
180
|
+
|
|
181
|
+
### MCP Tools
|
|
182
|
+
|
|
183
|
+
| Tool | Description |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `add_rule` | Create or update a project rule (name, trigger, category, severity, instruction) |
|
|
186
|
+
| `list_rules` | List all rules for the project, grouped by trigger |
|
|
187
|
+
| `get_harness_checklist` | Return enabled rules for a specific trigger — called by SDD commands at phase gates |
|
|
188
|
+
|
|
189
|
+
### Managing rules
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# Inside Claude Code, after opensddrag init:
|
|
193
|
+
|
|
194
|
+
# Add a rule interactively
|
|
195
|
+
/opsr:harness add
|
|
196
|
+
|
|
197
|
+
# Add a rule directly
|
|
198
|
+
/opsr:harness add name=no-raw-sql trigger=on_apply category=forbidden severity=error instruction="Never write raw SQL strings — use the repository layer"
|
|
199
|
+
|
|
200
|
+
# List all rules
|
|
201
|
+
/opsr:harness list
|
|
202
|
+
|
|
203
|
+
# Disable a rule
|
|
204
|
+
/opsr:harness disable no-raw-sql
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
121
208
|
|
|
122
209
|
## Server Setup
|
|
123
210
|
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -5,10 +5,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
5
5
|
import { join, basename } from "path";
|
|
6
6
|
|
|
7
7
|
import { checkHealth, createProject } from "../api.js";
|
|
8
|
+
import { DEFAULT_SERVER_URL, resolveServerUrl, resolveApiKey } from "../config.js";
|
|
8
9
|
import { renderClaudeMdBlock, renderClaudeMdStandalone } from "../templates/claude-md.js";
|
|
9
10
|
import { getCommands } from "../templates/commands/index.js";
|
|
10
11
|
import { getOpenCodeCommands } from "../templates/commands/opencode.js";
|
|
11
|
-
import { getSkills } from "../templates/skills/index.js";
|
|
12
|
+
import { getSkills, getOpenCodeSkills } from "../templates/skills/index.js";
|
|
13
|
+
import { getHarnessSkill, getOpenCodeHarnessSkill } from "../templates/skill-md.js";
|
|
12
14
|
|
|
13
15
|
// ── Config writers ─────────────────────────────────────────────────────────────
|
|
14
16
|
|
|
@@ -40,6 +42,9 @@ function writeOpenCode(cwd, serverUrl, _apiKey) {
|
|
|
40
42
|
url: `${serverUrl}/mcp`,
|
|
41
43
|
enabled: true,
|
|
42
44
|
};
|
|
45
|
+
if (_apiKey) {
|
|
46
|
+
config.mcp.opensddrag.headers = { Authorization: `Bearer ${_apiKey}` };
|
|
47
|
+
}
|
|
43
48
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
44
49
|
return "opencode.json";
|
|
45
50
|
}
|
|
@@ -55,7 +60,7 @@ export const initCommand = new Command("init")
|
|
|
55
60
|
.description("Connect the current project to an OpenSddRag MCP server")
|
|
56
61
|
.option("--project <slug>", "Project slug (default: current directory name)")
|
|
57
62
|
.option("--name <name>", "Project display name")
|
|
58
|
-
.option("--server <url>", "OpenSddRag server URL"
|
|
63
|
+
.option("--server <url>", "OpenSddRag server URL")
|
|
59
64
|
.option("--api-key <key>", "API key for authenticated servers")
|
|
60
65
|
.option("--tools <list>", "Comma-separated tools to configure: claude,opencode (default: ask)")
|
|
61
66
|
.option("--yes", "Skip confirmation prompts")
|
|
@@ -65,13 +70,16 @@ export const initCommand = new Command("init")
|
|
|
65
70
|
console.log(chalk.bold("\n OpenSddRag — Project Init\n"));
|
|
66
71
|
|
|
67
72
|
// ── Inputs ────────────────────────────────────────────────────────────────
|
|
68
|
-
const
|
|
69
|
-
|
|
73
|
+
const resolvedUrl = resolveServerUrl(opts, cwd);
|
|
74
|
+
let serverUrl;
|
|
75
|
+
if (opts.yes || resolvedUrl !== DEFAULT_SERVER_URL) {
|
|
76
|
+
serverUrl = resolvedUrl;
|
|
77
|
+
} else {
|
|
78
|
+
serverUrl = await input({ message: "OpenSddRag server URL:", default: DEFAULT_SERVER_URL });
|
|
79
|
+
}
|
|
70
80
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let apiKey = opts.apiKey || null;
|
|
74
|
-
if (!apiKey && isRemote && !opts.yes) {
|
|
81
|
+
let apiKey = resolveApiKey(opts);
|
|
82
|
+
if (!apiKey && !opts.yes) {
|
|
75
83
|
apiKey = await password({
|
|
76
84
|
message: "API key (leave blank to skip — server must have AUTH_ENABLED=false):",
|
|
77
85
|
mask: "*",
|
|
@@ -114,15 +122,15 @@ export const initCommand = new Command("init")
|
|
|
114
122
|
console.log("\n" + chalk.dim(" Will create/update:"));
|
|
115
123
|
if (selectedTools.includes("Claude Code")) {
|
|
116
124
|
console.log(chalk.dim(" .mcp.json — MCP server (type: http)"));
|
|
125
|
+
console.log(chalk.dim(" .claude/skills/opensddrag-*/SKILL.md — individual skill per command"));
|
|
126
|
+
console.log(chalk.dim(" .agents/skills/opensddrag-*/SKILL.md — individual skill per command"));
|
|
127
|
+
console.log(chalk.dim(" .claude/commands/opsr/ — slash commands (/opsr:propose, /opsr:apply, /opsr:harness...)"));
|
|
117
128
|
}
|
|
118
|
-
console.log(chalk.dim(" .claude/skills/opensddrag-*/SKILL.md — individual skill per command"));
|
|
119
|
-
console.log(chalk.dim(" .agents/skills/opensddrag-*/SKILL.md — individual skill per command"));
|
|
120
129
|
if (selectedTools.includes("OpenCode")) {
|
|
121
|
-
console.log(chalk.dim(" opencode.json
|
|
130
|
+
console.log(chalk.dim(" opencode.json — MCP server"));
|
|
122
131
|
console.log(chalk.dim(" .opencode/skills/opensddrag-*/SKILL.md — OpenCode-native skills"));
|
|
123
|
-
console.log(chalk.dim(" .opencode/commands/opsr/
|
|
132
|
+
console.log(chalk.dim(" .opencode/commands/opsr/ — slash commands (/opsr:propose, /opsr:apply...)"));
|
|
124
133
|
}
|
|
125
|
-
console.log(chalk.dim(" .claude/commands/opsr/ — slash commands (/opsr:propose, /opsr:apply...)"));
|
|
126
134
|
console.log(chalk.dim(" CLAUDE.md — OpenSddRag section"));
|
|
127
135
|
console.log(chalk.dim(` Remote: register '${slug}' in central database\n`));
|
|
128
136
|
|
|
@@ -138,9 +146,13 @@ export const initCommand = new Command("init")
|
|
|
138
146
|
console.log(chalk.green("✓"));
|
|
139
147
|
} catch {
|
|
140
148
|
console.log(chalk.red("✗"));
|
|
149
|
+
const isLocal = serverUrl.includes("localhost") || serverUrl.includes("127.0.0.1");
|
|
141
150
|
console.error(chalk.red(`\n Cannot reach ${serverUrl}`));
|
|
142
|
-
|
|
143
|
-
|
|
151
|
+
if (isLocal) {
|
|
152
|
+
console.error(chalk.dim(" Make sure the OpenSddRag server is running:"));
|
|
153
|
+
console.error(chalk.dim(" docker compose up -d"));
|
|
154
|
+
}
|
|
155
|
+
console.error(chalk.dim(" Pass --server <url> or set OPENSDDRAG_SERVER_URL."));
|
|
144
156
|
process.exit(1);
|
|
145
157
|
}
|
|
146
158
|
|
|
@@ -165,57 +177,60 @@ export const initCommand = new Command("init")
|
|
|
165
177
|
const file = TOOL_WRITERS[tool](cwd, serverUrl, apiKey);
|
|
166
178
|
configured.push(`${tool} → ${file}`);
|
|
167
179
|
}
|
|
168
|
-
// Individual skill files per command
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
for (const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
// Individual skill files per command — each tool gets its own generator
|
|
181
|
+
if (selectedTools.includes("Claude Code")) {
|
|
182
|
+
const ccSkills = [
|
|
183
|
+
...getSkills(slug, serverUrl),
|
|
184
|
+
{ name: "opensddrag-harness", content: getHarnessSkill({ slug, serverUrl }) },
|
|
185
|
+
];
|
|
186
|
+
const ccRoots = [join(cwd, ".claude", "skills"), join(cwd, ".agents", "skills")];
|
|
187
|
+
for (const skill of ccSkills) {
|
|
188
|
+
for (const root of ccRoots) {
|
|
189
|
+
const skillDir = join(root, skill.name);
|
|
190
|
+
mkdirSync(skillDir, { recursive: true });
|
|
191
|
+
writeFileSync(join(skillDir, "SKILL.md"), skill.content);
|
|
192
|
+
}
|
|
193
|
+
configured.push(`skill → ${skill.name}/SKILL.md (${ccRoots.length} root(s))`);
|
|
179
194
|
}
|
|
180
|
-
configured.push(`skill → .claude/skills/${skill.name}/SKILL.md`);
|
|
181
195
|
}
|
|
182
|
-
|
|
183
|
-
// Install OpenCode-native skills if OpenCode is selected
|
|
184
196
|
if (selectedTools.includes("OpenCode")) {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
197
|
+
const ocSkills = [
|
|
198
|
+
...getOpenCodeSkills(slug, serverUrl),
|
|
199
|
+
{ name: "opensddrag-harness", content: getOpenCodeHarnessSkill({ slug, serverUrl }) },
|
|
200
|
+
];
|
|
201
|
+
const ocRoot = join(cwd, ".opencode", "skills");
|
|
202
|
+
for (const skill of ocSkills) {
|
|
203
|
+
const skillDir = join(ocRoot, skill.name);
|
|
188
204
|
mkdirSync(skillDir, { recursive: true });
|
|
189
205
|
writeFileSync(join(skillDir, "SKILL.md"), skill.content);
|
|
190
|
-
configured.push(`skill →
|
|
206
|
+
configured.push(`skill → ${skill.name}/SKILL.md (1 root(s))`);
|
|
191
207
|
}
|
|
192
208
|
}
|
|
193
209
|
console.log(chalk.green("✓"));
|
|
194
210
|
for (const c of configured) console.log(chalk.dim(` ${c}`));
|
|
195
211
|
|
|
196
|
-
// ── 4. Slash commands
|
|
212
|
+
// ── 4. Slash commands ────────────────────────────────────────────────────
|
|
197
213
|
process.stdout.write(chalk.bold(" 4/5 ") + "Writing slash commands... ");
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
let totalCommands = 0;
|
|
215
|
+
if (selectedTools.includes("Claude Code")) {
|
|
216
|
+
const commands = getCommands(slug, serverUrl);
|
|
217
|
+
for (const cmd of commands) {
|
|
218
|
+
const cmdDir = join(cwd, ".claude", "commands", cmd.folder);
|
|
219
|
+
mkdirSync(cmdDir, { recursive: true });
|
|
220
|
+
writeFileSync(join(cmdDir, `${cmd.name}.md`), cmd.content);
|
|
221
|
+
}
|
|
222
|
+
totalCommands += commands.length;
|
|
207
223
|
}
|
|
208
|
-
|
|
209
|
-
// Install OpenCode-native commands if OpenCode is selected
|
|
210
224
|
if (selectedTools.includes("OpenCode")) {
|
|
211
225
|
const opencodeCommands = getOpenCodeCommands(slug, serverUrl);
|
|
212
226
|
for (const cmd of opencodeCommands) {
|
|
213
227
|
const cmdDir = join(cwd, ".opencode", "commands", cmd.folder);
|
|
214
228
|
mkdirSync(cmdDir, { recursive: true });
|
|
215
229
|
writeFileSync(join(cmdDir, `${cmd.name}.md`), cmd.content);
|
|
216
|
-
configured.push(`command → .opencode/commands/${cmd.folder}/${cmd.name}.md`);
|
|
217
230
|
}
|
|
231
|
+
totalCommands += opencodeCommands.length;
|
|
218
232
|
}
|
|
233
|
+
console.log(chalk.green(`✓ (${totalCommands} commands)`));
|
|
219
234
|
|
|
220
235
|
// ── 5. CLAUDE.md ──────────────────────────────────────────────────────────
|
|
221
236
|
process.stdout.write(chalk.bold(" 5/5 ") + "Updating CLAUDE.md... ");
|
package/src/commands/status.js
CHANGED
|
@@ -4,25 +4,22 @@ import { existsSync, readFileSync } from "fs";
|
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
|
|
6
6
|
import { checkHealth, listProjects } from "../api.js";
|
|
7
|
+
import { resolveServerUrl, loadOpensddragYaml } from "../config.js";
|
|
7
8
|
|
|
8
9
|
export const statusCommand = new Command("status")
|
|
9
10
|
.description("Check OpenSddRag connection status for the current project")
|
|
10
|
-
.
|
|
11
|
+
.option("--server <url>", "OpenSddRag server URL")
|
|
12
|
+
.action(async (opts) => {
|
|
11
13
|
const cwd = process.cwd();
|
|
12
14
|
|
|
13
15
|
console.log(chalk.bold("\n OpenSddRag — Status\n"));
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const serverUrl = resolveServerUrl(opts, cwd);
|
|
18
|
+
const yaml = loadOpensddragYaml(cwd);
|
|
19
|
+
const slug = yaml?.project ?? null;
|
|
17
20
|
|
|
18
21
|
// ── Local config ──────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
if (existsSync(yamlPath)) {
|
|
21
|
-
const yaml = readFileSync(yamlPath, "utf8");
|
|
22
|
-
const slugMatch = yaml.match(/^project:\s*(.+)$/m);
|
|
23
|
-
const serverMatch = yaml.match(/^server:\s*(.+)$/m);
|
|
24
|
-
if (slugMatch) slug = slugMatch[1].trim();
|
|
25
|
-
if (serverMatch) serverUrl = serverMatch[1].trim();
|
|
22
|
+
if (yaml) {
|
|
26
23
|
console.log(chalk.green(" ✓") + ` opensddrag.yaml → project: ${chalk.cyan(slug)}`);
|
|
27
24
|
} else {
|
|
28
25
|
console.log(chalk.red(" ✗") + " opensddrag.yaml not found — run: " + chalk.dim("opensddrag init"));
|
package/src/config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_SERVER_URL = "http://localhost:8000";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse opensddrag.yaml from cwd.
|
|
8
|
+
* Reads both `server:` and `server_url:` keys for backward compatibility.
|
|
9
|
+
* Returns { project, server_url } or null on missing/malformed file.
|
|
10
|
+
*/
|
|
11
|
+
export function loadOpensddragYaml(cwd) {
|
|
12
|
+
const yamlPath = join(cwd, "opensddrag.yaml");
|
|
13
|
+
if (!existsSync(yamlPath)) return null;
|
|
14
|
+
try {
|
|
15
|
+
const text = readFileSync(yamlPath, "utf8");
|
|
16
|
+
const projectMatch = text.match(/^project:\s*["']?(.+?)["']?\s*$/m);
|
|
17
|
+
const serverMatch = text.match(/^(?:server_url|server):\s*["']?(.+?)["']?\s*$/m);
|
|
18
|
+
return {
|
|
19
|
+
project: projectMatch?.[1]?.trim() ?? null,
|
|
20
|
+
server_url: serverMatch?.[1]?.trim() ?? null,
|
|
21
|
+
};
|
|
22
|
+
} catch {
|
|
23
|
+
process.stderr.write("Warning: Ignoring malformed opensddrag.yaml\n");
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the MCP server URL with precedence:
|
|
30
|
+
* 1. opts.server (--server CLI flag)
|
|
31
|
+
* 2. OPENSDDRAG_SERVER_URL env var
|
|
32
|
+
* 3. opensddrag.yaml server_url / server field
|
|
33
|
+
* 4. DEFAULT_SERVER_URL
|
|
34
|
+
*/
|
|
35
|
+
export function resolveServerUrl(opts, cwd) {
|
|
36
|
+
if (opts?.server) return opts.server;
|
|
37
|
+
if (process.env.OPENSDDRAG_SERVER_URL) return process.env.OPENSDDRAG_SERVER_URL;
|
|
38
|
+
const yaml = loadOpensddragYaml(cwd ?? process.cwd());
|
|
39
|
+
if (yaml?.server_url) return yaml.server_url;
|
|
40
|
+
return DEFAULT_SERVER_URL;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the API key with precedence:
|
|
45
|
+
* 1. opts.apiKey (--api-key CLI flag)
|
|
46
|
+
* 2. OPENSDDRAG_API_KEY env var
|
|
47
|
+
* 3. null
|
|
48
|
+
*/
|
|
49
|
+
export function resolveApiKey(opts) {
|
|
50
|
+
if (opts?.apiKey) return opts.apiKey;
|
|
51
|
+
if (process.env.OPENSDDRAG_API_KEY) return process.env.OPENSDDRAG_API_KEY;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
@@ -6,7 +6,8 @@ export function getCommands(slug, serverUrl) {
|
|
|
6
6
|
*/
|
|
7
7
|
const artifactHeader = (name) => `> **IMPORTANT — ${name}**
|
|
8
8
|
> This command requires the **\`opensddrag\`** MCP server (${serverUrl}), configured in \`.mcp.json\`.
|
|
9
|
-
> MCP tools provided by this server: \`create_artifact\`, \`read_artifact\`, \`list_artifacts\`, \`update_artifact\`, \`validate_artifact\`, \`link_artifacts\`, \`get_relationships\`, \`search_semantic\`, \`recall_episodes\`, \`get_working_context\`, \`update_working_context\`, \`record_trace\`
|
|
9
|
+
> MCP tools provided by this server: \`create_artifact\`, \`read_artifact\`, \`list_artifacts\`, \`update_artifact\`, \`validate_artifact\`, \`link_artifacts\`, \`get_relationships\`, \`search_semantic\`, \`recall_episodes\`, \`get_working_context\`, \`update_working_context\`, \`record_trace\`, \`get_harness_checklist\`
|
|
10
|
+
|
|
10
11
|
> **If these tools are NOT in your active tool list**: STOP immediately. Do NOT investigate or try alternatives. Tell the user: "The opensddrag MCP server is not connected. Please start it (\`docker compose up -d\`) and reload the project."
|
|
11
12
|
> All artifact reads/writes go through these MCP tools. DO NOT create local files. DO NOT write markdown to disk.
|
|
12
13
|
> **project_slug for every call: \`${slug}\`**
|
|
@@ -22,12 +23,67 @@ export function getCommands(slug, serverUrl) {
|
|
|
22
23
|
*/
|
|
23
24
|
const implementHeader = (name) => `> **IMPORTANT — ${name}**
|
|
24
25
|
> This command requires the **\`opensddrag\`** MCP server (${serverUrl}), configured in \`.mcp.json\`.
|
|
25
|
-
> MCP tools provided by this server: \`read_artifact\`, \`list_artifacts\`, \`update_artifact\`, \`get_relationships\`, \`search_semantic\`, \`recall_episodes\`, \`get_working_context\`, \`update_working_context\`, \`record_trace\`
|
|
26
|
+
> MCP tools provided by this server: \`read_artifact\`, \`list_artifacts\`, \`update_artifact\`, \`get_relationships\`, \`search_semantic\`, \`recall_episodes\`, \`get_working_context\`, \`update_working_context\`, \`record_trace\`, \`get_harness_checklist\`
|
|
26
27
|
> **If these tools are NOT in your active tool list**: STOP immediately. Do NOT investigate or try alternatives. Tell the user: "The opensddrag MCP server is not connected. Please start it (\`docker compose up -d\`) and reload the project."
|
|
27
28
|
> SDD planning artifacts are read/traced via these MCP tools. Code implementation writes local files using Edit, Write, Bash — this is expected and required.
|
|
28
29
|
> **project_slug for every MCP call: \`${slug}\`**
|
|
29
30
|
|
|
30
31
|
---
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Header for harness-management commands (harness).
|
|
36
|
+
* Project rules are stored in the MCP server's project_rules table.
|
|
37
|
+
* No local file writes — all persistence is via the harness MCP tools.
|
|
38
|
+
*/
|
|
39
|
+
const harnessHeader = (name) => `> **IMPORTANT — ${name}**
|
|
40
|
+
> This command requires the **\`opensddrag\`** MCP server (${serverUrl}), configured in \`.mcp.json\`.
|
|
41
|
+
> MCP tools provided by this server: \`add_rule\`, \`list_rules\`, \`get_harness_checklist\`, \`get_working_context\`, \`record_trace\`
|
|
42
|
+
> **If these tools are NOT in your active tool list**: STOP immediately. Do NOT investigate or try alternatives. Tell the user: "The opensddrag MCP server is not connected. Please start it (\`docker compose up -d\`) and reload the project."
|
|
43
|
+
> Harness rules are persisted in the MCP server's database. Do NOT write rule definitions to local files.
|
|
44
|
+
> **project_slug for every call: \`${slug}\`**
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reusable step block that calls `get_harness_checklist(trigger=...)` and
|
|
51
|
+
* presents the results as a phase-gate checklist. Injected into `/opsr:apply`,
|
|
52
|
+
* `/opsr:verify`, `/opsr:archive`, and `/opsr:spec` per spec
|
|
53
|
+
* `harness-engineering-harness-checklist-spec` (REQ-002, REQ-003).
|
|
54
|
+
*
|
|
55
|
+
* Behavior:
|
|
56
|
+
* - If the checklist is empty, print "No harness rules for this trigger." and
|
|
57
|
+
* continue normally.
|
|
58
|
+
* - If the checklist has error-severity rules, the agent MUST confirm each
|
|
59
|
+
* one is satisfied before proceeding to the next step.
|
|
60
|
+
* - Warning-severity rules are presented as advisory (SHOULD complete).
|
|
61
|
+
*
|
|
62
|
+
* @param {string} trigger - One of: "on_apply", "on_verify", "on_archive", "on_spec"
|
|
63
|
+
* @param {string} gateLabel - Short human-readable label for the gate, e.g. "Marking task archived"
|
|
64
|
+
* @returns {string} Markdown step content
|
|
65
|
+
*/
|
|
66
|
+
const harnessChecklistStep = (trigger, gateLabel) => `## Step — Harness checklist (${trigger})
|
|
67
|
+
Call MCP tool to load all enabled harness rules for the \`${trigger}\` trigger:
|
|
68
|
+
\`get_harness_checklist(trigger="${trigger}", project_slug="${slug}")\`
|
|
69
|
+
|
|
70
|
+
Process the response as follows:
|
|
71
|
+
- **If the result is an empty array \`[]\`** → output "No harness rules for this trigger." and continue to the next step.
|
|
72
|
+
- **If any rule has \`severity="error"\`** → present it as:
|
|
73
|
+
\`\`\`
|
|
74
|
+
MUST complete before proceeding (${gateLabel}):
|
|
75
|
+
- [<name>] <instruction>
|
|
76
|
+
\`\`\`
|
|
77
|
+
Then \`STOP\` and wait for the agent/user to confirm each error-severity rule has been satisfied before continuing.
|
|
78
|
+
- **If any rule has \`severity="warning"\`** → present it as:
|
|
79
|
+
\`\`\`
|
|
80
|
+
SHOULD complete:
|
|
81
|
+
- [<name>] <instruction>
|
|
82
|
+
\`\`\`
|
|
83
|
+
These are advisory; proceed if the agent judges them satisfied, otherwise complete them first.
|
|
84
|
+
- **If any rule has \`severity="info"\`** → present it inline as "Info: [<name>] <instruction>"; proceed normally.
|
|
85
|
+
- Rules are returned sorted error-first then by name; preserve that order when displaying.
|
|
86
|
+
- This step must run BEFORE the next phase-gate step (archiving the task, finalizing verification, archiving change artifacts, or saving the spec to the database).
|
|
31
87
|
|
|
32
88
|
`;
|
|
33
89
|
|
|
@@ -242,6 +298,7 @@ If no main spec → create BOTH:
|
|
|
242
298
|
- TO: \`### Requirement: New Name\`
|
|
243
299
|
\`\`\`
|
|
244
300
|
|
|
301
|
+
${harnessChecklistStep("on_spec", "Saving specs to the database")}
|
|
245
302
|
## Step 4 — Save each spec to the database
|
|
246
303
|
For each capability spec:
|
|
247
304
|
\`create_artifact(name="<change-name>-<capability-name>-spec", type="spec", content="<full spec markdown>", metadata={"change_name": "<change-name>", "capability": "<capability-name>", "is_delta": true/false}, project_slug="${slug}")\`
|
|
@@ -425,6 +482,7 @@ For each acceptance criterion (REQ-NNN) in the task:
|
|
|
425
482
|
- Confirm the implementation satisfies the requirement
|
|
426
483
|
- Verify no spec scenarios are broken
|
|
427
484
|
|
|
485
|
+
${harnessChecklistStep("on_apply", "Marking task archived")}
|
|
428
486
|
## Step 7 — Mark task as done in database
|
|
429
487
|
\`update_artifact(name="<task-name>", status="archived", project_slug="${slug}")\`
|
|
430
488
|
|
|
@@ -475,6 +533,7 @@ For each decision:
|
|
|
475
533
|
- Check if the implementation follows the chosen approach
|
|
476
534
|
- If implementation deviates from a decision → **SUGGESTION: Possible deviation from design**
|
|
477
535
|
|
|
536
|
+
${harnessChecklistStep("on_verify", "Declaring verification complete")}
|
|
478
537
|
## Step 5 — Generate report
|
|
479
538
|
Output a structured report:
|
|
480
539
|
|
|
@@ -600,6 +659,7 @@ If delta specs exist:
|
|
|
600
659
|
- If yes → execute <opsr:sync logic for each delta spec
|
|
601
660
|
- If no → archive without syncing
|
|
602
661
|
|
|
662
|
+
${harnessChecklistStep("on_archive", "Archiving change artifacts")}
|
|
603
663
|
## Step 5 — Archive all change artifacts
|
|
604
664
|
For each artifact in this change (proposal, all specs, design, all tasks):
|
|
605
665
|
\`update_artifact(name="<artifact-name>", status="archived", metadata={"archived_at": "<ISO timestamp>", "change_name": "<change-name>"}, project_slug="${slug}")\`
|
|
@@ -831,6 +891,134 @@ Group by: this project / other projects / past actions.
|
|
|
831
891
|
|
|
832
892
|
## Step 5 — Offer to read the full artifact
|
|
833
893
|
\`read_artifact(name="<artifact-name>", project_slug="${slug}")\`
|
|
894
|
+
`,
|
|
895
|
+
},
|
|
896
|
+
|
|
897
|
+
// ── <opsr:harness ───────────────────────────────────────────────────────────
|
|
898
|
+
{
|
|
899
|
+
folder: "opsr",
|
|
900
|
+
name: "harness",
|
|
901
|
+
content: `${harnessHeader("/opsr:harness")}## Purpose
|
|
902
|
+
Manage project harness rules: add new rules, list existing rules, and disable rules that are no longer needed.
|
|
903
|
+
Harness rules are persistent behavioral constraints injected into every agent session via \`get_working_context\` (for \`trigger="always"\` rules) and surfaced as phase-gate checklists via \`get_harness_checklist\`.
|
|
904
|
+
|
|
905
|
+
## Input
|
|
906
|
+
$ARGUMENTS = one of:
|
|
907
|
+
- \`add\` — followed by rule fields or a natural-language description
|
|
908
|
+
- \`list\` — show all rules for this project
|
|
909
|
+
- \`disable <rule-name>\` — soft-delete a rule by name
|
|
910
|
+
|
|
911
|
+
If $ARGUMENTS is empty, show the current rules list and ask what the user wants to do.
|
|
912
|
+
|
|
913
|
+
## Supported rule fields
|
|
914
|
+
|
|
915
|
+
| Field | Values |
|
|
916
|
+
|-------|--------|
|
|
917
|
+
| \`name\` | kebab-case slug, unique per project |
|
|
918
|
+
| \`trigger\` | \`always\` (every session) / \`on_apply\` / \`on_verify\` / \`on_archive\` / \`on_spec\` |
|
|
919
|
+
| \`category\` | \`architecture\` / \`naming\` / \`forbidden\` / \`doc-sync\` / \`verification\` |
|
|
920
|
+
| \`severity\` | \`error\` (MUST satisfy) / \`warning\` (SHOULD satisfy) / \`info\` (advisory) |
|
|
921
|
+
| \`instruction\` | free-text rule the agent must follow |
|
|
922
|
+
| \`metadata\` | optional JSON |
|
|
923
|
+
| \`enabled\` | \`true\` (default) / \`false\` (soft-delete) |
|
|
924
|
+
|
|
925
|
+
## Step 1 — Parse the operation
|
|
926
|
+
|
|
927
|
+
If $ARGUMENTS starts with \`add\`:
|
|
928
|
+
→ Go to **Step 2A — Add**
|
|
929
|
+
|
|
930
|
+
If $ARGUMENTS starts with \`list\`:
|
|
931
|
+
→ Go to **Step 2B — List**
|
|
932
|
+
|
|
933
|
+
If $ARGUMENTS starts with \`disable <name>\`:
|
|
934
|
+
→ Go to **Step 2C — Disable**
|
|
935
|
+
|
|
936
|
+
If $ARGUMENTS is empty:
|
|
937
|
+
→ Call \`list_rules(project_slug="${slug}")\` and show the current state. Ask the user what they want to do.
|
|
938
|
+
|
|
939
|
+
## Step 2A — Add a rule
|
|
940
|
+
|
|
941
|
+
If the user provided explicit fields after \`add\` (e.g. \`add name=repo-pattern trigger=always category=architecture severity=error instruction="..."\`), use them as-is.
|
|
942
|
+
|
|
943
|
+
Otherwise, ask the user for each field. **You can infer sensible defaults from natural language:**
|
|
944
|
+
- "always update CHANGELOG when applying" → trigger=\`on_apply\`, category=\`doc-sync\`, severity=\`warning\`
|
|
945
|
+
- "never do X" → trigger=\`always\`, category=\`forbidden\`, severity=\`error\`
|
|
946
|
+
- "use Y pattern" → trigger=\`always\`, category=\`architecture\`, severity=\`warning\`
|
|
947
|
+
|
|
948
|
+
**Show the inferred values and ask for confirmation** before calling \`add_rule\`.
|
|
949
|
+
|
|
950
|
+
Then call:
|
|
951
|
+
|
|
952
|
+
\`\`\`
|
|
953
|
+
add_rule(
|
|
954
|
+
name: "<kebab-case>",
|
|
955
|
+
trigger: "<always|on_apply|on_verify|on_archive|on_spec>",
|
|
956
|
+
category: "<architecture|naming|forbidden|doc-sync|verification>",
|
|
957
|
+
severity: "<error|warning|info>",
|
|
958
|
+
instruction: "<text>",
|
|
959
|
+
project_slug:"${slug}",
|
|
960
|
+
enabled: true
|
|
961
|
+
)
|
|
962
|
+
\`\`\`
|
|
963
|
+
|
|
964
|
+
Confirm to the user:
|
|
965
|
+
"Rule '<name>' added. It will [be injected into every working context | be checked during /opsr:<trigger> when the phase completes]."
|
|
966
|
+
|
|
967
|
+
Then record the operation:
|
|
968
|
+
\`record_trace(action="add_rule", result_summary="Added harness rule: <name>", project_slug="${slug}")\`
|
|
969
|
+
|
|
970
|
+
## Step 2B — List rules
|
|
971
|
+
|
|
972
|
+
Call:
|
|
973
|
+
|
|
974
|
+
\`list_rules(project_slug="${slug}", enabled_only=true)\`
|
|
975
|
+
|
|
976
|
+
Present the results **grouped by trigger**, in this order:
|
|
977
|
+
|
|
978
|
+
\`\`\`
|
|
979
|
+
### Always (loaded at session start)
|
|
980
|
+
- [<category>:<severity>] <name> — <truncated instruction (max 80 chars)>
|
|
981
|
+
|
|
982
|
+
### On Apply (checked during /opsr:apply)
|
|
983
|
+
- ...
|
|
984
|
+
|
|
985
|
+
### On Verify (checked during /opsr:verify)
|
|
986
|
+
- ...
|
|
987
|
+
|
|
988
|
+
### On Archive (checked during /opsr:archive)
|
|
989
|
+
- ...
|
|
990
|
+
|
|
991
|
+
### On Spec (checked during /opsr:spec)
|
|
992
|
+
- ...
|
|
993
|
+
\`\`\`
|
|
994
|
+
|
|
995
|
+
If the result list is empty, respond:
|
|
996
|
+
"No harness rules defined for this project. Run \`/opsr:harness add\` to create the first rule."
|
|
997
|
+
|
|
998
|
+
If rules exist in only some sections, show "(none)" for empty sections.
|
|
999
|
+
|
|
1000
|
+
## Step 2C — Disable a rule
|
|
1001
|
+
|
|
1002
|
+
Extract the rule name from $ARGUMENTS (the token after \`disable\`).
|
|
1003
|
+
|
|
1004
|
+
Call:
|
|
1005
|
+
|
|
1006
|
+
\`add_rule(name: "<name>", enabled: false, project_slug: "${slug}")\`
|
|
1007
|
+
|
|
1008
|
+
(You don't need to pass the other fields — \`add_rule\` is an upsert keyed on \`(project_id, name)\` and will set the existing rule's \`enabled\` flag to \`false\`.)
|
|
1009
|
+
|
|
1010
|
+
Confirm:
|
|
1011
|
+
"Rule '<name>' disabled. It will no longer appear in checklists or session context. Re-add with the same name to re-enable."
|
|
1012
|
+
|
|
1013
|
+
Then record:
|
|
1014
|
+
\`record_trace(action="disable_rule", result_summary="Disabled harness rule: <name>", project_slug="${slug}")\`
|
|
1015
|
+
|
|
1016
|
+
## Notes
|
|
1017
|
+
|
|
1018
|
+
- The same \`add_rule\` MCP call is used for create, update, and soft-delete — it is idempotent on \`(project_id, name)\`.
|
|
1019
|
+
- Disabled rules are preserved in the database and can be re-enabled by calling \`add_rule\` with the same \`name\` and \`enabled=true\`.
|
|
1020
|
+
- Rules are project-scoped — they never leak across projects.
|
|
1021
|
+
- The OpenCode equivalent of this command is the \`opensddrag-harness\` skill, which calls the same MCP tools.
|
|
834
1022
|
`,
|
|
835
1023
|
},
|
|
836
1024
|
];
|