openspecpm 0.1.0-alpha.0 → 1.0.1

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/cli/src/notify.js CHANGED
@@ -24,19 +24,42 @@ export async function notify({ config, title, body, level = 'info', fetchImpl =
24
24
  const errors = [];
25
25
  for (const t of targets) {
26
26
  try {
27
- await fetchImpl(t.url, {
27
+ const res = await fetchImpl(t.url, {
28
28
  method: 'POST',
29
29
  headers: { 'Content-Type': 'application/json' },
30
30
  body: JSON.stringify(formatPayload(t.kind, { title, body, level })),
31
31
  });
32
+ // A 401/403/500 from Slack/Teams reaches the network but the message
33
+ // never lands. Without this check, those would be counted as `sent`,
34
+ // and a user running `standup --broadcast` would think their channel
35
+ // got the update when the webhook silently rejected it.
36
+ if (!res?.ok) {
37
+ const status = res?.status ?? '?';
38
+ const statusText = res?.statusText ?? '';
39
+ let snippet = '';
40
+ try {
41
+ snippet = (await res.text()).slice(0, 200);
42
+ } catch { /* res.text() failed; ignore */ }
43
+ errors.push({ target: t.kind, error: redactUrl(t.url, `HTTP ${status} ${statusText}${snippet ? ` — ${snippet}` : ''}`) });
44
+ continue;
45
+ }
32
46
  sent++;
33
47
  } catch (err) {
34
- errors.push({ target: t.kind, error: err.message });
48
+ // Strip the webhook URL from fetch's error message before returning it.
49
+ // The URL is itself a bearer credential for Slack/Teams — anyone holding
50
+ // it can post to the channel. We don't want it surfacing in audit.log
51
+ // or stdout via a caller that surfaces this errors[] array.
52
+ errors.push({ target: t.kind, error: redactUrl(t.url, err.message ?? String(err)) });
35
53
  }
36
54
  }
37
55
  return { sent, errors };
38
56
  }
39
57
 
58
+ function redactUrl(url, msg) {
59
+ if (!url || typeof msg !== 'string') return msg;
60
+ return msg.split(url).join('<webhook>');
61
+ }
62
+
40
63
  function formatPayload(kind, { title, body, level }) {
41
64
  const text = `*${title}*\n${body}`;
42
65
  if (kind === 'slack') return { text };
@@ -49,15 +49,46 @@ export async function probe({ runner = execa } = {}) {
49
49
  return { version };
50
50
  }
51
51
 
52
+ // Feature names appear in filesystem paths (`openspec/changes/<feature>/`) and in
53
+ // the subprocess invocation of `openspec propose <feature>`. An unvalidated
54
+ // `feature` like `../../etc` would let path.join escape the openspec/changes
55
+ // tree and write/read files anywhere the process has access to. Validate at
56
+ // every entry point that consumes a feature name.
57
+ const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/i;
58
+
59
+ export function assertSafeFeatureName(feature) {
60
+ if (typeof feature !== 'string' || feature.length === 0) {
61
+ const e = new Error('feature name is required.');
62
+ e.remediation = 'Provide a feature slug like `dark-mode`.';
63
+ throw e;
64
+ }
65
+ // Reject the obviously dangerous patterns up front for clearer error messages.
66
+ if (feature.includes('..') || feature.includes('/') || feature.includes('\\') || /^[a-z]:/i.test(feature)) {
67
+ const e = new Error(`Invalid feature name: "${feature}".`);
68
+ e.remediation = 'Feature names cannot contain "..", "/", "\\\\", or drive letters. Use a slug like `dark-mode`.';
69
+ throw e;
70
+ }
71
+ if (!FEATURE_NAME_RE.test(feature)) {
72
+ const e = new Error(`Invalid feature name: "${feature}".`);
73
+ e.remediation = 'Feature names must match /^[a-z0-9][a-z0-9._-]*$/i — start with a letter or digit; only letters, digits, `.`, `_`, `-` after that.';
74
+ throw e;
75
+ }
76
+ }
77
+
52
78
  export function changeDir(feature, cwd = process.cwd()) {
79
+ assertSafeFeatureName(feature);
53
80
  return join(cwd, OPENSPEC_CHANGES_DIR, feature);
54
81
  }
55
82
 
56
83
  export function changeExists(feature, cwd = process.cwd()) {
84
+ assertSafeFeatureName(feature);
57
85
  return existsSync(changeDir(feature, cwd));
58
86
  }
59
87
 
60
88
  export async function propose(feature, prompt, { runner = execa, cwd = process.cwd() } = {}) {
89
+ // Validate before the subprocess call so we never hand a traversal value to
90
+ // the openspec CLI (it may or may not validate on its own; we don't trust).
91
+ assertSafeFeatureName(feature);
61
92
  await probe({ runner });
62
93
  await runner('openspec', ['propose', feature, '--prompt', prompt], { cwd, stdio: 'inherit' });
63
94
  return changeDir(feature, cwd);
@@ -36,20 +36,45 @@ export async function loadChange(name, cwd = process.cwd()) {
36
36
  let proposal = {};
37
37
  const proposalPath = join(dir, 'proposal.md');
38
38
  if (existsSync(proposalPath)) {
39
- const raw = await readFile(proposalPath, 'utf8');
40
- proposal = fm.parse(raw).data ?? {};
39
+ const parsed = await safeParseFrontmatter(proposalPath, name, 'proposal.md');
40
+ proposal = parsed.data ?? {};
41
41
  }
42
42
  let items = [];
43
43
  const tasksPath = join(dir, 'tasks.md');
44
44
  if (existsSync(tasksPath)) {
45
- const raw = await readFile(tasksPath, 'utf8');
46
- const { data, body } = fm.parse(raw);
47
- items = data.items ?? parseChecklist(body);
45
+ const { data, body } = await safeParseFrontmatter(tasksPath, name, 'tasks.md');
46
+ items = coerceItems(data.items, body, name);
48
47
  }
49
48
  const mtime = await mostRecentMtime(dir);
50
49
  return { name, dir, proposal, items, mtime };
51
50
  }
52
51
 
52
+ export async function safeParseFrontmatter(path, changeName, fileLabel) {
53
+ const raw = await readFile(path, 'utf8');
54
+ try {
55
+ return fm.parse(raw);
56
+ } catch (err) {
57
+ const e = new Error(`${changeName}/${fileLabel}: YAML frontmatter is malformed (${err.message?.split('\n')[0] ?? err.name}).`);
58
+ e.remediation = `Repair the YAML in openspec/changes/${changeName}/${fileLabel}. Validate with \`openspecpm validate\` after fixing.`;
59
+ throw e;
60
+ }
61
+ }
62
+
63
+ export function coerceItems(rawItems, body, changeName) {
64
+ if (rawItems === undefined || rawItems === null) {
65
+ // No items: in frontmatter — fall back to the checklist body parser.
66
+ return parseChecklist(body);
67
+ }
68
+ if (!Array.isArray(rawItems)) {
69
+ const e = new Error(`${changeName}/tasks.md: frontmatter "items:" must be a YAML array, got ${Array.isArray(rawItems) ? 'array' : typeof rawItems}.`);
70
+ e.remediation = `Edit openspec/changes/${changeName}/tasks.md so "items:" is a list (each entry starts with "- title: ...").`;
71
+ throw e;
72
+ }
73
+ // Filter out malformed entries (silently skipping is better than crashing
74
+ // deep in a downstream consumer with `TypeError: items is not iterable`).
75
+ return rawItems.filter((t) => t && typeof t === 'object' && typeof t.title === 'string');
76
+ }
77
+
53
78
  /**
54
79
  * Resolve a dep token against a change. Tokens may be:
55
80
  * "task title" — same-change reference by title
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.1",
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 five differences: **OpenSpec** authors the specs (with a heuristic BDD linter plus an optional LLM judge), **adapters** make the PM backend pluggable (GitHub / Azure DevOps / Jira / Linear / GitLab), the wizard is **friendly to non-engineers**, every command is **audit-logged** by default, and `depends_on:` reaches **across features** so `next`/`blocked` reflect the whole project.
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> (close + archive)
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` |
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.
@@ -1,105 +1,106 @@
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.
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
+ - LLM BDD judge (optional, opt-in via `--llm` or `judge.enabled` config): `ANTHROPIC_API_KEY`.
31
+
32
+ ## Frontmatter schemas
33
+
34
+ All artifacts use YAML frontmatter. Required fields:
35
+
36
+ ### proposal.md
37
+
38
+ ```yaml
39
+ ---
40
+ name: <feature-slug>
41
+ status: draft | in_review | approved | shipped
42
+ created: <ISO-8601 timestamp>
43
+ schema_version: 1
44
+ external: # filled by `openspecpm sync`
45
+ github:
46
+ adapter: github
47
+ id: "42"
48
+ url: https://github.com/.../issues/42
49
+ ---
50
+ ```
51
+
52
+ ### tasks.md
53
+
54
+ ```yaml
55
+ ---
56
+ schema_version: 1
57
+ items:
58
+ - title: "Implement X"
59
+ sync_state: pending | created | failed
60
+ external_id: "43" # filled after sync
61
+ external_url: https://...
62
+ depends_on: [] # task titles or external ids
63
+ parallel: true | false
64
+ ---
65
+ ```
66
+
67
+ If `items:` is absent, OpenSpecPM falls back to parsing `- [ ] title` checklist lines in the body.
68
+
69
+ ## BDD format
70
+
71
+ Scenarios in `specs/*.md` should use Gherkin-style triplets:
72
+
73
+ ```
74
+ Scenario: User toggles dark mode
75
+ Given the user is signed in
76
+ And their theme preference is "system"
77
+ When they select "Dark" in the appearance menu
78
+ Then the UI re-renders in dark theme
79
+ And their preference is saved to the profile
80
+ ```
81
+
82
+ Lint heuristics (enforced softly at `propose`, hard at `sync`):
83
+
84
+ - Each scenario has one `Given`, one `When`, one `Then` (with optional `And`s).
85
+ - `Then` uses an observable verb (displays, returns, stores, rejects, emails, …).
86
+ - Reject "should work", "should be correct", "is successful" as `Then` predicates.
87
+ - Reject tautological `Then` (paraphrase of the `When`).
88
+
89
+ ## ISO timestamps
90
+
91
+ All `created` / `updated` / `synced` fields use ISO-8601 with timezone:
92
+
93
+ ```
94
+ 2026-05-17T14:30:00Z
95
+ ```
96
+
97
+ ## Sync markers
98
+
99
+ Comments pushed to the PM tool are append-only and stamped:
100
+
101
+ ```
102
+ <!-- SYNCED: 2026-05-17T14:30:00Z -->
103
+ …progress narrative…
104
+ ```
105
+
106
+ This lets re-syncs detect what's already been sent without duplicating.
@@ -29,9 +29,9 @@ A work item moved to `in_progress` in the PM tool, a local progress directory cr
29
29
 
30
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
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.
32
+ 6. **Periodically broadcast progress.** Append to local `progress.md`, then sync to the PM tool with `openspecpm comment <task>`. Don't post per-keystroke — once per meaningful checkpoint.
33
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.
34
+ 7. **When done.** Run `openspecpm ship <feature>` to close the work item with a final comment and archive the OpenSpec change.
35
35
 
36
36
  ## Worktrees: hidden by default
37
37
 
@@ -41,7 +41,7 @@ Engineers may want a `git worktree` per feature so concurrent work doesn't colli
41
41
  openspecpm start <feature> --dev
42
42
  ```
43
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.
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 flow skips the worktree and works in the current checkout.
45
45
 
46
46
  ## Parallel agents
47
47
 
@@ -52,7 +52,7 @@ For each parallel task:
52
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
53
  3. Wait for completion, then merge.
54
54
 
55
- In Sprint 3, `openspecpm fan-out <feature>` automates the launch.
55
+ `openspecpm fan-out <feature>` automates the launch by emitting ready-to-paste prompts for `parallel: true` tasks.
56
56
 
57
57
  ## What to avoid
58
58
 
@@ -42,6 +42,6 @@ A fully-formed OpenSpec change at `openspec/changes/<feature>/` containing:
42
42
 
43
43
  ## After this phase
44
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).
45
+ - If the user is ready to push to their PM tool: route to `references/sync.md`.
46
+ - If the user wants to decompose into tasks first: route to `references/structure.md`.
47
47
  - If the user wants to check what other changes exist: run `openspecpm status`.