specweave 1.0.301 → 1.0.303
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/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +6 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +29 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +212 -2
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/src/cli/commands/refresh-plugins.d.ts.map +1 -1
- package/dist/src/cli/commands/refresh-plugins.js +9 -0
- package/dist/src/cli/commands/refresh-plugins.js.map +1 -1
- package/dist/src/config/types.d.ts +2 -2
- package/dist/src/core/increment/increment-utils.d.ts +27 -4
- package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
- package/dist/src/core/increment/increment-utils.js +44 -17
- package/dist/src/core/increment/increment-utils.js.map +1 -1
- package/dist/src/core/increment/template-creator.d.ts +26 -0
- package/dist/src/core/increment/template-creator.d.ts.map +1 -1
- package/dist/src/core/increment/template-creator.js +179 -20
- package/dist/src/core/increment/template-creator.js.map +1 -1
- package/dist/src/importers/import-to-increment.d.ts +111 -0
- package/dist/src/importers/import-to-increment.d.ts.map +1 -0
- package/dist/src/importers/import-to-increment.js +223 -0
- package/dist/src/importers/import-to-increment.js.map +1 -0
- package/dist/src/importers/increment-external-ref-detector.d.ts +78 -0
- package/dist/src/importers/increment-external-ref-detector.d.ts.map +1 -0
- package/dist/src/importers/increment-external-ref-detector.js +130 -0
- package/dist/src/importers/increment-external-ref-detector.js.map +1 -0
- package/dist/src/init/research/types.d.ts +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/stop-auto-v5.sh +2 -2
- package/plugins/specweave/hooks/stop-sync.sh +10 -5
- package/plugins/specweave/hooks/user-prompt-submit.sh +27 -5
- package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +6 -3
- package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +4 -3
- package/plugins/specweave/skills/import/SKILL.md +186 -0
- package/plugins/specweave/skills/increment/SKILL.md +30 -16
- package/plugins/specweave/skills/pm/SKILL.md +29 -2
- package/plugins/specweave/skills/pm/phases/00-deep-interview.md +12 -0
- package/plugins/specweave-github/lib/github-feature-sync-cli.js +5 -0
- package/plugins/specweave-github/lib/github-feature-sync-cli.ts +7 -1
- package/plugins/specweave-github/lib/github-feature-sync.js +225 -2
- package/plugins/specweave-github/lib/github-feature-sync.ts +290 -2
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Import external issues from GitHub, Jira, or Azure DevOps and create SpecWeave increments with platform suffixes (G/J/A). Supports filtering and duplicate prevention. Use when saying "import issues", "pull from github", "grab jira issues", or "import from ado".
|
|
3
|
+
argument-hint: "[platform] [filter-query]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# External Issue Import
|
|
7
|
+
|
|
8
|
+
Import issues from external trackers (GitHub, JIRA, Azure DevOps) and create SpecWeave increments with platform-specific suffixes: **G** (GitHub), **J** (JIRA), **A** (ADO).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
### STEP 1: Load Configuration
|
|
15
|
+
|
|
16
|
+
1. Read `.specweave/config.json` — check `sync` section
|
|
17
|
+
2. Identify which platforms are configured (`sync.github`, `sync.jira`, `sync.ado`)
|
|
18
|
+
3. If NO platforms configured:
|
|
19
|
+
- Tell user: "No external tools configured. Run `/sw:sync-setup` to connect GitHub, JIRA, or ADO."
|
|
20
|
+
- **STOP**
|
|
21
|
+
|
|
22
|
+
### STEP 2: Platform Selection
|
|
23
|
+
|
|
24
|
+
1. If user specified a platform in the command argument (e.g., `/sw:import github`), use that
|
|
25
|
+
2. If multiple platforms configured and none specified, ask user which to import from:
|
|
26
|
+
- Use AskUserQuestion with configured platforms as options
|
|
27
|
+
3. Validate the selected platform is configured and has credentials
|
|
28
|
+
|
|
29
|
+
### STEP 3: Filter Configuration
|
|
30
|
+
|
|
31
|
+
Ask user for optional filters (or parse from arguments):
|
|
32
|
+
|
|
33
|
+
- **Status**: open (default), closed, all
|
|
34
|
+
- **Labels**: comma-separated label filter
|
|
35
|
+
- **Date range**: last N months (default: 3)
|
|
36
|
+
- **Milestone/Epic**: filter by milestone or epic
|
|
37
|
+
- **Search query**: text search in title/description
|
|
38
|
+
- **Max items**: limit results (default: 20)
|
|
39
|
+
|
|
40
|
+
If user provides no filters, use defaults: open issues, last 3 months, max 20.
|
|
41
|
+
|
|
42
|
+
### STEP 4: Fetch External Issues
|
|
43
|
+
|
|
44
|
+
1. Read credentials from `.env` or environment:
|
|
45
|
+
- GitHub: `GITHUB_TOKEN` or `gh auth status`
|
|
46
|
+
- JIRA: `JIRA_EMAIL` + `JIRA_API_TOKEN` + domain from config
|
|
47
|
+
- ADO: `ADO_PAT` + org/project from config
|
|
48
|
+
|
|
49
|
+
2. Use the platform's API to fetch issues matching filters:
|
|
50
|
+
- **GitHub**: `gh api repos/{owner}/{repo}/issues` with query params
|
|
51
|
+
- **JIRA**: JIRA REST API v3 with JQL query
|
|
52
|
+
- **ADO**: ADO REST API with WIQL query
|
|
53
|
+
|
|
54
|
+
3. Parse results into a display-friendly list
|
|
55
|
+
|
|
56
|
+
### STEP 5: Display and Select
|
|
57
|
+
|
|
58
|
+
Present issues in a numbered table:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
# | ID | Title | Status | Priority | Labels
|
|
62
|
+
--|-----------|--------------------------------|--------|----------|--------
|
|
63
|
+
1 | #123 | Fix login redirect loop | open | P1 | bug
|
|
64
|
+
2 | #456 | Add dark mode support | open | P2 | feature
|
|
65
|
+
3 | #789 | Update API documentation | open | P3 | docs
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Ask user to select which issues to import:
|
|
69
|
+
- Single: "1"
|
|
70
|
+
- Multiple: "1,3,5"
|
|
71
|
+
- All: "all"
|
|
72
|
+
- Range: "1-5"
|
|
73
|
+
|
|
74
|
+
### STEP 6: Duplicate Detection
|
|
75
|
+
|
|
76
|
+
For each selected issue, check if already imported:
|
|
77
|
+
|
|
78
|
+
1. Generate the canonical `external_ref` string:
|
|
79
|
+
- GitHub: `github#{owner}/{repo}#{issue_number}`
|
|
80
|
+
- JIRA: `jira#{project_key}#{issue_key}`
|
|
81
|
+
- ADO: `ado#{org}/{project}#{work_item_id}`
|
|
82
|
+
|
|
83
|
+
2. Scan ALL `.specweave/increments/**/metadata.json` files for matching `external_ref`
|
|
84
|
+
- Check: active, _archive, _abandoned, _paused directories
|
|
85
|
+
|
|
86
|
+
3. For duplicates found:
|
|
87
|
+
- Report: "Skipping #{issue_id} — already imported as {increment_id}"
|
|
88
|
+
- Remove from selection
|
|
89
|
+
|
|
90
|
+
### STEP 7: Create Increments
|
|
91
|
+
|
|
92
|
+
For each non-duplicate selected issue:
|
|
93
|
+
|
|
94
|
+
1. **Generate increment ID** with platform suffix:
|
|
95
|
+
- GitHub issue #123 "fix-login-bug" → `0271G-fix-login-bug`
|
|
96
|
+
- JIRA PROJ-456 "payment-flow" → `0272J-payment-flow`
|
|
97
|
+
- ADO #789 "ci-pipeline" → `0273A-ci-pipeline`
|
|
98
|
+
|
|
99
|
+
2. **Create increment files** via `createIncrementTemplates()` with `externalSource`:
|
|
100
|
+
- `metadata.json` — includes `external_ref`, `origin: "external"`, `source_platform`
|
|
101
|
+
- `spec.md` — pre-filled with issue title, description, and acceptance criteria
|
|
102
|
+
- `plan.md` — template (to be completed via architect skill)
|
|
103
|
+
- `tasks.md` — derived from acceptance criteria if available, template otherwise
|
|
104
|
+
|
|
105
|
+
3. **Map priority**: Use external priority if available, default to P2
|
|
106
|
+
4. **Map type**: bug → bug, feature/epic/story → feature
|
|
107
|
+
|
|
108
|
+
### STEP 8: Post-Import Summary
|
|
109
|
+
|
|
110
|
+
Display results:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Import Complete
|
|
114
|
+
===============
|
|
115
|
+
|
|
116
|
+
Created:
|
|
117
|
+
- 0271G-fix-login-bug (from GitHub #123)
|
|
118
|
+
- 0273A-ci-pipeline (from ADO #789)
|
|
119
|
+
|
|
120
|
+
Skipped (duplicates):
|
|
121
|
+
- GitHub #456 — already imported as 0200G-dark-mode
|
|
122
|
+
|
|
123
|
+
Errors: none
|
|
124
|
+
|
|
125
|
+
Next steps:
|
|
126
|
+
- /sw:do 0271G — Start working on first import
|
|
127
|
+
- /sw:auto 0271G — Run autonomously
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Platform Suffix Reference
|
|
133
|
+
|
|
134
|
+
| Platform | Suffix | Example |
|
|
135
|
+
|----------|--------|---------|
|
|
136
|
+
| GitHub | G | `0271G-fix-login-bug` |
|
|
137
|
+
| JIRA | J | `0272J-payment-flow` |
|
|
138
|
+
| ADO | A | `0273A-ci-pipeline` |
|
|
139
|
+
| Legacy | E | `0111E-old-import` (backwards compat) |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Edge Cases
|
|
144
|
+
|
|
145
|
+
### No issues found
|
|
146
|
+
Tell user "No matching issues found. Try adjusting filters." Suggest broader search.
|
|
147
|
+
|
|
148
|
+
### External tool API error
|
|
149
|
+
Report the error clearly. Suggest checking credentials: "Run `/sw:sync-setup` to verify credentials."
|
|
150
|
+
|
|
151
|
+
### Issue has no description
|
|
152
|
+
Create spec with title only and mark as needs-review.
|
|
153
|
+
|
|
154
|
+
### Issue has no acceptance criteria
|
|
155
|
+
Create template-style tasks.md with placeholder tasks.
|
|
156
|
+
|
|
157
|
+
### Rate limiting
|
|
158
|
+
Report rate limit and suggest waiting or reducing the import batch size.
|
|
159
|
+
|
|
160
|
+
### Umbrella / multi-repo project
|
|
161
|
+
If in an umbrella project with multiple repos under `repositories/`, ask which repo's `.specweave/` should receive the increment.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Configuration Reference
|
|
166
|
+
|
|
167
|
+
Required in `.specweave/config.json`:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"sync": {
|
|
172
|
+
"enabled": true,
|
|
173
|
+
"github": { "enabled": true, "owner": "...", "repo": "..." },
|
|
174
|
+
"jira": { "enabled": true, "domain": "...", "projectKey": "..." },
|
|
175
|
+
"ado": { "enabled": true, "organization": "...", "project": "..." }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Credentials in `.env` (never committed):
|
|
181
|
+
```
|
|
182
|
+
GITHUB_TOKEN=ghp_...
|
|
183
|
+
JIRA_EMAIL=user@example.com
|
|
184
|
+
JIRA_API_TOKEN=...
|
|
185
|
+
ADO_PAT=...
|
|
186
|
+
```
|
|
@@ -46,15 +46,20 @@ Increment planning produces specs, plans, and task breakdowns that require user
|
|
|
46
46
|
STEP 0A: Discipline Check (BLOCKING)
|
|
47
47
|
STEP 0B: WIP Enforcement
|
|
48
48
|
STEP 0C: Tech Stack Detection
|
|
49
|
-
STEP 1: Pre-flight (TDD mode, multi-project, Deep Interview)
|
|
50
|
-
STEP 1a: Deep Interview (if enabled)
|
|
49
|
+
STEP 1: Pre-flight (TDD mode, multi-project, Deep Interview check)
|
|
51
50
|
STEP 2: Project Context (resolve project/board)
|
|
52
|
-
STEP 3: Create Increment (via Template API)
|
|
51
|
+
STEP 3: Create Increment (via Template API) ← folder + ID exist after this
|
|
52
|
+
STEP 3a: Deep Interview (if enabled) ← runs AFTER folder exists
|
|
53
53
|
STEP 4: Delegation (architect + test-aware-planner)
|
|
54
54
|
STEP 5: Post-Creation Sync
|
|
55
55
|
STEP 6: Execution Strategy Recommendation
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
**CRITICAL**: Step 3 (Create Increment) MUST run before Step 3a (Deep Interview).
|
|
59
|
+
The interview state file is written to `.specweave/state/interview-{increment-id}.json`,
|
|
60
|
+
and the enforcement guard looks for it by increment ID. If the interview runs before the
|
|
61
|
+
increment folder exists, the guard cannot find the state file and blocks spec.md writing.
|
|
62
|
+
|
|
58
63
|
## Step 0A: Discipline Check (MANDATORY)
|
|
59
64
|
|
|
60
65
|
**Cannot start N+1 until N is DONE.**
|
|
@@ -120,24 +125,13 @@ jq -r '.testing.defaultTestMode // "test-after"' .specweave/config.json 2>/dev/n
|
|
|
120
125
|
# 2. Check multi-project config
|
|
121
126
|
specweave context projects 2>/dev/null
|
|
122
127
|
|
|
123
|
-
# 3. Check deep interview mode
|
|
124
|
-
jq -r '.planning.deepInterview.enabled // false' .specweave/config.json 2>/dev/null
|
|
128
|
+
# 3. Check deep interview mode (note: interview itself runs at Step 3a, after increment exists)
|
|
129
|
+
DEEP_INTERVIEW=$(jq -r '.planning.deepInterview.enabled // false' .specweave/config.json 2>/dev/null)
|
|
125
130
|
|
|
126
131
|
# 4. Check WIP limits
|
|
127
132
|
find .specweave/increments -maxdepth 2 -name "metadata.json" -exec grep -l '"status":"active"' {} \; 2>/dev/null | wc -l
|
|
128
133
|
```
|
|
129
134
|
|
|
130
|
-
## Step 1a: Deep Interview Mode (if enabled)
|
|
131
|
-
|
|
132
|
-
**If deep interview is enabled, delegate to PM skill:**
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
Skill({ skill: "sw:pm", args: "Deep interview mode for: <user description>" })
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
**THINK about complexity first** - assess before asking:
|
|
139
|
-
- Trivial: 0-3 questions | Small: 4-8 | Medium: 9-18 | Large: 19-40+
|
|
140
|
-
|
|
141
135
|
## Step 2: Project Context
|
|
142
136
|
|
|
143
137
|
```bash
|
|
@@ -246,6 +240,26 @@ Create files in order: metadata.json FIRST, then spec.md, plan.md, tasks.md.
|
|
|
246
240
|
4. **Increment naming** - Format: `####-descriptive-kebab-case`
|
|
247
241
|
5. **Multi-repo** - In umbrella projects with `repositories/` folder, create increments in EACH repo's `.specweave/`, not the umbrella root
|
|
248
242
|
|
|
243
|
+
## Step 3a: Deep Interview Mode (if enabled)
|
|
244
|
+
|
|
245
|
+
**IMPORTANT**: This step runs AFTER the increment folder is created (Step 3), so the
|
|
246
|
+
interview state file can reference the real increment ID.
|
|
247
|
+
|
|
248
|
+
**If deep interview is enabled, delegate to PM skill:**
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
Skill({ skill: "sw:pm", args: "Deep interview for increment XXXX-name: <user description>" })
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The PM skill will:
|
|
255
|
+
1. Assess complexity and determine question count (trivial: 0-3, small: 4-8, medium: 9-18, large: 19-40)
|
|
256
|
+
2. Interview the user across relevant categories
|
|
257
|
+
3. Write interview state to `.specweave/state/interview-{increment-id}.json`
|
|
258
|
+
4. Return interview summary for spec.md creation
|
|
259
|
+
|
|
260
|
+
**After PM returns**, read the interview state file to confirm all categories are covered
|
|
261
|
+
before proceeding to spec.md creation (especially when `enforcement: "strict"`).
|
|
262
|
+
|
|
249
263
|
## Step 4: Delegation
|
|
250
264
|
|
|
251
265
|
After increment creation:
|
|
@@ -44,8 +44,35 @@ If `true`:
|
|
|
44
44
|
- Small features: 4-8 questions
|
|
45
45
|
- Medium features: 9-18 questions
|
|
46
46
|
- Large features: 19-40 questions
|
|
47
|
-
3.
|
|
48
|
-
|
|
47
|
+
3. Check `minQuestions` config: `jq -r '.planning.deepInterview.minQuestions // 5' .specweave/config.json`
|
|
48
|
+
- If complexity assessment yields fewer questions than minQuestions, use minQuestions as the floor
|
|
49
|
+
4. Cover relevant categories (skip those that don't apply)
|
|
50
|
+
5. Only proceed to Research phase after sufficient clarity
|
|
51
|
+
|
|
52
|
+
### Writing Interview State to Disk (CRITICAL)
|
|
53
|
+
|
|
54
|
+
**This skill runs with `context: fork` (isolated LLM context), but file writes persist.**
|
|
55
|
+
|
|
56
|
+
When invoked from `sw:increment` with an increment ID (e.g., "Deep interview for increment 0266-foo: ..."),
|
|
57
|
+
you MUST write the interview state file to disk so the enforcement guard can find it:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Extract increment ID from the args (e.g., "Deep interview for increment 0266-foo: ...")
|
|
61
|
+
# Initialize interview state file BEFORE starting questions
|
|
62
|
+
mkdir -p .specweave/state
|
|
63
|
+
echo '{"incrementId":"XXXX-name","startedAt":"'$(date -Iseconds)'","coveredCategories":{}}' \
|
|
64
|
+
> .specweave/state/interview-XXXX-name.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
After covering each category, update the state file:
|
|
68
|
+
```bash
|
|
69
|
+
jq '.coveredCategories.architecture = {"coveredAt": "'$(date -Iseconds)'", "summary": "..."}' \
|
|
70
|
+
.specweave/state/interview-XXXX-name.json > tmp && mv tmp .specweave/state/interview-XXXX-name.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Why this matters**: The `interview-enforcement-guard.sh` (PreToolUse hook on Write) checks
|
|
74
|
+
`.specweave/state/interview-{increment-id}.json` before allowing spec.md writes. If this file
|
|
75
|
+
is missing or incomplete, spec.md creation is BLOCKED in strict mode.
|
|
49
76
|
|
|
50
77
|
## Core Principles
|
|
51
78
|
|
|
@@ -207,6 +207,18 @@ When factors point to different complexity levels:
|
|
|
207
207
|
| **Medium** | 9-18 questions | Multiple components, some integration points |
|
|
208
208
|
| **Large** | 19-40 questions | Architectural, cross-cutting, high-risk (payments, security) |
|
|
209
209
|
|
|
210
|
+
### Config Floor: minQuestions
|
|
211
|
+
|
|
212
|
+
The project may define a minimum question count in config:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
jq -r '.planning.deepInterview.minQuestions // 5' .specweave/config.json 2>/dev/null
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Rule**: If your complexity assessment yields fewer questions than `minQuestions`, use `minQuestions` as the floor. For example, if you assess a feature as "trivial" (0-3 questions) but `minQuestions` is 5, ask at least 5 questions.
|
|
219
|
+
|
|
220
|
+
This prevents teams from accidentally under-interviewing when they have set an organizational minimum.
|
|
221
|
+
|
|
210
222
|
### What's a "Single Component"?
|
|
211
223
|
|
|
212
224
|
A "single component" means **isolated scope** - changes contained to one area:
|
|
@@ -135,12 +135,17 @@ async function main() {
|
|
|
135
135
|
}
|
|
136
136
|
process.exit(0);
|
|
137
137
|
} catch (error) {
|
|
138
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
139
|
+
console.log(`
|
|
140
|
+
[ERROR] Sync failed for ${featureId}: ${msg}`);
|
|
138
141
|
console.error(`
|
|
139
142
|
\u274C Sync failed:`, error);
|
|
140
143
|
process.exit(1);
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
146
|
main().catch((error) => {
|
|
147
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
148
|
+
console.log(`[FATAL] ${msg}`);
|
|
144
149
|
console.error("Fatal error:", error);
|
|
145
150
|
process.exit(1);
|
|
146
151
|
});
|
|
@@ -195,7 +195,11 @@ async function main() {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
process.exit(0);
|
|
198
|
-
} catch (error) {
|
|
198
|
+
} catch (error: unknown) {
|
|
199
|
+
// FIXED (v1.0.302): Write errors to stdout too, since stderr may be suppressed
|
|
200
|
+
// by run_with_timeout() in shell handlers. This ensures errors appear in throttle.log.
|
|
201
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
202
|
+
console.log(`\n[ERROR] Sync failed for ${featureId}: ${msg}`);
|
|
199
203
|
console.error(`\n❌ Sync failed:`, error);
|
|
200
204
|
process.exit(1);
|
|
201
205
|
}
|
|
@@ -203,6 +207,8 @@ async function main() {
|
|
|
203
207
|
|
|
204
208
|
// Run CLI
|
|
205
209
|
main().catch(error => {
|
|
210
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
211
|
+
console.log(`[FATAL] ${msg}`);
|
|
206
212
|
console.error('Fatal error:', error);
|
|
207
213
|
process.exit(1);
|
|
208
214
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, readFile, writeFile } from "fs/promises";
|
|
1
|
+
import { readdir, readFile, writeFile, mkdir } from "fs/promises";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as yaml from "yaml";
|
|
@@ -160,7 +160,8 @@ const _GitHubFeatureSync = class _GitHubFeatureSync {
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
163
|
-
* Find Feature folder in specs directory
|
|
163
|
+
* Find Feature folder in specs directory.
|
|
164
|
+
* Falls back to auto-creating from increment spec.md if living docs don't exist.
|
|
164
165
|
*/
|
|
165
166
|
async findFeatureFolder(featureId) {
|
|
166
167
|
const projectFolders = await this.findProjectFolders();
|
|
@@ -175,8 +176,230 @@ const _GitHubFeatureSync = class _GitHubFeatureSync {
|
|
|
175
176
|
console.log(` \u26A0\uFE0F Found feature in legacy _features folder - consider migrating to project folder`);
|
|
176
177
|
return legacyFolder;
|
|
177
178
|
}
|
|
179
|
+
console.log(` \u2139\uFE0F Feature folder not found in living docs, attempting auto-create from spec.md...`);
|
|
180
|
+
const created = await this.createFeatureFolderFromSpec(featureId, projectFolders);
|
|
181
|
+
if (created) {
|
|
182
|
+
return created;
|
|
183
|
+
}
|
|
178
184
|
return null;
|
|
179
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Find the increment folder for a given feature ID.
|
|
188
|
+
* Converts FS-271 -> finds 0271-xxx-xxx/ in .specweave/increments/
|
|
189
|
+
*/
|
|
190
|
+
async findIncrementFolder(featureId) {
|
|
191
|
+
const numMatch = featureId.match(/FS-0*(\d+)E?/i);
|
|
192
|
+
if (!numMatch) return null;
|
|
193
|
+
const num = parseInt(numMatch[1], 10);
|
|
194
|
+
const paddedNum = String(num).padStart(4, "0");
|
|
195
|
+
const incrementsDir = path.join(this.projectRoot, ".specweave/increments");
|
|
196
|
+
if (!existsSync(incrementsDir)) return null;
|
|
197
|
+
const entries = await readdir(incrementsDir);
|
|
198
|
+
const match = entries.find((e) => e.startsWith(paddedNum + "-"));
|
|
199
|
+
if (!match) return null;
|
|
200
|
+
return path.join(incrementsDir, match);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Auto-create a feature folder (FEATURE.md + us-NNN.md files) from an
|
|
204
|
+
* increment's spec.md. This enables GitHub sync even when the living docs
|
|
205
|
+
* builder hasn't run yet.
|
|
206
|
+
*/
|
|
207
|
+
async createFeatureFolderFromSpec(featureId, projectFolders) {
|
|
208
|
+
try {
|
|
209
|
+
const incrementFolder = await this.findIncrementFolder(featureId);
|
|
210
|
+
if (!incrementFolder) {
|
|
211
|
+
console.log(` \u26A0\uFE0F No increment folder found for ${featureId}`);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const specPath = path.join(incrementFolder, "spec.md");
|
|
215
|
+
if (!existsSync(specPath)) {
|
|
216
|
+
console.log(` \u26A0\uFE0F No spec.md found in ${path.basename(incrementFolder)}`);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const specContent = await readFile(specPath, "utf-8");
|
|
220
|
+
const fmMatch = specContent.match(/^---\n([\s\S]*?)\n---/);
|
|
221
|
+
if (!fmMatch) {
|
|
222
|
+
console.log(` \u26A0\uFE0F spec.md has no YAML frontmatter`);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const frontmatter = yaml.parse(fmMatch[1]);
|
|
226
|
+
const title = frontmatter.title || path.basename(incrementFolder).replace(/^\d+-/, "");
|
|
227
|
+
const status = frontmatter.status || "active";
|
|
228
|
+
const priority = frontmatter.priority || "P2";
|
|
229
|
+
const created = frontmatter.created || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
230
|
+
const incrementId = frontmatter.increment || path.basename(incrementFolder);
|
|
231
|
+
let targetProjectFolder = projectFolders[0];
|
|
232
|
+
const projectMatch = specContent.match(/\*\*Project\*\*:\s*(\S+)/);
|
|
233
|
+
if (projectMatch) {
|
|
234
|
+
const projectName = projectMatch[1];
|
|
235
|
+
const matchingFolder = projectFolders.find((f) => path.basename(f) === projectName);
|
|
236
|
+
if (matchingFolder) {
|
|
237
|
+
targetProjectFolder = matchingFolder;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!targetProjectFolder) {
|
|
241
|
+
console.log(` \u26A0\uFE0F No project folder available for feature creation`);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const featureFolder = path.join(targetProjectFolder, featureId);
|
|
245
|
+
await mkdir(featureFolder, { recursive: true });
|
|
246
|
+
const userStories = this.parseUserStoriesFromSpec(specContent, featureId);
|
|
247
|
+
const featureMd = this.buildFeatureMd(featureId, title, status, priority, created, incrementId, userStories);
|
|
248
|
+
await writeFile(path.join(featureFolder, "FEATURE.md"), featureMd, "utf-8");
|
|
249
|
+
for (const us of userStories) {
|
|
250
|
+
const usFilename = `us-${us.id.replace("US-", "").padStart(3, "0")}-${this.slugify(us.title)}.md`;
|
|
251
|
+
const usMd = this.buildUserStoryMd(us, featureId, incrementId);
|
|
252
|
+
await writeFile(path.join(featureFolder, usFilename), usMd, "utf-8");
|
|
253
|
+
}
|
|
254
|
+
console.log(` \u2705 Auto-created feature folder with ${userStories.length} user stories`);
|
|
255
|
+
return featureFolder;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.log(` \u26A0\uFE0F Failed to auto-create feature folder: ${error.message}`);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Parse user stories from spec.md markdown content.
|
|
263
|
+
*/
|
|
264
|
+
parseUserStoriesFromSpec(specContent, featureId) {
|
|
265
|
+
const stories = [];
|
|
266
|
+
const usRegex = /### (US-\d+):\s*(.+?)(?:\s*\((P\d)\))?\s*\n([\s\S]*?)(?=\n### US-|\n## |\n---\s*\n### US-|$)/g;
|
|
267
|
+
let match;
|
|
268
|
+
while ((match = usRegex.exec(specContent)) !== null) {
|
|
269
|
+
const usId = match[1];
|
|
270
|
+
const rawTitle = match[2].trim();
|
|
271
|
+
const priority = match[3] || "P2";
|
|
272
|
+
const body = match[4];
|
|
273
|
+
if (rawTitle === "[Story Title]") continue;
|
|
274
|
+
const projectMatch = body.match(/\*\*Project\*\*:\s*(\S+)/);
|
|
275
|
+
const project = projectMatch ? projectMatch[1] : "specweave";
|
|
276
|
+
const storyMatch = body.match(/\*\*As a\*\*\s+([\s\S]*?)(?=\n\*\*Acceptance Criteria|$)/);
|
|
277
|
+
const storyText = storyMatch ? storyMatch[1].trim() : "";
|
|
278
|
+
const acs = [];
|
|
279
|
+
const acRegex = /- \[[ x]\] \*\*AC-[^*]+\*\*:\s*(.+)/g;
|
|
280
|
+
let acMatch;
|
|
281
|
+
while ((acMatch = acRegex.exec(body)) !== null) {
|
|
282
|
+
acs.push(acMatch[0]);
|
|
283
|
+
}
|
|
284
|
+
const totalAcs = acs.length;
|
|
285
|
+
const completedAcs = acs.filter((ac) => ac.startsWith("- [x]")).length;
|
|
286
|
+
let status = "not-started";
|
|
287
|
+
if (totalAcs > 0 && completedAcs === totalAcs) status = "complete";
|
|
288
|
+
else if (completedAcs > 0) status = "active";
|
|
289
|
+
stories.push({
|
|
290
|
+
id: usId,
|
|
291
|
+
title: rawTitle,
|
|
292
|
+
priority,
|
|
293
|
+
project,
|
|
294
|
+
storyText,
|
|
295
|
+
acceptanceCriteria: acs,
|
|
296
|
+
status
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return stories;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Build FEATURE.md content matching the living docs format.
|
|
303
|
+
*/
|
|
304
|
+
buildFeatureMd(featureId, title, status, priority, created, incrementId, userStories) {
|
|
305
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
306
|
+
const mappedStatus = status === "planned" ? "planning" : status === "completed" || status === "done" ? "complete" : status === "active" || status === "in-progress" ? "active" : "planning";
|
|
307
|
+
const fm = {
|
|
308
|
+
id: featureId,
|
|
309
|
+
title,
|
|
310
|
+
type: "feature",
|
|
311
|
+
status: mappedStatus,
|
|
312
|
+
priority,
|
|
313
|
+
created,
|
|
314
|
+
lastUpdated: now,
|
|
315
|
+
tldr: title,
|
|
316
|
+
complexity: "medium",
|
|
317
|
+
auto_created: true
|
|
318
|
+
};
|
|
319
|
+
const yamlFm = yaml.stringify(fm);
|
|
320
|
+
let body = `
|
|
321
|
+
# ${title}
|
|
322
|
+
|
|
323
|
+
## TL;DR
|
|
324
|
+
|
|
325
|
+
**What**: ${title}
|
|
326
|
+
**Status**: ${mappedStatus} | **Priority**: ${priority}
|
|
327
|
+
**User Stories**: ${userStories.length}
|
|
328
|
+
|
|
329
|
+
## Overview
|
|
330
|
+
|
|
331
|
+
${title}
|
|
332
|
+
|
|
333
|
+
## Implementation History
|
|
334
|
+
|
|
335
|
+
| Increment | Status |
|
|
336
|
+
|-----------|--------|
|
|
337
|
+
| [${incrementId}](../../../../../increments/${incrementId}/spec.md) | ${mappedStatus} |
|
|
338
|
+
|
|
339
|
+
## User Stories
|
|
340
|
+
`;
|
|
341
|
+
for (const us of userStories) {
|
|
342
|
+
body += `
|
|
343
|
+
- [${us.id}: ${us.title}](./${us.id.toLowerCase()}.md)`;
|
|
344
|
+
}
|
|
345
|
+
return `---
|
|
346
|
+
${yamlFm}---${body}
|
|
347
|
+
`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Build us-NNN.md content matching the living docs format.
|
|
351
|
+
*/
|
|
352
|
+
buildUserStoryMd(us, featureId, incrementId) {
|
|
353
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
354
|
+
const fm = {
|
|
355
|
+
id: us.id,
|
|
356
|
+
feature: featureId,
|
|
357
|
+
title: us.title,
|
|
358
|
+
status: us.status,
|
|
359
|
+
priority: us.priority,
|
|
360
|
+
created: now,
|
|
361
|
+
project: us.project
|
|
362
|
+
};
|
|
363
|
+
const yamlFm = yaml.stringify(fm);
|
|
364
|
+
let body = `
|
|
365
|
+
# ${us.id}: ${us.title}
|
|
366
|
+
|
|
367
|
+
**Feature**: [${featureId}](./FEATURE.md)
|
|
368
|
+
|
|
369
|
+
`;
|
|
370
|
+
if (us.storyText) {
|
|
371
|
+
body += `${us.storyText}
|
|
372
|
+
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
body += `---
|
|
376
|
+
|
|
377
|
+
## Acceptance Criteria
|
|
378
|
+
|
|
379
|
+
`;
|
|
380
|
+
if (us.acceptanceCriteria.length > 0) {
|
|
381
|
+
body += us.acceptanceCriteria.join("\n") + "\n";
|
|
382
|
+
} else {
|
|
383
|
+
body += `- [ ] **AC-${us.id.replace("US-", "US")}-01**: Pending specification
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
body += `
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Implementation
|
|
390
|
+
|
|
391
|
+
**Increment**: [${incrementId}](../../../../../increments/${incrementId}/spec.md)
|
|
392
|
+
`;
|
|
393
|
+
return `---
|
|
394
|
+
${yamlFm}---${body}
|
|
395
|
+
`;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Convert a title to a URL-safe slug.
|
|
399
|
+
*/
|
|
400
|
+
slugify(text) {
|
|
401
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 60);
|
|
402
|
+
}
|
|
180
403
|
/**
|
|
181
404
|
* Backfill increment metadata.json with GitHub issue reference (v1.0.240)
|
|
182
405
|
*
|