specweave 1.0.301 → 1.0.304

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.
Files changed (74) hide show
  1. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -1
  2. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +44 -25
  3. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -1
  4. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +6 -0
  5. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +36 -1
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  8. package/dist/plugins/specweave-github/lib/github-feature-sync.js +266 -5
  9. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  10. package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts +2 -1
  11. package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts.map +1 -1
  12. package/dist/plugins/specweave-github/lib/user-story-content-builder.js +6 -4
  13. package/dist/plugins/specweave-github/lib/user-story-content-builder.js.map +1 -1
  14. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -1
  15. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +37 -17
  16. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -1
  17. package/dist/src/cli/commands/refresh-plugins.d.ts.map +1 -1
  18. package/dist/src/cli/commands/refresh-plugins.js +9 -0
  19. package/dist/src/cli/commands/refresh-plugins.js.map +1 -1
  20. package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
  21. package/dist/src/cli/commands/sync-progress.js +72 -2
  22. package/dist/src/cli/commands/sync-progress.js.map +1 -1
  23. package/dist/src/config/types.d.ts +2 -2
  24. package/dist/src/core/increment/increment-utils.d.ts +27 -4
  25. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  26. package/dist/src/core/increment/increment-utils.js +44 -17
  27. package/dist/src/core/increment/increment-utils.js.map +1 -1
  28. package/dist/src/core/increment/template-creator.d.ts +26 -0
  29. package/dist/src/core/increment/template-creator.d.ts.map +1 -1
  30. package/dist/src/core/increment/template-creator.js +179 -20
  31. package/dist/src/core/increment/template-creator.js.map +1 -1
  32. package/dist/src/importers/import-to-increment.d.ts +111 -0
  33. package/dist/src/importers/import-to-increment.d.ts.map +1 -0
  34. package/dist/src/importers/import-to-increment.js +223 -0
  35. package/dist/src/importers/import-to-increment.js.map +1 -0
  36. package/dist/src/importers/increment-external-ref-detector.d.ts +78 -0
  37. package/dist/src/importers/increment-external-ref-detector.d.ts.map +1 -0
  38. package/dist/src/importers/increment-external-ref-detector.js +130 -0
  39. package/dist/src/importers/increment-external-ref-detector.js.map +1 -0
  40. package/dist/src/init/research/types.d.ts +1 -1
  41. package/dist/src/sync/external-issue-auto-creator.d.ts.map +1 -1
  42. package/dist/src/sync/external-issue-auto-creator.js +28 -1
  43. package/dist/src/sync/external-issue-auto-creator.js.map +1 -1
  44. package/dist/src/sync/sync-coordinator.d.ts +6 -0
  45. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  46. package/dist/src/sync/sync-coordinator.js +42 -2
  47. package/dist/src/sync/sync-coordinator.js.map +1 -1
  48. package/package.json +1 -1
  49. package/plugins/specweave/hooks/lib/update-active-increment.sh +2 -2
  50. package/plugins/specweave/hooks/lib/update-status-line.sh +2 -2
  51. package/plugins/specweave/hooks/stop-auto-v5.sh +28 -8
  52. package/plugins/specweave/hooks/stop-sync.sh +10 -5
  53. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +8 -4
  54. package/plugins/specweave/hooks/user-prompt-submit.sh +130 -112
  55. package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +6 -3
  56. package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +4 -3
  57. package/plugins/specweave/skills/auto/SKILL.md +5 -3
  58. package/plugins/specweave/skills/done/SKILL.md +9 -3
  59. package/plugins/specweave/skills/import/SKILL.md +186 -0
  60. package/plugins/specweave/skills/increment/SKILL.md +30 -16
  61. package/plugins/specweave/skills/pm/SKILL.md +29 -2
  62. package/plugins/specweave/skills/pm/phases/00-deep-interview.md +12 -0
  63. package/plugins/specweave/skills/team-lead/SKILL.md +4 -2
  64. package/plugins/specweave/skills/team-merge/SKILL.md +2 -2
  65. package/plugins/specweave-github/lib/github-ac-comment-poster.js +31 -19
  66. package/plugins/specweave-github/lib/github-ac-comment-poster.ts +44 -27
  67. package/plugins/specweave-github/lib/github-feature-sync-cli.js +5 -0
  68. package/plugins/specweave-github/lib/github-feature-sync-cli.ts +7 -1
  69. package/plugins/specweave-github/lib/github-feature-sync.js +274 -6
  70. package/plugins/specweave-github/lib/github-feature-sync.ts +353 -5
  71. package/plugins/specweave-github/lib/user-story-content-builder.js +6 -4
  72. package/plugins/specweave-github/lib/user-story-content-builder.ts +6 -4
  73. package/plugins/specweave-github/lib/user-story-issue-builder.js +26 -11
  74. package/plugins/specweave-github/lib/user-story-issue-builder.ts +37 -19
@@ -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. Cover relevant categories (skip those that don't apply)
48
- 4. Only proceed to Research phase after sufficient clarity
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:
@@ -664,8 +664,10 @@ Orchestrator Final Check:
664
664
  2. No unresolved BLOCKING_ISSUE messages
665
665
  3. Run full test suite (all domains combined)
666
666
  4. Run /sw:grill on the combined increment
667
- 5. If all pass -> /sw:team-merge
668
- 6. If failures -> identify owning agent, send fix request via SendMessage
667
+ 5. Run /sw:done --auto <id> for each increment in dependency order
668
+ 6. If any /sw:done --auto fails, report the failure and continue with remaining increments
669
+ 7. If all pass -> /sw:team-merge
670
+ 8. If failures -> identify owning agent, send fix request via SendMessage
669
671
  ```
670
672
 
671
673
  ### Grill Checklist per Domain
@@ -71,8 +71,8 @@ Closure order respects contract chain:
71
71
  For each teammate's increment, in dependency order:
72
72
 
73
73
  ```bash
74
- # Run /sw:done per increment -- triggers quality gates
75
- /sw:done <increment-id>
74
+ # Run /sw:done --auto per increment -- triggers quality gates, skips user confirmation
75
+ /sw:done <increment-id> --auto
76
76
  ```
77
77
 
78
78
  This ensures:
@@ -1,4 +1,6 @@
1
1
  import { readFile } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import * as path from "path";
2
4
  import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
3
5
  import { pushSyncUserStories } from "./github-push-sync.js";
4
6
  async function postACProgressComments(incrementId, affectedUSIds, specPath, options) {
@@ -16,7 +18,7 @@ async function postACProgressComments(incrementId, affectedUSIds, specPath, opti
16
18
  });
17
19
  return result;
18
20
  }
19
- const issueLinks = parseIssueLinks(content);
21
+ const issueLinks = await parseIssueLinks(specPath);
20
22
  const repoSlug = `${options.owner}/${options.repo}`;
21
23
  const env = options.token ? { GH_TOKEN: options.token } : void 0;
22
24
  for (const usId of affectedUSIds) {
@@ -53,26 +55,36 @@ async function postACProgressComments(incrementId, affectedUSIds, specPath, opti
53
55
  }
54
56
  return result;
55
57
  }
56
- function parseIssueLinks(content) {
58
+ async function parseIssueLinks(specPath) {
57
59
  const links = {};
58
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
59
- if (!fmMatch) return links;
60
- const frontmatter = fmMatch[1];
61
- const usBlockMatch = frontmatter.match(/userStories:\s*\n((?:\s{6,}[\s\S]*?)(?=\n\s{0,3}\S|$))/);
62
- if (!usBlockMatch) return links;
63
- const usBlock = usBlockMatch[1];
64
- const usEntries = usBlock.match(/^\s+(US-\d+):\s*\n((?:\s+\w[\s\S]*?)(?=\n\s+US-|\s*$))/gm);
65
- if (!usEntries) return links;
66
- for (const entry of usEntries) {
67
- const idMatch = entry.match(/(US-\d+):/);
68
- const numMatch = entry.match(/issueNumber:\s*(\d+)/);
69
- const urlMatch = entry.match(/issueUrl:\s*"([^"]+)"/);
70
- if (idMatch && numMatch) {
71
- links[idMatch[1]] = {
72
- issueNumber: parseInt(numMatch[1], 10),
73
- issueUrl: urlMatch ? urlMatch[1] : ""
74
- };
60
+ try {
61
+ const metadataPath = path.join(path.dirname(specPath), "metadata.json");
62
+ if (!existsSync(metadataPath)) return links;
63
+ const raw = await readFile(metadataPath, "utf-8");
64
+ const metadata = JSON.parse(raw);
65
+ if (metadata.github?.issues && Array.isArray(metadata.github.issues)) {
66
+ for (const entry of metadata.github.issues) {
67
+ if (entry.userStory && entry.number) {
68
+ links[entry.userStory] = {
69
+ issueNumber: entry.number,
70
+ issueUrl: entry.url || ""
71
+ };
72
+ }
73
+ }
74
+ }
75
+ if (metadata.externalLinks?.github?.issues) {
76
+ const issues = metadata.externalLinks.github.issues;
77
+ for (const [usId, data] of Object.entries(issues)) {
78
+ const issueData = data;
79
+ if (issueData.issueNumber) {
80
+ links[usId] = {
81
+ issueNumber: issueData.issueNumber,
82
+ issueUrl: issueData.issueUrl || ""
83
+ };
84
+ }
85
+ }
75
86
  }
87
+ } catch {
76
88
  }
77
89
  return links;
78
90
  }
@@ -9,6 +9,8 @@
9
9
  */
10
10
 
11
11
  import { readFile } from 'fs/promises';
12
+ import { existsSync } from 'fs';
13
+ import * as path from 'path';
12
14
  import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
13
15
  import { pushSyncUserStories } from './github-push-sync.js';
14
16
  import type { UserStoryForSync } from './github-push-sync.js';
@@ -69,7 +71,7 @@ export async function postACProgressComments(
69
71
  return result;
70
72
  }
71
73
 
72
- const issueLinks = parseIssueLinks(content);
74
+ const issueLinks = await parseIssueLinks(specPath);
73
75
  const repoSlug = `${options.owner}/${options.repo}`;
74
76
  const env = options.token ? { GH_TOKEN: options.token } : undefined;
75
77
 
@@ -116,37 +118,52 @@ export async function postACProgressComments(
116
118
  }
117
119
 
118
120
  /**
119
- * Parse issue links from spec.md YAML frontmatter.
120
- * Extracts externalLinks.github.userStories entries.
121
+ * Parse issue links from metadata.json (sibling of spec.md).
122
+ *
123
+ * Supports TWO formats:
124
+ * - OLD: metadata.github.issues[] with { userStory, number, url }
125
+ * - NEW: metadata.externalLinks.github.issues with { [US-XXX]: { issueNumber, issueUrl } }
126
+ *
127
+ * Falls back to empty if metadata.json is missing or invalid.
121
128
  */
122
- function parseIssueLinks(content: string): Record<string, ParsedUSIssueLink> {
129
+ async function parseIssueLinks(specPath: string): Promise<Record<string, ParsedUSIssueLink>> {
123
130
  const links: Record<string, ParsedUSIssueLink> = {};
124
131
 
125
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
126
- if (!fmMatch) return links;
127
-
128
- const frontmatter = fmMatch[1];
129
-
130
- // Parse userStories section from YAML frontmatter
131
- const usBlockMatch = frontmatter.match(/userStories:\s*\n((?:\s{6,}[\s\S]*?)(?=\n\s{0,3}\S|$))/);
132
- if (!usBlockMatch) return links;
133
-
134
- const usBlock = usBlockMatch[1];
135
- const usEntries = usBlock.match(/^\s+(US-\d+):\s*\n((?:\s+\w[\s\S]*?)(?=\n\s+US-|\s*$))/gm);
136
-
137
- if (!usEntries) return links;
138
-
139
- for (const entry of usEntries) {
140
- const idMatch = entry.match(/(US-\d+):/);
141
- const numMatch = entry.match(/issueNumber:\s*(\d+)/);
142
- const urlMatch = entry.match(/issueUrl:\s*"([^"]+)"/);
132
+ try {
133
+ const metadataPath = path.join(path.dirname(specPath), 'metadata.json');
134
+ if (!existsSync(metadataPath)) return links;
135
+
136
+ const raw = await readFile(metadataPath, 'utf-8');
137
+ const metadata = JSON.parse(raw);
138
+
139
+ // OLD format: metadata.github.issues[] array
140
+ if (metadata.github?.issues && Array.isArray(metadata.github.issues)) {
141
+ for (const entry of metadata.github.issues) {
142
+ if (entry.userStory && entry.number) {
143
+ links[entry.userStory] = {
144
+ issueNumber: entry.number,
145
+ issueUrl: entry.url || '',
146
+ };
147
+ }
148
+ }
149
+ }
143
150
 
144
- if (idMatch && numMatch) {
145
- links[idMatch[1]] = {
146
- issueNumber: parseInt(numMatch[1], 10),
147
- issueUrl: urlMatch ? urlMatch[1] : '',
148
- };
151
+ // NEW format: metadata.externalLinks.github.issues object
152
+ if (metadata.externalLinks?.github?.issues) {
153
+ const issues = metadata.externalLinks.github.issues;
154
+ for (const [usId, data] of Object.entries(issues)) {
155
+ const issueData = data as { issueNumber?: number; issueUrl?: string };
156
+ if (issueData.issueNumber) {
157
+ // NEW format entries override OLD format for the same US
158
+ links[usId] = {
159
+ issueNumber: issueData.issueNumber,
160
+ issueUrl: issueData.issueUrl || '',
161
+ };
162
+ }
163
+ }
149
164
  }
165
+ } catch {
166
+ // Graceful fallback: return empty if metadata.json is missing or invalid
150
167
  }
151
168
 
152
169
  return links;
@@ -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
  });