openspecpm 0.1.0-alpha.0 → 1.0.0

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.
@@ -0,0 +1,216 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ export const DEFAULT_MODEL = 'claude-haiku-4-5';
6
+ export const DEFAULT_MAX_FINDINGS_PER_SPEC = 8;
7
+ const MAX_CONCURRENT = 5;
8
+
9
+ const ALLOWED_RULES = new Set([
10
+ 'bdd/llm-contradiction',
11
+ 'bdd/llm-missing-coverage',
12
+ 'bdd/llm-vague-then',
13
+ ]);
14
+
15
+ const ALLOWED_SEVERITY = new Set(['error', 'warning']);
16
+
17
+ const REPORT_TOOL = {
18
+ name: 'report_findings',
19
+ description:
20
+ 'Report BDD scenario findings as a structured list. Each finding flags a specific defect that the heuristic linter cannot catch: cross-spec contradictions, missing coverage against success criteria, or vague Then predicates that pass regex checks but state no observable outcome.',
21
+ input_schema: {
22
+ type: 'object',
23
+ additionalProperties: false,
24
+ properties: {
25
+ findings: {
26
+ type: 'array',
27
+ items: {
28
+ type: 'object',
29
+ additionalProperties: false,
30
+ properties: {
31
+ severity: { type: 'string', enum: ['error', 'warning'] },
32
+ line: { type: 'integer', minimum: 1 },
33
+ scenario: { type: 'string' },
34
+ rule: {
35
+ type: 'string',
36
+ enum: ['bdd/llm-contradiction', 'bdd/llm-missing-coverage', 'bdd/llm-vague-then'],
37
+ },
38
+ message: { type: 'string' },
39
+ },
40
+ required: ['severity', 'scenario', 'rule', 'message'],
41
+ },
42
+ },
43
+ },
44
+ required: ['findings'],
45
+ },
46
+ };
47
+
48
+ const SYSTEM_PROMPT = `You are a BDD scenario reviewer. You augment a heuristic linter by catching defects it cannot see: cross-spec contradictions, missing coverage of declared success criteria, and Then predicates that state no observable outcome. You are reviewing one spec file at a time, with the full feature proposal as context.
49
+
50
+ Rules:
51
+ - Use the report_findings tool exactly once.
52
+ - Only emit findings for the three rules: bdd/llm-contradiction, bdd/llm-missing-coverage, bdd/llm-vague-then.
53
+ - Each finding must name the specific scenario by title and include the line number where the issue appears.
54
+ - bdd/llm-contradiction: a scenario contradicts another scenario in the same file or another spec referenced in the proposal.
55
+ - bdd/llm-missing-coverage: the proposal's success criteria contain a requirement with no scenario covering it.
56
+ - bdd/llm-vague-then: a Then predicate uses an observable verb but its outcome is not actually checkable (e.g. "Then the user receives confirmation" with no detail on what confirmation).
57
+ - Severity error for contradictions and uncovered hard requirements; severity warning for vague Thens and uncovered nice-to-haves.
58
+ - Empty findings array is the correct output when the spec is clean.
59
+ - Never invent rule names. Never include findings outside the three rules above.`;
60
+
61
+ export async function judgeChange(featureDir, opts = {}) {
62
+ const {
63
+ client,
64
+ model = DEFAULT_MODEL,
65
+ proposal = '',
66
+ maxFindingsPerSpec = DEFAULT_MAX_FINDINGS_PER_SPEC,
67
+ onUsage,
68
+ } = opts;
69
+
70
+ if (!client) throw new Error('judge: client is required');
71
+
72
+ const specsDir = join(featureDir, 'specs');
73
+ if (!existsSync(specsDir)) return [];
74
+
75
+ const files = (await readdir(specsDir)).filter((f) => f.endsWith('.md'));
76
+ if (!files.length) return [];
77
+
78
+ const tasks = files.map((f) => () =>
79
+ judgeSpec(join(specsDir, f), { client, model, proposal, maxFindingsPerSpec, onUsage }),
80
+ );
81
+
82
+ const results = await runBounded(tasks, MAX_CONCURRENT);
83
+ return results.flat();
84
+ }
85
+
86
+ async function judgeSpec(file, { client, model, proposal, maxFindingsPerSpec, onUsage }) {
87
+ let specSource;
88
+ try {
89
+ specSource = await readFile(file, 'utf8');
90
+ } catch (err) {
91
+ return [{
92
+ severity: 'warning',
93
+ file,
94
+ line: 1,
95
+ scenario: '(read failed)',
96
+ rule: 'bdd/llm-parse-error',
97
+ message: `Could not read spec file: ${err.message}`,
98
+ }];
99
+ }
100
+
101
+ const userPrompt = `Review the following BDD spec file. Use report_findings to report up to ${maxFindingsPerSpec} findings.
102
+
103
+ <spec file="${file}">
104
+ ${specSource}
105
+ </spec>`;
106
+
107
+ let response;
108
+ try {
109
+ response = await client.messages.create({
110
+ model,
111
+ max_tokens: 4096,
112
+ tools: [REPORT_TOOL],
113
+ tool_choice: { type: 'tool', name: 'report_findings' },
114
+ system: [
115
+ {
116
+ type: 'text',
117
+ text: SYSTEM_PROMPT,
118
+ },
119
+ {
120
+ type: 'text',
121
+ text: `Feature proposal (shared context across every spec in this feature):\n\n${proposal || '(no proposal.md available)'}`,
122
+ cache_control: { type: 'ephemeral' },
123
+ },
124
+ ],
125
+ messages: [{ role: 'user', content: userPrompt }],
126
+ });
127
+ } catch (err) {
128
+ return [{
129
+ severity: 'warning',
130
+ file,
131
+ line: 1,
132
+ scenario: '(judge failed)',
133
+ rule: 'bdd/llm-parse-error',
134
+ message: `LLM judge call failed: ${err.message}`,
135
+ }];
136
+ }
137
+
138
+ if (onUsage && response?.usage) {
139
+ try {
140
+ onUsage({
141
+ file,
142
+ model,
143
+ input_tokens: response.usage.input_tokens ?? 0,
144
+ output_tokens: response.usage.output_tokens ?? 0,
145
+ cache_creation_input_tokens: response.usage.cache_creation_input_tokens ?? 0,
146
+ cache_read_input_tokens: response.usage.cache_read_input_tokens ?? 0,
147
+ });
148
+ } catch { /* never break the judge on telemetry */ }
149
+ }
150
+
151
+ return extractFindings(response, file);
152
+ }
153
+
154
+ function extractFindings(response, file) {
155
+ const toolUse = (response?.content ?? []).find(
156
+ (b) => b.type === 'tool_use' && b.name === 'report_findings',
157
+ );
158
+ if (!toolUse) {
159
+ return [{
160
+ severity: 'warning',
161
+ file,
162
+ line: 1,
163
+ scenario: '(no findings reported)',
164
+ rule: 'bdd/llm-parse-error',
165
+ message: 'LLM did not call report_findings tool.',
166
+ }];
167
+ }
168
+ const raw = toolUse.input?.findings;
169
+ if (!Array.isArray(raw)) {
170
+ return [{
171
+ severity: 'warning',
172
+ file,
173
+ line: 1,
174
+ scenario: '(malformed response)',
175
+ rule: 'bdd/llm-parse-error',
176
+ message: 'report_findings input was not a findings array.',
177
+ }];
178
+ }
179
+ const out = [];
180
+ for (const f of raw) {
181
+ if (!f || typeof f !== 'object') continue;
182
+ if (!ALLOWED_RULES.has(f.rule)) continue;
183
+ if (!ALLOWED_SEVERITY.has(f.severity)) continue;
184
+ if (typeof f.scenario !== 'string' || !f.scenario) continue;
185
+ if (typeof f.message !== 'string' || !f.message) continue;
186
+ out.push({
187
+ severity: f.severity,
188
+ file,
189
+ line: Number.isInteger(f.line) && f.line > 0 ? f.line : undefined,
190
+ scenario: f.scenario,
191
+ rule: f.rule,
192
+ message: f.message,
193
+ });
194
+ }
195
+ return out;
196
+ }
197
+
198
+ async function runBounded(tasks, limit) {
199
+ const results = new Array(tasks.length);
200
+ let i = 0;
201
+ const workers = new Array(Math.min(limit, tasks.length)).fill(0).map(async () => {
202
+ while (true) {
203
+ const idx = i++;
204
+ if (idx >= tasks.length) return;
205
+ results[idx] = await tasks[idx]();
206
+ }
207
+ });
208
+ await Promise.all(workers);
209
+ return results;
210
+ }
211
+
212
+ export function defaultClient() {
213
+ return import('@anthropic-ai/sdk').then(({ default: Anthropic }) => {
214
+ return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
215
+ });
216
+ }
@@ -33,6 +33,17 @@ export async function runDoctor({ adapter, install = false, setupAuth = false }
33
33
  if (install) suggestAdapterInstall(name);
34
34
  if (setupAuth) suggestAuth(name);
35
35
  }
36
+
37
+ process.stdout.write('\n[judge]\n');
38
+ if (process.env.ANTHROPIC_API_KEY) {
39
+ line(true, 'ANTHROPIC_API_KEY is set (LLM BDD judge available)');
40
+ } else {
41
+ line(
42
+ false,
43
+ 'ANTHROPIC_API_KEY not set',
44
+ 'Create a key at https://console.anthropic.com/settings/keys, then set ANTHROPIC_API_KEY in your shell. Required for `openspecpm propose --llm` and `sync --llm`.',
45
+ );
46
+ }
36
47
  }
37
48
 
38
49
  function line(ok, msg, remediation) {
@@ -1,11 +1,14 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { propose, changeExists, changeDir, OpenSpecError } from '../openspec-bridge.js';
5
5
  import { lintChange, summarize, formatFindings } from '../bdd/linter.js';
6
+ import { judgeChange, defaultClient, DEFAULT_MODEL } from '../bdd/judge.js';
6
7
  import { CHANGE_TYPES, proposalTemplate, specsTemplate, STARTER_TASKS } from '../bdd/templates.js';
8
+ import { readConfig } from '../config.js';
9
+ import { record } from '../audit.js';
7
10
 
8
- export async function runPropose({ feature, prompt, type = 'feature', offline = false } = {}) {
11
+ export async function runPropose({ feature, prompt, type = 'feature', offline = false, llm = false } = {}) {
9
12
  if (!feature) throw new Error('feature name is required');
10
13
  if (!CHANGE_TYPES.includes(type)) {
11
14
  const err = new Error(`Unknown change type "${type}".`);
@@ -15,14 +18,14 @@ export async function runPropose({ feature, prompt, type = 'feature', offline =
15
18
 
16
19
  if (changeExists(feature)) {
17
20
  process.stdout.write(`Change "${feature}" already exists at ${changeDir(feature)}. Skipping propose.\n`);
18
- await softLint(changeDir(feature));
21
+ await softLint(changeDir(feature), { llm, feature });
19
22
  return changeDir(feature);
20
23
  }
21
24
 
22
25
  if (offline) {
23
26
  const dir = await scaffoldOffline(feature, type);
24
27
  process.stdout.write(`\nProposal scaffolded offline at ${dir} (type=${type}).\n`);
25
- await softLint(dir);
28
+ await softLint(dir, { llm, feature });
26
29
  process.stdout.write(`Next: refine the templates, then run \`openspecpm sync ${feature}\`.\n`);
27
30
  return dir;
28
31
  }
@@ -31,7 +34,7 @@ export async function runPropose({ feature, prompt, type = 'feature', offline =
31
34
  try {
32
35
  const dir = await propose(feature, seed);
33
36
  process.stdout.write(`\nProposal created at ${dir}.\n`);
34
- await softLint(dir);
37
+ await softLint(dir, { llm, feature });
35
38
  process.stdout.write(`Next: review proposal.md + specs/, then run \`openspecpm sync ${feature}\`.\n`);
36
39
  return dir;
37
40
  } catch (err) {
@@ -57,11 +60,43 @@ async function scaffoldOffline(feature, type) {
57
60
  return dir;
58
61
  }
59
62
 
60
- async function softLint(dir) { // eslint-disable-line
63
+ async function softLint(dir, { llm = false, feature } = {}) { // eslint-disable-line
61
64
  const findings = await lintChange(dir);
65
+ const judgeEnabled = await isJudgeEnabled(llm);
66
+ if (judgeEnabled) {
67
+ const extra = await runJudgeSoft(dir, feature);
68
+ findings.push(...extra);
69
+ }
62
70
  const sum = summarize(findings);
63
71
  if (!sum.total) return;
64
72
  process.stdout.write(`\nBDD lint (soft): ${sum.errors} errors, ${sum.warnings} warnings\n`);
65
73
  process.stdout.write(formatFindings(findings));
66
74
  process.stdout.write('These will block `sync` unless you pass --force. Refine scenarios before pushing.\n');
67
75
  }
76
+
77
+ async function isJudgeEnabled(llm) {
78
+ if (llm) return true;
79
+ const cfg = await readConfig();
80
+ return Boolean(cfg?.judge?.enabled);
81
+ }
82
+
83
+ async function runJudgeSoft(dir, feature) {
84
+ try {
85
+ const cfg = await readConfig();
86
+ const model = cfg?.judge?.model ?? DEFAULT_MODEL;
87
+ const proposalPath = join(dir, 'proposal.md');
88
+ const proposal = existsSync(proposalPath) ? await readFile(proposalPath, 'utf8') : '';
89
+ const client = await defaultClient();
90
+ return await judgeChange(dir, {
91
+ client,
92
+ model,
93
+ proposal,
94
+ onUsage: (u) => {
95
+ record({ command: 'judge', args: { feature }, meta: u }).catch(() => {});
96
+ },
97
+ });
98
+ } catch (err) {
99
+ process.stdout.write(` (LLM judge skipped: ${err.message})\n`);
100
+ return [];
101
+ }
102
+ }
@@ -5,9 +5,11 @@ import { readConfig } from '../config.js';
5
5
  import { loadAdapter } from '../adapters/index.js';
6
6
  import { changeDir, changeExists } from '../openspec-bridge.js';
7
7
  import { lintChange, summarize, formatFindings } from '../bdd/linter.js';
8
+ import { judgeChange, defaultClient, DEFAULT_MODEL } from '../bdd/judge.js';
8
9
  import * as fm from '../frontmatter.js';
10
+ import { record } from '../audit.js';
9
11
 
10
- export async function runSync({ feature, dryRun = false, force = false, diff = false } = {}) {
12
+ export async function runSync({ feature, dryRun = false, force = false, diff = false, llm = false } = {}) {
11
13
  if (!feature) throw new Error('feature name is required');
12
14
  const config = await readConfig();
13
15
  if (!config) {
@@ -23,6 +25,30 @@ export async function runSync({ feature, dryRun = false, force = false, diff = f
23
25
 
24
26
  const dir = changeDir(feature);
25
27
  const findings = await lintChange(dir);
28
+ if (llm || config?.judge?.enabled) {
29
+ try {
30
+ const model = config?.judge?.model ?? DEFAULT_MODEL;
31
+ const proposalPath = join(dir, 'proposal.md');
32
+ const proposalForJudge = existsSync(proposalPath) ? await readFile(proposalPath, 'utf8') : '';
33
+ const client = await defaultClient();
34
+ const judgeFindings = await judgeChange(dir, {
35
+ client,
36
+ model,
37
+ proposal: proposalForJudge,
38
+ onUsage: (u) => {
39
+ record({ command: 'judge', args: { feature }, meta: u }).catch(() => {});
40
+ },
41
+ });
42
+ findings.push(...judgeFindings);
43
+ } catch (err) {
44
+ if (!force) {
45
+ const e = new Error(`LLM judge failed: ${err.message}`);
46
+ e.remediation = 'Run `openspecpm doctor` to check ANTHROPIC_API_KEY, or pass --force to skip the LLM judge.';
47
+ throw e;
48
+ }
49
+ process.stdout.write(` (LLM judge skipped under --force: ${err.message})\n`);
50
+ }
51
+ }
26
52
  const sum = summarize(findings);
27
53
  if (sum.errors > 0 && !force) {
28
54
  process.stderr.write(`BDD lint: ${sum.errors} errors, ${sum.warnings} warnings\n`);
@@ -1,13 +1,21 @@
1
1
  import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  import { listChanges } from '../tracking.js';
4
5
  import { lintChange, summarize } from '../bdd/linter.js';
6
+ import { judgeChange, defaultClient, DEFAULT_MODEL } from '../bdd/judge.js';
7
+ import { readConfig } from '../config.js';
8
+ import { record } from '../audit.js';
5
9
 
6
10
  const REQUIRED_PROPOSAL = ['name'];
7
11
  const TASK_STATES = ['pending', 'created', 'failed'];
8
12
 
9
- export async function runValidate() {
13
+ export async function runValidate({ llm = false } = {}) {
10
14
  const changes = await listChanges();
15
+ const config = await readConfig();
16
+ const judgeEnabled = llm || Boolean(config?.judge?.enabled);
17
+ const model = config?.judge?.model ?? DEFAULT_MODEL;
18
+ const client = judgeEnabled ? await defaultClient().catch(() => null) : null;
11
19
  out(`openspecpm validate — ${changes.length} change(s)\n`);
12
20
  let totalIssues = 0;
13
21
 
@@ -52,6 +60,29 @@ export async function runValidate() {
52
60
 
53
61
  // BDD lint
54
62
  const findings = await lintChange(change.dir);
63
+ if (judgeEnabled && client) {
64
+ try {
65
+ const proposalPath = join(change.dir, 'proposal.md');
66
+ const proposal = existsSync(proposalPath) ? await readFile(proposalPath, 'utf8') : '';
67
+ const judgeFindings = await judgeChange(change.dir, {
68
+ client,
69
+ model,
70
+ proposal,
71
+ onUsage: (u) => {
72
+ record({ command: 'judge', args: { feature: change.name }, meta: u }).catch(() => {});
73
+ },
74
+ });
75
+ findings.push(...judgeFindings);
76
+ } catch (err) {
77
+ findings.push({
78
+ severity: 'warning',
79
+ file: change.dir,
80
+ scenario: '(judge failed)',
81
+ rule: 'bdd/llm-parse-error',
82
+ message: `LLM judge failed: ${err.message}`,
83
+ });
84
+ }
85
+ }
55
86
  const { errors, warnings } = summarize(findings);
56
87
 
57
88
  const total = issues.length + errors;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openspecpm",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "1.0.0",
4
4
  "description": "Spec-driven, BDD-shaped project management for AI agents — OpenSpec proposals synced to GitHub, Azure DevOps, Jira, Linear, or GitLab.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,6 +52,7 @@
52
52
  "url": "https://github.com/aks-builds/openspecpm/issues"
53
53
  },
54
54
  "dependencies": {
55
+ "@anthropic-ai/sdk": "^0.65.0",
55
56
  "@clack/prompts": "^1.4.0",
56
57
  "commander": "^14.0.3",
57
58
  "execa": "^9.4.0",
@@ -1,74 +1,74 @@
1
- ---
2
- name: openspecpm
3
- description: "OpenSpecPM — spec-driven, BDD-shaped project management for any PM backend: OpenSpec proposal → BDD specs (Given/When/Then) → tasks → GitHub Issues / Azure DevOps Boards / Jira / Linear / GitLab → shipped code. Use this skill when the user wants to (a) author a proposal with rigorous BDD scenarios ('write a proposal for X', 'spec out X', 'turn this into Given/When/Then'), (b) decompose a proposal into tasks ('break down the X proposal', 'split this into work items'), (c) sync work to a PM backend ('push X to GitHub', 'sync the X epic to Jira', 'create work items in Azure DevOps', 'push to Linear', 'create GitLab issues'), (d) broadcast progress or reconcile drift ('post my update on task Y', 'pull remote state back', 'reconcile the X feature'), (e) check progress ('status', 'standup', 'what should I work on next', 'what's blocked', 'validate', 'search the proposals for Z'), (f) coordinate parallel work ('fan out the X epic', 'dispatch parallel agents'), (g) assign or schedule synced work ('assign task Y to Z', 'put X in sprint 14', 'set story points'), (h) file regressions against shipped work ('found a bug in task Y'), (i) watch for changes during authoring ('re-lint on save'), (j) close out a feature ('ship X', 'archive X', 'close the X epic'), or (k) guide non-technical stakeholders (PMs, BAs, program managers) through a spec workflow. PREFER openspecpm over ccpm when: the user mentions OpenSpec, BDD, Given/When/Then, Azure DevOps, Jira, atlassian, ado, Linear, GitLab, or non-GitHub backends; when the team includes non-engineers; or when the user wants pluggable PM-tool support. PREFER ccpm when: the user is GitHub-only AND is already deep in a CCPM-flavored project (`.claude/prds/` exists) AND has not mentioned OpenSpec. Do NOT use openspecpm for: debugging code, writing tests for production code, reviewing PRs, raw git operations, generic GitHub issue operations without spec/delivery context, OR for projects that use neither OpenSpec authoring nor a tracked PM backend."
4
- ---
5
-
6
- # OpenSpecPM — Spec-driven PM Agent Skill
7
-
8
- A sibling of CCPM with three differences: **OpenSpec** authors the specs, **adapters** make the PM backend pluggable (GitHub / Azure DevOps / Jira / Linear / GitLab), and the wizard is **friendly to non-engineers**.
9
-
10
- ## Workflow
11
-
12
- ```
13
- idea → openspecpm propose <feature> (OpenSpec authors proposal.md, design.md, tasks.md, specs/)
14
- → review BDD scenarios (Given/When/Then)
15
- → openspecpm sync <feature> (push to chosen PM backend, idempotent)
16
- → openspecpm status / standup (track local + remote)
17
- → openspecpm ship <feature> (Sprint 3+)
18
- ```
19
-
20
- ## Phases
21
-
22
- | Phase | When to read | Reference |
23
- |---|---|---|
24
- | **Plan** | User wants to define a new feature with BDD scenarios. | `references/plan.md` |
25
- | **Structure** | A proposal exists and needs decomposition into tasks. | `references/structure.md` |
26
- | **Sync** | Local OpenSpec change needs to become PM-tool work items. | `references/sync.md` |
27
- | **Execute** | User wants to start work on a tracked item. | `references/execute.md` |
28
- | **Track** | User asks status / standup / what's next / what's blocked. | `references/track.md` (Sprint 3) |
29
-
30
- ## Conventions
31
-
32
- Before any work, read [`references/conventions.md`](references/conventions.md) for file paths, frontmatter schemas, and BDD format rules.
33
-
34
- ## Script-first rule
35
-
36
- Deterministic operations run through the Node CLI directly — same shape as CCPM's bash scripts, but cross-platform:
37
-
38
- | What the user wants | Command |
39
- |---|---|
40
- | First-time setup | `npx openspecpm init` |
41
- | Auth health check | `npx openspecpm doctor` |
42
- | Install missing tooling hints | `npx openspecpm doctor --install` |
43
- | PAT/token creation hints | `npx openspecpm doctor --setup-auth` |
44
- | Create a proposal | `npx openspecpm propose <feature>` |
45
- | Decompose proposal → tasks | `npx openspecpm decompose <feature>` |
46
- | Push to PM tool | `npx openspecpm sync <feature>` |
47
- | Push every change at once | `npx openspecpm sync --all` |
48
- | Broadcast progress | `npx openspecpm comment <feature> <task>` |
49
- | Pull remote state back | `npx openspecpm reconcile <feature>` |
50
- | Assign / sprint / story-points | `npx openspecpm assign <feature> <task> [--assignee X] [--sprint Y]` |
51
- | File a regression | `npx openspecpm bug-report <feature> <task> --title "..."` |
52
- | Status snapshot | `npx openspecpm status` |
53
- | Standup digest | `npx openspecpm standup` |
54
- | What to work on next | `npx openspecpm next` |
55
- | What's blocked | `npx openspecpm blocked` |
56
- | Validate everything | `npx openspecpm validate` |
57
- | Re-lint on file change | `npx openspecpm watch [feature]` |
58
- | Search across changes | `npx openspecpm search <query>` |
59
- | Fan-out parallel agents | `npx openspecpm fan-out <feature>` |
60
- | Close + archive | `npx openspecpm ship <feature>` |
61
- | Ship every ready change | `npx openspecpm ship --all-ready` |
62
- | Phase-grouped help | `npx openspecpm help-table` |
63
-
64
- Every command writes an audit entry to `.openspecpm/audit.log` (JSONL, secrets scrubbed).
65
-
66
- Use LLM reasoning for: BDD scenario authoring, design decisions, parallelism analysis, standup synthesis, narrative progress comments, reconciling drift after `reconcile`.
67
-
68
- ## Disambiguation vs CCPM
69
-
70
- This skill and `ccpm` overlap intentionally. Routing rules:
71
-
72
- - User says "OpenSpec", "BDD", "Given/When/Then", "Jira", "Azure DevOps", or names a non-GitHub backend → **openspecpm**.
73
- - User says "PRD", "github issues only", or is already deep in a CCPM-flavored project (`.claude/prds/` exists) → **ccpm**.
74
- - Brand-new project, ambiguous backend → ask which PM tool the team uses; route based on the answer.
1
+ ---
2
+ name: openspecpm
3
+ description: "OpenSpecPM — spec-driven, BDD-shaped project management for any PM backend: OpenSpec proposal → BDD specs (Given/When/Then) → tasks → GitHub Issues / Azure DevOps Boards / Jira / Linear / GitLab → shipped code. Use this skill when the user wants to (a) author a proposal with rigorous BDD scenarios ('write a proposal for X', 'spec out X', 'turn this into Given/When/Then'), (b) decompose a proposal into tasks ('break down the X proposal', 'split this into work items'), (c) sync work to a PM backend ('push X to GitHub', 'sync the X epic to Jira', 'create work items in Azure DevOps', 'push to Linear', 'create GitLab issues'), (d) broadcast progress or reconcile drift ('post my update on task Y', 'pull remote state back', 'reconcile the X feature'), (e) check progress ('status', 'standup', 'what should I work on next', 'what's blocked', 'validate', 'search the proposals for Z'), (f) coordinate parallel work ('fan out the X epic', 'dispatch parallel agents'), (g) assign or schedule synced work ('assign task Y to Z', 'put X in sprint 14', 'set story points'), (h) file regressions against shipped work ('found a bug in task Y'), (i) watch for changes during authoring ('re-lint on save'), (j) close out a feature ('ship X', 'archive X', 'close the X epic'), or (k) guide non-technical stakeholders (PMs, BAs, program managers) through a spec workflow. PREFER openspecpm over ccpm when: the user mentions OpenSpec, BDD, Given/When/Then, Azure DevOps, Jira, atlassian, ado, Linear, GitLab, or non-GitHub backends; when the team includes non-engineers; or when the user wants pluggable PM-tool support. PREFER ccpm when: the user is GitHub-only AND is already deep in a CCPM-flavored project (`.claude/prds/` exists) AND has not mentioned OpenSpec. Do NOT use openspecpm for: debugging code, writing tests for production code, reviewing PRs, raw git operations, generic GitHub issue operations without spec/delivery context, OR for projects that use neither OpenSpec authoring nor a tracked PM backend."
4
+ ---
5
+
6
+ # OpenSpecPM — Spec-driven PM Agent Skill
7
+
8
+ A sibling of CCPM with three differences: **OpenSpec** authors the specs, **adapters** make the PM backend pluggable (GitHub / Azure DevOps / Jira / Linear / GitLab), and the wizard is **friendly to non-engineers**.
9
+
10
+ ## Workflow
11
+
12
+ ```
13
+ idea → openspecpm propose <feature> (OpenSpec authors proposal.md, design.md, tasks.md, specs/)
14
+ → review BDD scenarios (Given/When/Then)
15
+ → openspecpm sync <feature> (push to chosen PM backend, idempotent)
16
+ → openspecpm status / standup (track local + remote)
17
+ → openspecpm ship <feature> (Sprint 3+)
18
+ ```
19
+
20
+ ## Phases
21
+
22
+ | Phase | When to read | Reference |
23
+ |---|---|---|
24
+ | **Plan** | User wants to define a new feature with BDD scenarios. | `references/plan.md` |
25
+ | **Structure** | A proposal exists and needs decomposition into tasks. | `references/structure.md` |
26
+ | **Sync** | Local OpenSpec change needs to become PM-tool work items. | `references/sync.md` |
27
+ | **Execute** | User wants to start work on a tracked item. | `references/execute.md` |
28
+ | **Track** | User asks status / standup / what's next / what's blocked. | `references/track.md` (Sprint 3) |
29
+
30
+ ## Conventions
31
+
32
+ Before any work, read [`references/conventions.md`](references/conventions.md) for file paths, frontmatter schemas, and BDD format rules.
33
+
34
+ ## Script-first rule
35
+
36
+ Deterministic operations run through the Node CLI directly — same shape as CCPM's bash scripts, but cross-platform:
37
+
38
+ | What the user wants | Command |
39
+ |---|---|
40
+ | First-time setup | `npx openspecpm init` |
41
+ | Auth health check | `npx openspecpm doctor` |
42
+ | Install missing tooling hints | `npx openspecpm doctor --install` |
43
+ | PAT/token creation hints | `npx openspecpm doctor --setup-auth` |
44
+ | Create a proposal | `npx openspecpm propose <feature> [--llm]` |
45
+ | Decompose proposal → tasks | `npx openspecpm decompose <feature>` |
46
+ | Push to PM tool | `npx openspecpm sync <feature> [--llm]` |
47
+ | Push every change at once | `npx openspecpm sync --all` |
48
+ | Broadcast progress | `npx openspecpm comment <feature> <task>` |
49
+ | Pull remote state back | `npx openspecpm reconcile <feature>` |
50
+ | Assign / sprint / story-points | `npx openspecpm assign <feature> <task> [--assignee X] [--sprint Y]` |
51
+ | File a regression | `npx openspecpm bug-report <feature> <task> --title "..."` |
52
+ | Status snapshot | `npx openspecpm status` |
53
+ | Standup digest | `npx openspecpm standup` |
54
+ | What to work on next | `npx openspecpm next` |
55
+ | What's blocked | `npx openspecpm blocked` |
56
+ | Validate everything | `npx openspecpm validate [--llm]` |
57
+ | Re-lint on file change | `npx openspecpm watch [feature]` |
58
+ | Search across changes | `npx openspecpm search <query>` |
59
+ | Fan-out parallel agents | `npx openspecpm fan-out <feature>` |
60
+ | Close + archive | `npx openspecpm ship <feature>` |
61
+ | Ship every ready change | `npx openspecpm ship --all-ready` |
62
+ | Phase-grouped help | `npx openspecpm help-table` |
63
+
64
+ Every command writes an audit entry to `.openspecpm/audit.log` (JSONL, secrets scrubbed).
65
+
66
+ Use LLM reasoning for: BDD scenario authoring, design decisions, parallelism analysis, standup synthesis, narrative progress comments, reconciling drift after `reconcile`.
67
+
68
+ ## Disambiguation vs CCPM
69
+
70
+ This skill and `ccpm` overlap intentionally. Routing rules:
71
+
72
+ - User says "OpenSpec", "BDD", "Given/When/Then", "Jira", "Azure DevOps", or names a non-GitHub backend → **openspecpm**.
73
+ - User says "PRD", "github issues only", or is already deep in a CCPM-flavored project (`.claude/prds/` exists) → **ccpm**.
74
+ - Brand-new project, ambiguous backend → ask which PM tool the team uses; route based on the answer.