openspecpm 0.1.0-alpha.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.
- package/CHANGELOG.md +86 -0
- package/LICENSE +21 -0
- package/README.md +352 -0
- package/cli/bin/openspecpm.js +198 -0
- package/cli/src/adapters/azure.js +230 -0
- package/cli/src/adapters/base.js +86 -0
- package/cli/src/adapters/github.js +224 -0
- package/cli/src/adapters/gitlab.js +170 -0
- package/cli/src/adapters/index.js +54 -0
- package/cli/src/adapters/jira.js +228 -0
- package/cli/src/adapters/linear.js +261 -0
- package/cli/src/audit.js +78 -0
- package/cli/src/bdd/linter.js +183 -0
- package/cli/src/bdd/templates.js +172 -0
- package/cli/src/commands/assign.js +78 -0
- package/cli/src/commands/blocked.js +17 -0
- package/cli/src/commands/bug-report.js +78 -0
- package/cli/src/commands/bulk.js +67 -0
- package/cli/src/commands/comment.js +83 -0
- package/cli/src/commands/decompose.js +106 -0
- package/cli/src/commands/doctor.js +61 -0
- package/cli/src/commands/fan-out.js +79 -0
- package/cli/src/commands/help.js +69 -0
- package/cli/src/commands/init.js +111 -0
- package/cli/src/commands/next.js +18 -0
- package/cli/src/commands/propose.js +67 -0
- package/cli/src/commands/reconcile.js +74 -0
- package/cli/src/commands/search.js +52 -0
- package/cli/src/commands/ship.js +79 -0
- package/cli/src/commands/standup.js +42 -0
- package/cli/src/commands/status.js +29 -0
- package/cli/src/commands/sync.js +128 -0
- package/cli/src/commands/validate.js +79 -0
- package/cli/src/commands/watch.js +67 -0
- package/cli/src/config.js +43 -0
- package/cli/src/frontmatter.js +22 -0
- package/cli/src/http.js +92 -0
- package/cli/src/install-hints.js +44 -0
- package/cli/src/notify.js +56 -0
- package/cli/src/openspec-bridge.js +64 -0
- package/cli/src/ratelimit.js +50 -0
- package/cli/src/telemetry.js +45 -0
- package/cli/src/tracking.js +197 -0
- package/package.json +60 -0
- package/skill/openspecpm/SKILL.md +74 -0
- package/skill/openspecpm/references/conventions.md +105 -0
- package/skill/openspecpm/references/execute.md +62 -0
- package/skill/openspecpm/references/plan.md +47 -0
- package/skill/openspecpm/references/structure.md +52 -0
- package/skill/openspecpm/references/sync.md +56 -0
- package/skill/openspecpm/references/track.md +55 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { OPENSPEC_CHANGES_DIR } from './openspec-bridge.js';
|
|
5
|
+
import * as fm from './frontmatter.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} TaskItem
|
|
9
|
+
* @property {string} title
|
|
10
|
+
* @property {'pending'|'created'|'failed'} sync_state
|
|
11
|
+
* @property {string} [external_id]
|
|
12
|
+
* @property {string} [external_url]
|
|
13
|
+
* @property {string[]} [depends_on]
|
|
14
|
+
* @property {boolean} [done]
|
|
15
|
+
*
|
|
16
|
+
* @typedef {Object} ChangeView
|
|
17
|
+
* @property {string} name
|
|
18
|
+
* @property {string} dir
|
|
19
|
+
* @property {Object} proposal // frontmatter from proposal.md
|
|
20
|
+
* @property {TaskItem[]} items
|
|
21
|
+
* @property {Date} mtime // most recent mtime across change files
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export async function listChanges(cwd = process.cwd()) {
|
|
25
|
+
const dir = join(cwd, OPENSPEC_CHANGES_DIR);
|
|
26
|
+
if (!existsSync(dir)) return [];
|
|
27
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
28
|
+
const names = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const name of names) out.push(await loadChange(name, cwd));
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function loadChange(name, cwd = process.cwd()) {
|
|
35
|
+
const dir = join(cwd, OPENSPEC_CHANGES_DIR, name);
|
|
36
|
+
let proposal = {};
|
|
37
|
+
const proposalPath = join(dir, 'proposal.md');
|
|
38
|
+
if (existsSync(proposalPath)) {
|
|
39
|
+
const raw = await readFile(proposalPath, 'utf8');
|
|
40
|
+
proposal = fm.parse(raw).data ?? {};
|
|
41
|
+
}
|
|
42
|
+
let items = [];
|
|
43
|
+
const tasksPath = join(dir, 'tasks.md');
|
|
44
|
+
if (existsSync(tasksPath)) {
|
|
45
|
+
const raw = await readFile(tasksPath, 'utf8');
|
|
46
|
+
const { data, body } = fm.parse(raw);
|
|
47
|
+
items = data.items ?? parseChecklist(body);
|
|
48
|
+
}
|
|
49
|
+
const mtime = await mostRecentMtime(dir);
|
|
50
|
+
return { name, dir, proposal, items, mtime };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a dep token against a change. Tokens may be:
|
|
55
|
+
* "task title" — same-change reference by title
|
|
56
|
+
* "<external-id>" — same-change reference by remote id
|
|
57
|
+
* "<feature>/<task title>" — cross-change reference by title
|
|
58
|
+
* "<feature>/<external-id>" — cross-change reference by remote id
|
|
59
|
+
*/
|
|
60
|
+
function resolveDep(dep, change, allChanges) {
|
|
61
|
+
const cross = String(dep).match(/^([^/]+)\/(.+)$/);
|
|
62
|
+
if (cross) {
|
|
63
|
+
const [, featureName, rest] = cross;
|
|
64
|
+
const target = allChanges.find((c) => c.name === featureName);
|
|
65
|
+
if (!target) return { found: false };
|
|
66
|
+
const ref = target.items.find((t) => t.title === rest) ?? target.items.find((t) => String(t.external_id) === String(rest));
|
|
67
|
+
return ref ? { found: true, ref } : { found: false };
|
|
68
|
+
}
|
|
69
|
+
const ref = change.items.find((t) => t.title === dep) ?? change.items.find((t) => String(t.external_id) === String(dep));
|
|
70
|
+
return ref ? { found: true, ref } : { found: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function unmetDeps(task, allTasks, options = {}) {
|
|
74
|
+
// Backwards compat: when called with a flat task list, treat as same-change.
|
|
75
|
+
// When called with options.change + options.allChanges, support cross-feature deps.
|
|
76
|
+
const deps = task.depends_on ?? [];
|
|
77
|
+
if (!deps.length) return [];
|
|
78
|
+
|
|
79
|
+
const unmet = [];
|
|
80
|
+
if (options.change && options.allChanges) {
|
|
81
|
+
for (const dep of deps) {
|
|
82
|
+
const r = resolveDep(dep, options.change, options.allChanges);
|
|
83
|
+
if (!r.found) { unmet.push({ dep, reason: 'not-found' }); continue; }
|
|
84
|
+
const ref = r.ref;
|
|
85
|
+
if (ref.done || (ref.sync_state === 'created' && ref.closed)) continue;
|
|
86
|
+
unmet.push({ dep, reason: ref.sync_state === 'failed' ? 'dep-failed' : 'dep-open' });
|
|
87
|
+
}
|
|
88
|
+
return unmet;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Legacy path
|
|
92
|
+
const byTitle = new Map(allTasks.map((t) => [t.title, t]));
|
|
93
|
+
const byId = new Map(allTasks.filter((t) => t.external_id).map((t) => [String(t.external_id), t]));
|
|
94
|
+
for (const dep of deps) {
|
|
95
|
+
const cross = String(dep).match(/^([^/]+)\/(.+)$/);
|
|
96
|
+
if (cross) {
|
|
97
|
+
// Cross-feature dep with legacy callsite — we can't resolve, mark not-found.
|
|
98
|
+
unmet.push({ dep, reason: 'cross-feature-unresolved' });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const ref = byTitle.get(dep) ?? byId.get(String(dep));
|
|
102
|
+
if (!ref) { unmet.push({ dep, reason: 'not-found' }); continue; }
|
|
103
|
+
if (ref.done || (ref.sync_state === 'created' && ref.closed)) continue;
|
|
104
|
+
if (!ref.done) unmet.push({ dep, reason: ref.sync_state === 'failed' ? 'dep-failed' : 'dep-open' });
|
|
105
|
+
}
|
|
106
|
+
return unmet;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function findNextTasks(changes) {
|
|
110
|
+
const candidates = [];
|
|
111
|
+
for (const change of changes) {
|
|
112
|
+
for (const task of change.items) {
|
|
113
|
+
if (task.done) continue;
|
|
114
|
+
if (task.sync_state === 'created' && task.closed) continue;
|
|
115
|
+
const unmet = unmetDeps(task, change.items, { change, allChanges: changes });
|
|
116
|
+
if (unmet.length === 0) candidates.push({ change: change.name, task });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return candidates;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function findBlockedTasks(changes) {
|
|
123
|
+
const blocked = [];
|
|
124
|
+
for (const change of changes) {
|
|
125
|
+
for (const task of change.items) {
|
|
126
|
+
if (task.done) continue;
|
|
127
|
+
const unmet = unmetDeps(task, change.items, { change, allChanges: changes });
|
|
128
|
+
if (unmet.length > 0) blocked.push({ change: change.name, task, unmet });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return blocked;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function findRecentUpdates(changes, sinceMs = 24 * 60 * 60 * 1000) {
|
|
135
|
+
const cutoff = Date.now() - sinceMs;
|
|
136
|
+
const recent = [];
|
|
137
|
+
for (const change of changes) {
|
|
138
|
+
const updatesDir = join(change.dir, 'updates');
|
|
139
|
+
if (!existsSync(updatesDir)) continue;
|
|
140
|
+
const tasks = await readdir(updatesDir, { withFileTypes: true });
|
|
141
|
+
for (const t of tasks) {
|
|
142
|
+
if (!t.isDirectory()) continue;
|
|
143
|
+
const progressPath = join(updatesDir, t.name, 'progress.md');
|
|
144
|
+
if (!existsSync(progressPath)) continue;
|
|
145
|
+
const s = await stat(progressPath);
|
|
146
|
+
if (s.mtimeMs >= cutoff) {
|
|
147
|
+
recent.push({ change: change.name, task: t.name, path: progressPath, mtime: s.mtime });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
recent.sort((a, b) => b.mtime - a.mtime);
|
|
152
|
+
return recent;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function summarizeChange(change) {
|
|
156
|
+
const total = change.items.length;
|
|
157
|
+
const counts = { pending: 0, created: 0, failed: 0, done: 0 };
|
|
158
|
+
for (const t of change.items) {
|
|
159
|
+
if (t.done) counts.done++;
|
|
160
|
+
else counts[t.sync_state ?? 'pending']++;
|
|
161
|
+
}
|
|
162
|
+
return { total, counts };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseChecklist(body) {
|
|
166
|
+
const items = [];
|
|
167
|
+
for (const line of (body ?? '').split(/\r?\n/)) {
|
|
168
|
+
const m = line.match(/^\s*-\s*\[( |x|X)\]\s+(.+?)\s*$/);
|
|
169
|
+
if (m) items.push({ title: m[2], done: m[1].toLowerCase() === 'x', sync_state: 'pending' });
|
|
170
|
+
}
|
|
171
|
+
return items;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function mostRecentMtime(dir) {
|
|
175
|
+
let latest = 0;
|
|
176
|
+
async function walk(d) {
|
|
177
|
+
let entries;
|
|
178
|
+
try {
|
|
179
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
180
|
+
} catch {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
for (const e of entries) {
|
|
184
|
+
const p = join(d, e.name);
|
|
185
|
+
if (e.isDirectory()) {
|
|
186
|
+
await walk(p);
|
|
187
|
+
} else {
|
|
188
|
+
try {
|
|
189
|
+
const s = await stat(p);
|
|
190
|
+
if (s.mtimeMs > latest) latest = s.mtimeMs;
|
|
191
|
+
} catch { /* ignore */ }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
await walk(dir);
|
|
196
|
+
return latest ? new Date(latest) : new Date(0);
|
|
197
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openspecpm",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Spec-driven, BDD-shaped project management for AI agents — OpenSpec proposals synced to GitHub, Azure DevOps, Jira, Linear, or GitLab.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openspecpm": "cli/bin/openspecpm.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli/bin",
|
|
11
|
+
"cli/src",
|
|
12
|
+
"skill",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"start": "node cli/bin/openspecpm.js",
|
|
20
|
+
"prepublishOnly": "npm test"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"openspec",
|
|
27
|
+
"ccpm",
|
|
28
|
+
"agent-skill",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"anthropic",
|
|
31
|
+
"ai-agents",
|
|
32
|
+
"project-management",
|
|
33
|
+
"bdd",
|
|
34
|
+
"spec-driven",
|
|
35
|
+
"cli",
|
|
36
|
+
"nodejs",
|
|
37
|
+
"github",
|
|
38
|
+
"github-issues",
|
|
39
|
+
"azure-devops",
|
|
40
|
+
"jira",
|
|
41
|
+
"linear",
|
|
42
|
+
"gitlab"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"author": "aks-builds <its.aks@outlook.com>",
|
|
46
|
+
"homepage": "https://github.com/aks-builds/openspecpm#readme",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/aks-builds/openspecpm.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/aks-builds/openspecpm/issues"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@clack/prompts": "^1.4.0",
|
|
56
|
+
"commander": "^14.0.3",
|
|
57
|
+
"execa": "^9.4.0",
|
|
58
|
+
"yaml": "^2.6.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +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.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Conventions
|
|
2
|
+
|
|
3
|
+
These rules apply to every phase of OpenSpecPM. Read this before touching files.
|
|
4
|
+
|
|
5
|
+
## Paths
|
|
6
|
+
|
|
7
|
+
| Artifact | Path |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Project config | `.openspecpm/config.json` (chosen adapter, repo/org/project identifiers) |
|
|
10
|
+
| Project state | `.openspecpm/state.json` (not committed; idempotency hints) |
|
|
11
|
+
| OpenSpec changes | `openspec/changes/<feature>/` (owned by OpenSpec) |
|
|
12
|
+
| Proposal | `openspec/changes/<feature>/proposal.md` |
|
|
13
|
+
| Design | `openspec/changes/<feature>/design.md` |
|
|
14
|
+
| Tasks | `openspec/changes/<feature>/tasks.md` |
|
|
15
|
+
| BDD specs | `openspec/changes/<feature>/specs/*.md` |
|
|
16
|
+
| Progress (local) | `openspec/changes/<feature>/updates/<task-id>/progress.md` |
|
|
17
|
+
| Archive | `openspec/archive/<YYYY-MM-DD>-<feature>/` (owned by OpenSpec) |
|
|
18
|
+
|
|
19
|
+
The OpenSpec layout is authoritative — do not invent a parallel `.claude/...` tree. We sit *above* OpenSpec, not beside it.
|
|
20
|
+
|
|
21
|
+
## Secrets
|
|
22
|
+
|
|
23
|
+
Never write tokens to `.openspecpm/config.json`. Use environment variables:
|
|
24
|
+
|
|
25
|
+
- GitHub: `gh auth login` handles the token; no env var needed.
|
|
26
|
+
- Azure DevOps: `AZURE_DEVOPS_EXT_PAT` (Work Items: Read/Write).
|
|
27
|
+
- Jira: `JIRA_EMAIL` + `JIRA_API_TOKEN`.
|
|
28
|
+
- Linear: `LINEAR_API_KEY`.
|
|
29
|
+
- GitLab: `GITLAB_TOKEN` (`api` scope).
|
|
30
|
+
|
|
31
|
+
## Frontmatter schemas
|
|
32
|
+
|
|
33
|
+
All artifacts use YAML frontmatter. Required fields:
|
|
34
|
+
|
|
35
|
+
### proposal.md
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
---
|
|
39
|
+
name: <feature-slug>
|
|
40
|
+
status: draft | in_review | approved | shipped
|
|
41
|
+
created: <ISO-8601 timestamp>
|
|
42
|
+
schema_version: 1
|
|
43
|
+
external: # filled by `openspecpm sync`
|
|
44
|
+
github:
|
|
45
|
+
adapter: github
|
|
46
|
+
id: "42"
|
|
47
|
+
url: https://github.com/.../issues/42
|
|
48
|
+
---
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### tasks.md
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
---
|
|
55
|
+
schema_version: 1
|
|
56
|
+
items:
|
|
57
|
+
- title: "Implement X"
|
|
58
|
+
sync_state: pending | created | failed
|
|
59
|
+
external_id: "43" # filled after sync
|
|
60
|
+
external_url: https://...
|
|
61
|
+
depends_on: [] # task titles or external ids
|
|
62
|
+
parallel: true | false
|
|
63
|
+
---
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If `items:` is absent, OpenSpecPM falls back to parsing `- [ ] title` checklist lines in the body.
|
|
67
|
+
|
|
68
|
+
## BDD format
|
|
69
|
+
|
|
70
|
+
Scenarios in `specs/*.md` should use Gherkin-style triplets:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
Scenario: User toggles dark mode
|
|
74
|
+
Given the user is signed in
|
|
75
|
+
And their theme preference is "system"
|
|
76
|
+
When they select "Dark" in the appearance menu
|
|
77
|
+
Then the UI re-renders in dark theme
|
|
78
|
+
And their preference is saved to the profile
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Lint heuristics (enforced softly at `propose`, hard at `sync` in Sprint 3+):
|
|
82
|
+
|
|
83
|
+
- Each scenario has one `Given`, one `When`, one `Then` (with optional `And`s).
|
|
84
|
+
- `Then` uses an observable verb (displays, returns, stores, rejects, emails, …).
|
|
85
|
+
- Reject "should work", "should be correct", "is successful" as `Then` predicates.
|
|
86
|
+
- Reject tautological `Then` (paraphrase of the `When`).
|
|
87
|
+
|
|
88
|
+
## ISO timestamps
|
|
89
|
+
|
|
90
|
+
All `created` / `updated` / `synced` fields use ISO-8601 with timezone:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
2026-05-17T14:30:00Z
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Sync markers
|
|
97
|
+
|
|
98
|
+
Comments pushed to the PM tool are append-only and stamped:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
<!-- SYNCED: 2026-05-17T14:30:00Z -->
|
|
102
|
+
…progress narrative…
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This lets re-syncs detect what's already been sent without duplicating.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Execute — Starting work on a tracked item
|
|
2
|
+
|
|
3
|
+
**When to use this:** The user says "let's work on X", "start issue 42", "begin Task PROJ-7", or names a synced work item they want to make progress on.
|
|
4
|
+
|
|
5
|
+
## Outcome
|
|
6
|
+
|
|
7
|
+
A work item moved to `in_progress` in the PM tool, a local progress directory created, and the agent is ready to write code / docs / configuration that implements the work item.
|
|
8
|
+
|
|
9
|
+
## Flow
|
|
10
|
+
|
|
11
|
+
1. **Resolve the work item.** Match the user's reference (issue number, Jira key, ADO ID, or natural-language feature name) against the local tasks tree:
|
|
12
|
+
- Read `openspec/changes/*/tasks.md` and find the item whose `external_id` matches, or whose `title` is the closest match.
|
|
13
|
+
- If ambiguous, ask the user to disambiguate by listing the 2–3 candidates.
|
|
14
|
+
|
|
15
|
+
2. **Move the item to in-progress in the PM tool.** Via the adapter:
|
|
16
|
+
- GitHub: `updateWorkItem({ addLabels: ['in-progress'] })`
|
|
17
|
+
- Azure DevOps: `updateWorkItem({ state: 'Active' })`
|
|
18
|
+
- Jira: `updateWorkItem({ transition: <to-In-Progress> })`
|
|
19
|
+
|
|
20
|
+
3. **Create the local progress directory.**
|
|
21
|
+
- Path: `openspec/changes/<feature>/updates/<task>/`
|
|
22
|
+
- Drop a stub `progress.md` with frontmatter `{started: <ISO-8601>, owner: <user>, work_item: <external_id>}`.
|
|
23
|
+
|
|
24
|
+
4. **Announce the start.** Add a comment to the work item via `addProgressComment`:
|
|
25
|
+
```
|
|
26
|
+
<!-- SYNCED: 2026-05-17T14:30:00Z -->
|
|
27
|
+
Started work on this item.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
5. **Now do the work.** Use the spec scenarios in `openspec/changes/<feature>/specs/` as the acceptance criteria. Implement code, write tests, refactor — whatever the task requires. Use the BDD scenarios as the test plan, not just the spec.
|
|
31
|
+
|
|
32
|
+
6. **Periodically broadcast progress.** Append to local `progress.md`, then sync to the PM tool with `openspecpm comment <task>` (Sprint 3). Don't post per-keystroke — once per meaningful checkpoint.
|
|
33
|
+
|
|
34
|
+
7. **When done.** Run `openspecpm ship <feature>` (Sprint 3) to close the work item with a final comment and archive the OpenSpec change.
|
|
35
|
+
|
|
36
|
+
## Worktrees: hidden by default
|
|
37
|
+
|
|
38
|
+
Engineers may want a `git worktree` per feature so concurrent work doesn't collide. To opt in:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
openspecpm start <feature> --dev
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This creates `../openspec-<feature>/` on branch `openspec/<feature>`. **For non-technical users, do not surface this.** They will be confused by ghost folders. The default `start` command (Sprint 3+) skips the worktree and works in the current checkout.
|
|
45
|
+
|
|
46
|
+
## Parallel agents
|
|
47
|
+
|
|
48
|
+
If `tasks.md` flags items with `parallel: true` and they have no `depends_on` overlap, multiple agents can take them concurrently — this is CCPM's killer feature and OpenSpecPM inherits it.
|
|
49
|
+
|
|
50
|
+
For each parallel task:
|
|
51
|
+
1. Mark each as in-progress via the adapter.
|
|
52
|
+
2. Launch a sub-agent with a focused prompt: "Implement task T from the X feature. The BDD scenarios are at specs/Y.md. Use only files under <stream-scope>."
|
|
53
|
+
3. Wait for completion, then merge.
|
|
54
|
+
|
|
55
|
+
In Sprint 3, `openspecpm fan-out <feature>` automates the launch.
|
|
56
|
+
|
|
57
|
+
## What to avoid
|
|
58
|
+
|
|
59
|
+
- Don't start work on a task that hasn't been synced — the PM tool won't track it and you'll lose traceability.
|
|
60
|
+
- Don't bypass BDD scenarios. They're the contract the proposal was approved on.
|
|
61
|
+
- Don't move multiple items to in-progress simultaneously unless they're flagged `parallel: true` — that hides bottlenecks.
|
|
62
|
+
- Don't write progress comments containing secrets, credentials, or anything you wouldn't want visible to everyone with read access to the PM tool.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Plan — Authoring a proposal
|
|
2
|
+
|
|
3
|
+
**When to use this:** the user wants to define a new feature, capture requirements, or write a spec — phrases like "let's plan X", "write a proposal for X", "spec out X", "what should X do".
|
|
4
|
+
|
|
5
|
+
## Outcome
|
|
6
|
+
|
|
7
|
+
A fully-formed OpenSpec change at `openspec/changes/<feature>/` containing:
|
|
8
|
+
|
|
9
|
+
- `proposal.md` — what we're building and why
|
|
10
|
+
- `design.md` — technical approach
|
|
11
|
+
- `tasks.md` — implementation checklist
|
|
12
|
+
- `specs/*.md` — BDD scenarios (Given/When/Then)
|
|
13
|
+
|
|
14
|
+
## Flow
|
|
15
|
+
|
|
16
|
+
1. **Clarify the feature.** Ask 1–3 sharp questions to nail down the user-visible behavior. Avoid asking about implementation yet. Examples:
|
|
17
|
+
- "Who triggers this and from where?"
|
|
18
|
+
- "What's the success outcome the user sees?"
|
|
19
|
+
- "What edge cases matter?"
|
|
20
|
+
|
|
21
|
+
2. **Run the CLI** to scaffold OpenSpec artifacts:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
openspecpm propose <feature> --prompt "<one-line description>"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This shells out to `openspec propose`. If OpenSpec is missing, the user will be guided to install it.
|
|
28
|
+
|
|
29
|
+
3. **Open the generated files** and refine. For each scenario in `specs/*.md`:
|
|
30
|
+
- Confirm one `Given`/`When`/`Then` per scenario (chained `And` lines OK).
|
|
31
|
+
- Replace vague `Then` predicates ("it works") with observable verbs.
|
|
32
|
+
- Add the edge-case scenarios surfaced in step 1.
|
|
33
|
+
|
|
34
|
+
4. **Set `status: draft → in_review`** in `proposal.md` frontmatter when the human is ready for review.
|
|
35
|
+
|
|
36
|
+
## What to avoid
|
|
37
|
+
|
|
38
|
+
- Don't write code at this stage. The proposal is requirements, not implementation.
|
|
39
|
+
- Don't fill in `external:` frontmatter yourself — `openspecpm sync` does that.
|
|
40
|
+
- Don't invent paths outside `openspec/changes/<feature>/`. OpenSpec owns the layout.
|
|
41
|
+
- Don't enforce strict Gherkin grammar on non-technical authors. The point is intent shape, not syntax.
|
|
42
|
+
|
|
43
|
+
## After this phase
|
|
44
|
+
|
|
45
|
+
- If the user is ready to push to their PM tool: route to `references/sync.md` (Sprint 2).
|
|
46
|
+
- If the user wants to decompose into tasks first: route to `references/structure.md` (Sprint 2).
|
|
47
|
+
- If the user wants to check what other changes exist: run `openspecpm status`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Structure — Decomposing a proposal into tasks
|
|
2
|
+
|
|
3
|
+
**When to use this:** A proposal exists at `openspec/changes/<feature>/proposal.md` and the user wants to break it down into individual work items.
|
|
4
|
+
|
|
5
|
+
## Outcome
|
|
6
|
+
|
|
7
|
+
A populated `openspec/changes/<feature>/tasks.md` where the `items:` frontmatter array lists every task with: title, dependency edges, parallelizability, and a `sync_state: pending` marker.
|
|
8
|
+
|
|
9
|
+
## Flow
|
|
10
|
+
|
|
11
|
+
1. **Read the proposal and specs.** Open `proposal.md`, `design.md`, and every file in `specs/`. Build a mental model of what behavior must exist for the change to be complete.
|
|
12
|
+
|
|
13
|
+
2. **Group by stream.** Most features decompose into 2–5 independent streams. Common patterns:
|
|
14
|
+
- **Data:** schema, migrations, persistence layer
|
|
15
|
+
- **Service:** business logic, validation, domain rules
|
|
16
|
+
- **API:** HTTP/RPC/CLI surface
|
|
17
|
+
- **UI:** components, flows, accessibility
|
|
18
|
+
- **Tests:** end-to-end coverage of the new behavior
|
|
19
|
+
|
|
20
|
+
Streams that can be worked in parallel get `parallel: true`. Streams that must wait on another stream get `depends_on: ["<earlier-task-title>"]`.
|
|
21
|
+
|
|
22
|
+
3. **Right-size each task.** A task should be 2–8 hours of focused work for one engineer. If a task is bigger, split. If smaller, merge into the next one (overhead exceeds value).
|
|
23
|
+
|
|
24
|
+
4. **Write tasks.md.** Use the frontmatter schema from `conventions.md`. Every task gets:
|
|
25
|
+
```yaml
|
|
26
|
+
- title: "Add user_preferences.theme column"
|
|
27
|
+
sync_state: pending
|
|
28
|
+
depends_on: []
|
|
29
|
+
parallel: true
|
|
30
|
+
effort_hours: 3
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
5. **Cross-check against BDD scenarios.** Every scenario in `specs/*.md` should map to at least one task. If a scenario is unimplemented, add a task. If a task isn't traceable to any scenario, ask whether it's actually needed.
|
|
34
|
+
|
|
35
|
+
## Hierarchy and the target PM tool
|
|
36
|
+
|
|
37
|
+
The backend's `capabilities().hierarchyDepth` determines how the structure projects into work items:
|
|
38
|
+
|
|
39
|
+
| Backend | Depth | Mapping |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| GitHub | 2 | Epic issue → sub-issues (via `gh-sub-issue`) |
|
|
42
|
+
| Linear | 2 | Project → Issues |
|
|
43
|
+
| GitLab | 2 | Parent issue → linked child issues (`relates_to` / `blocks`) |
|
|
44
|
+
| Jira | 3 | Epic → Story → Sub-task |
|
|
45
|
+
| Azure DevOps | 4 | Epic → Feature → User Story → Task |
|
|
46
|
+
|
|
47
|
+
The sync layer will **collapse levels gracefully** when authoring depth exceeds backend depth — e.g. on GitHub, "Feature" and "Story" levels are flattened into siblings of the Epic with a label tag, and a warning is printed. You do not need to author differently for each backend; author the deepest sensible structure and let the adapter compress.
|
|
48
|
+
|
|
49
|
+
## After this phase
|
|
50
|
+
|
|
51
|
+
- Route to `references/sync.md` to push tasks to the PM tool.
|
|
52
|
+
- If decomposition surfaced unknowns, route back to `references/plan.md` to refine the proposal.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Sync — Pushing an OpenSpec change to the PM tool
|
|
2
|
+
|
|
3
|
+
**When to use this:** The user has a proposal + tasks ready and wants them tracked in GitHub Issues / Azure DevOps Boards / Jira / Linear / GitLab Issues.
|
|
4
|
+
|
|
5
|
+
## Outcome
|
|
6
|
+
|
|
7
|
+
For the chosen adapter, the epic and every task are created as work items, linked into a hierarchy where the backend supports it, and recorded back into local frontmatter (`external:` on `proposal.md`, `external_id` + `external_url` on each task in `tasks.md`).
|
|
8
|
+
|
|
9
|
+
## The CLI does the work
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
openspecpm sync <feature> # create + update
|
|
13
|
+
openspecpm sync <feature> --dry-run # print the call plan, no remote writes
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Do not script `gh issue create` or hand-craft REST calls in agent reasoning — let the adapter handle vocabulary differences across backends.
|
|
17
|
+
|
|
18
|
+
## Idempotency contract
|
|
19
|
+
|
|
20
|
+
Sync is safe to re-run. The rules:
|
|
21
|
+
|
|
22
|
+
1. **Epic:** if `proposal.md` frontmatter already carries `external.<adapter>` with an `id`, the existing epic is reused. No duplicate epics are ever created.
|
|
23
|
+
2. **Tasks:** each task carries `sync_state: pending | created | failed`. `sync` skips `created` items and retries `failed` ones.
|
|
24
|
+
3. **Comments:** progress comments stamped `<!-- SYNCED: <iso-timestamp> -->` are append-only. Re-running sync after a comment was posted does not repost.
|
|
25
|
+
|
|
26
|
+
This means: if the network fails halfway through a 30-task sync, fix the cause, re-run, and only the un-created items get created.
|
|
27
|
+
|
|
28
|
+
## Capabilities and hierarchy collapse
|
|
29
|
+
|
|
30
|
+
Each adapter reports `capabilities()`. The sync layer reads `hierarchyDepth`:
|
|
31
|
+
|
|
32
|
+
- **GitHub (depth 2):** Epic issue + flat task sub-issues. If the task tree authored depth >2, intermediate levels are flattened into siblings tagged `openspec:<feature>` and a one-line warning is printed.
|
|
33
|
+
- **Linear (depth 2):** Project as epic, issues as tasks under the project. `parent` relation when supported by the workspace.
|
|
34
|
+
- **GitLab (depth 2):** Parent issue + child issues linked via `relates_to` / `blocks`. Milestone serves as the epic container if `--milestone` is supplied.
|
|
35
|
+
- **Jira (depth 3):** Epic → Story → optional Sub-task. Stories without sub-tasks just sit under the Epic via `Relates` link.
|
|
36
|
+
- **Azure DevOps (depth 4):** Epic → Feature → User Story → Task with `System.LinkTypes.Hierarchy-Reverse` Parent links.
|
|
37
|
+
|
|
38
|
+
## Field mapping per adapter
|
|
39
|
+
|
|
40
|
+
| OpenSpec field | GitHub | Azure DevOps | Jira | Linear | GitLab |
|
|
41
|
+
|---|---|---|---|---|---|
|
|
42
|
+
| `task.title` | Issue title | `System.Title` | `summary` | `title` | `title` |
|
|
43
|
+
| `task.body` | Issue body (markdown) | `System.Description` (HTML) | `description` (ADF) | `description` (markdown) | `description` (markdown) |
|
|
44
|
+
| `feature.name` (tag) | label `openspec:<name>` | tag `openspec:<name>` | label `openspec-<name>` | label `openspec:<name>` | label `openspec:<name>` |
|
|
45
|
+
| `task.depends_on` | task-list reference in body | `System.LinkTypes.Dependency` link | `Blocks` issue link | issue relation `blocks` | issue link `blocks` |
|
|
46
|
+
| `task.iteration` | (ignored, depth=2) | `System.IterationPath` | sprint custom field | `cycleId` | `milestone` |
|
|
47
|
+
| `task.assignee` | `--add-assignee` | `System.AssignedTo` | `assignee.accountId` | `assigneeId` | `assignee_ids` |
|
|
48
|
+
| `task.effort_hours` | (ignored) | `Microsoft.VSTS.Scheduling.Effort` | story-points custom field | `estimate` | `weight` |
|
|
49
|
+
|
|
50
|
+
The CLI handles the translation. Author tasks in the OpenSpec/CCPM dialect described in `conventions.md`.
|
|
51
|
+
|
|
52
|
+
## After sync
|
|
53
|
+
|
|
54
|
+
- Each item is now visible in the PM tool. The user can route stakeholders to those URLs for sign-off.
|
|
55
|
+
- Progress narrative still lives locally in `openspec/changes/<feature>/updates/<task>/progress.md`. Use `openspecpm comment <task>` (Sprint 3) to broadcast a new update to the PM tool.
|
|
56
|
+
- Route to `references/execute.md` when the user is ready to start building.
|