specweave 0.28.15 → 0.28.19
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/bin/specweave.js +1 -1
- package/dist/plugins/specweave-ado/lib/ado-board-resolver.d.ts +94 -0
- package/dist/plugins/specweave-ado/lib/ado-board-resolver.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-board-resolver.js +219 -0
- package/dist/plugins/specweave-ado/lib/ado-board-resolver.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +6 -11
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +6 -11
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +19 -0
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +380 -0
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +1 -0
- package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +92 -0
- package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/increment-issue-builder.js +349 -0
- package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-board-resolver.d.ts +50 -0
- package/dist/plugins/specweave-jira/lib/jira-board-resolver.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-board-resolver.js +84 -0
- package/dist/plugins/specweave-jira/lib/jira-board-resolver.js.map +1 -0
- package/dist/src/cli/commands/import-external.d.ts.map +1 -1
- package/dist/src/cli/commands/import-external.js +12 -7
- package/dist/src/cli/commands/import-external.js.map +1 -1
- package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/external-import.js +138 -23
- package/dist/src/cli/helpers/init/external-import.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/ado-area-selection.d.ts +65 -0
- package/dist/src/cli/helpers/issue-tracker/ado-area-selection.d.ts.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/ado-area-selection.js +278 -0
- package/dist/src/cli/helpers/issue-tracker/ado-area-selection.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/jira-board-selection.d.ts +64 -0
- package/dist/src/cli/helpers/issue-tracker/jira-board-selection.d.ts.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/jira-board-selection.js +251 -0
- package/dist/src/cli/helpers/issue-tracker/jira-board-selection.js.map +1 -0
- package/dist/src/core/ac-test-validator-cli.js +4 -1
- package/dist/src/core/ac-test-validator-cli.js.map +1 -1
- package/dist/src/core/ac-test-validator.d.ts.map +1 -1
- package/dist/src/core/ac-test-validator.js +4 -1
- package/dist/src/core/ac-test-validator.js.map +1 -1
- package/dist/src/core/qa/qa-runner.js +7 -10
- package/dist/src/core/qa/qa-runner.js.map +1 -1
- package/dist/src/core/types/increment-metadata.d.ts +75 -0
- package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
- package/dist/src/core/types/sync-profile.d.ts +137 -5
- package/dist/src/core/types/sync-profile.d.ts.map +1 -1
- package/dist/src/core/types/sync-profile.js +63 -0
- package/dist/src/core/types/sync-profile.js.map +1 -1
- package/dist/src/importers/external-importer.d.ts +25 -0
- package/dist/src/importers/external-importer.d.ts.map +1 -1
- package/dist/src/importers/github-importer.d.ts.map +1 -1
- package/dist/src/importers/github-importer.js +5 -3
- package/dist/src/importers/github-importer.js.map +1 -1
- package/dist/src/importers/item-converter.d.ts +51 -0
- package/dist/src/importers/item-converter.d.ts.map +1 -1
- package/dist/src/importers/item-converter.js +39 -12
- package/dist/src/importers/item-converter.js.map +1 -1
- package/dist/src/init/repo/types.d.ts +1 -1
- package/dist/src/living-docs/fs-id-allocator.d.ts +72 -3
- package/dist/src/living-docs/fs-id-allocator.d.ts.map +1 -1
- package/dist/src/living-docs/fs-id-allocator.js +142 -16
- package/dist/src/living-docs/fs-id-allocator.js.map +1 -1
- package/dist/src/locales/de/cli.json +14 -0
- package/dist/src/locales/es/cli.json +14 -0
- package/dist/src/locales/fr/cli.json +14 -0
- package/dist/src/locales/ja/cli.json +14 -0
- package/dist/src/locales/ko/cli.json +14 -0
- package/dist/src/locales/pt/cli.json +14 -0
- package/dist/src/locales/ru/cli.json +14 -0
- package/dist/src/locales/zh/cli.json +14 -0
- package/dist/src/utils/chalk-fallback.d.ts +38 -0
- package/dist/src/utils/chalk-fallback.d.ts.map +1 -0
- package/dist/src/utils/chalk-fallback.js +118 -0
- package/dist/src/utils/chalk-fallback.js.map +1 -0
- package/dist/src/utils/project-id-generator.d.ts +127 -0
- package/dist/src/utils/project-id-generator.d.ts.map +1 -0
- package/dist/src/utils/project-id-generator.js +228 -0
- package/dist/src/utils/project-id-generator.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/agents/AGENTS-INDEX.md +9 -7
- package/plugins/specweave/agents/pm/AGENT.md +202 -0
- package/plugins/specweave/commands/specweave-import-external.md +5 -3
- package/plugins/specweave/commands/specweave-qa.md +9 -9
- package/plugins/specweave/commands/specweave-save.md +531 -193
- package/plugins/specweave/commands/specweave-sync-docs.md +6 -2
- package/plugins/specweave/commands/specweave-validate.md +8 -7
- package/plugins/specweave/hooks/pre-task-completion.sh +35 -17
- package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.d.ts +16 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.js +121 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.js.map +1 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator.d.ts +111 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator.js +295 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator.js.map +1 -0
- package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +75 -0
- package/plugins/specweave/lib/vendor/utils/chalk-fallback.d.ts +38 -0
- package/plugins/specweave/lib/vendor/utils/chalk-fallback.js +118 -0
- package/plugins/specweave/lib/vendor/utils/chalk-fallback.js.map +1 -0
- package/plugins/specweave/lib/vendor/utils/fs-native.d.ts +179 -0
- package/plugins/specweave/lib/vendor/utils/fs-native.js +319 -0
- package/plugins/specweave/lib/vendor/utils/fs-native.js.map +1 -0
- package/plugins/specweave/skills/code-reviewer/SKILL.md +1 -1
- package/plugins/specweave/skills/docs-updater/SKILL.md +61 -0
- package/plugins/specweave/skills/increment-planner/SKILL.md +10 -335
- package/plugins/specweave/skills/increment-planner/templates/metadata.json +13 -0
- package/plugins/specweave/skills/increment-planner/templates/plan.md +50 -0
- package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +86 -0
- package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +50 -0
- package/plugins/specweave/skills/increment-planner/templates/tasks-multi-project.md +86 -0
- package/plugins/specweave/skills/increment-planner/templates/tasks-single-project.md +48 -0
- package/plugins/specweave/skills/increment-quality-judge-v2/SKILL.md +18 -0
- package/plugins/specweave-ado/commands/specweave-ado-import-areas.md +358 -0
- package/plugins/specweave-alternatives/skills/architecture-alternatives/SKILL.md +1 -0
- package/plugins/specweave-alternatives/skills/bmad-method/SKILL.md +1 -0
- package/plugins/specweave-core/skills/code-quality/SKILL.md +1 -0
- package/plugins/specweave-core/skills/design-patterns/SKILL.md +1 -0
- package/plugins/specweave-core/skills/software-architecture/SKILL.md +1 -0
- package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +14 -10
- package/plugins/specweave-github/commands/specweave-github-sync.md +57 -0
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +86 -0
- package/plugins/specweave-github/lib/github-feature-sync.ts +6 -11
- package/plugins/specweave-github/lib/github-increment-sync-cli.js +343 -0
- package/plugins/specweave-github/lib/github-increment-sync-cli.ts +484 -0
- package/plugins/specweave-github/lib/increment-issue-builder.js +368 -0
- package/plugins/specweave-github/lib/increment-issue-builder.ts +471 -0
- package/plugins/specweave-github/skills/github-issue-standard/SKILL.md +19 -24
- package/plugins/specweave-infrastructure/agents/observability-engineer/AGENT.md +15 -23
- package/plugins/specweave-jira/commands/specweave-jira-import-boards.md +331 -0
- package/plugins/specweave-ml/agents/data-scientist/AGENT.md +16 -20
- package/plugins/specweave-ml/agents/ml-engineer/AGENT.md +18 -19
- package/plugins/specweave-ml/skills/{ml-pipeline-workflow → mlops-dag-builder}/SKILL.md +18 -14
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +129 -0
- package/plugins/specweave-ui/skills/browser-automation/SKILL.md +1 -1
- package/plugins/specweave-ui/skills/ui-testing/SKILL.md +10 -122
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +0 -70
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/epic-content-builder.js +0 -258
- package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +0 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +0 -83
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.js +0 -466
- package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +0 -1
- package/plugins/specweave/agents/increment-quality-judge-v2/AGENT.md +0 -705
- package/plugins/specweave-github/lib/epic-content-builder.js +0 -265
- package/plugins/specweave-github/lib/epic-content-builder.ts +0 -376
- package/plugins/specweave-github/lib/github-epic-sync.js +0 -488
- package/plugins/specweave-github/lib/github-epic-sync.ts +0 -715
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile, writeFile } from "fs/promises";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
import * as yaml from "yaml";
|
|
5
|
-
import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
|
|
6
|
-
import { DuplicateDetector } from "./duplicate-detector.js";
|
|
7
|
-
import { EpicContentBuilder } from "./epic-content-builder.js";
|
|
8
|
-
class GitHubEpicSync {
|
|
9
|
-
constructor(client, specsDir) {
|
|
10
|
-
this.client = client;
|
|
11
|
-
this.specsDir = specsDir;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Sync Epic folder to GitHub (Milestone + Issues)
|
|
15
|
-
*/
|
|
16
|
-
async syncEpicToGitHub(epicId) {
|
|
17
|
-
console.log(`
|
|
18
|
-
\u{1F504} Syncing Epic ${epicId} to GitHub...`);
|
|
19
|
-
const epicFolder = await this.findEpicFolder(epicId);
|
|
20
|
-
if (!epicFolder) {
|
|
21
|
-
throw new Error(`Epic ${epicId} not found in ${this.specsDir}`);
|
|
22
|
-
}
|
|
23
|
-
const readmePath = path.join(epicFolder, "FEATURE.md");
|
|
24
|
-
const epicData = await this.parseEpicReadme(readmePath);
|
|
25
|
-
console.log(` \u{1F4E6} Epic: ${epicData.title}`);
|
|
26
|
-
console.log(` \u{1F4CA} Increments: ${epicData.total_increments}`);
|
|
27
|
-
let milestoneNumber = epicData.external_tools.github.id;
|
|
28
|
-
let milestoneUrl = epicData.external_tools.github.url;
|
|
29
|
-
if (!milestoneNumber) {
|
|
30
|
-
console.log(` \u{1F680} Creating GitHub Milestone...`);
|
|
31
|
-
const milestone = await this.createMilestone(epicData);
|
|
32
|
-
milestoneNumber = milestone.number;
|
|
33
|
-
milestoneUrl = milestone.url;
|
|
34
|
-
console.log(` \u2705 Created Milestone #${milestoneNumber}`);
|
|
35
|
-
await this.updateEpicReadme(readmePath, {
|
|
36
|
-
type: "milestone",
|
|
37
|
-
id: milestoneNumber,
|
|
38
|
-
url: milestoneUrl
|
|
39
|
-
});
|
|
40
|
-
} else {
|
|
41
|
-
console.log(` \u267B\uFE0F Updating existing Milestone #${milestoneNumber}...`);
|
|
42
|
-
await this.updateMilestone(milestoneNumber, epicData);
|
|
43
|
-
console.log(` \u2705 Updated Milestone #${milestoneNumber}`);
|
|
44
|
-
}
|
|
45
|
-
let issuesCreated = 0;
|
|
46
|
-
let issuesUpdated = 0;
|
|
47
|
-
let duplicatesDetected = 0;
|
|
48
|
-
console.log(`
|
|
49
|
-
\u{1F4DD} Syncing ${epicData.increments.length} increments...`);
|
|
50
|
-
for (const increment of epicData.increments) {
|
|
51
|
-
const incrementFile = path.join(epicFolder, `${increment.id}.md`);
|
|
52
|
-
if (!existsSync(incrementFile)) {
|
|
53
|
-
console.log(` \u26A0\uFE0F Increment file not found: ${increment.id}.md`);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
const incrementData = await this.parseIncrementFile(incrementFile);
|
|
57
|
-
const existingIssue = increment.external.github;
|
|
58
|
-
if (!existingIssue) {
|
|
59
|
-
console.log(` \u{1F50D} Checking GitHub for existing issue: ${increment.id}...`);
|
|
60
|
-
const githubIssue = await this.findExistingIssue(epicData.id, increment.id);
|
|
61
|
-
if (githubIssue) {
|
|
62
|
-
console.log(` \u267B\uFE0F Found existing Issue #${githubIssue} for ${increment.id} (self-healing)`);
|
|
63
|
-
await this.updateIncrementExternalLink(
|
|
64
|
-
readmePath,
|
|
65
|
-
incrementFile,
|
|
66
|
-
increment.id,
|
|
67
|
-
githubIssue
|
|
68
|
-
);
|
|
69
|
-
issuesUpdated++;
|
|
70
|
-
duplicatesDetected++;
|
|
71
|
-
} else {
|
|
72
|
-
const issueNumber = await this.createIssue(
|
|
73
|
-
epicData.id,
|
|
74
|
-
incrementData,
|
|
75
|
-
milestoneNumber
|
|
76
|
-
);
|
|
77
|
-
issuesCreated++;
|
|
78
|
-
console.log(` \u2705 Created Issue #${issueNumber} for ${increment.id}`);
|
|
79
|
-
await this.updateIncrementExternalLink(
|
|
80
|
-
readmePath,
|
|
81
|
-
incrementFile,
|
|
82
|
-
increment.id,
|
|
83
|
-
issueNumber
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
await this.updateIssue(
|
|
88
|
-
epicData.id,
|
|
89
|
-
existingIssue,
|
|
90
|
-
incrementData,
|
|
91
|
-
milestoneNumber
|
|
92
|
-
);
|
|
93
|
-
issuesUpdated++;
|
|
94
|
-
console.log(` \u267B\uFE0F Updated Issue #${existingIssue} for ${increment.id}`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
console.log(`
|
|
98
|
-
\u2705 Epic sync complete!`);
|
|
99
|
-
console.log(` Milestone: ${milestoneUrl}`);
|
|
100
|
-
console.log(` Issues created: ${issuesCreated}`);
|
|
101
|
-
console.log(` Issues updated: ${issuesUpdated}`);
|
|
102
|
-
if (duplicatesDetected > 0) {
|
|
103
|
-
console.log(` \u{1F517} Self-healed: ${duplicatesDetected} (found existing issues)`);
|
|
104
|
-
}
|
|
105
|
-
console.log(`
|
|
106
|
-
\u{1F50D} Post-sync validation...`);
|
|
107
|
-
const validation = await this.validateSync(epicData.id);
|
|
108
|
-
if (validation.duplicatesFound > 0) {
|
|
109
|
-
console.warn(`
|
|
110
|
-
\u26A0\uFE0F WARNING: ${validation.duplicatesFound} duplicate(s) detected!`);
|
|
111
|
-
console.warn(` This may indicate a previous sync created duplicates.`);
|
|
112
|
-
console.warn(` Run cleanup command to resolve:`);
|
|
113
|
-
console.warn(` /specweave-github:cleanup-duplicates ${epicData.id}`);
|
|
114
|
-
console.warn(`
|
|
115
|
-
Duplicate groups:`);
|
|
116
|
-
for (const [title, numbers] of validation.duplicateGroups) {
|
|
117
|
-
console.warn(` - "${title}": Issues #${numbers.join(", #")}`);
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
console.log(` \u2705 No duplicates found`);
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
milestoneNumber,
|
|
124
|
-
milestoneUrl,
|
|
125
|
-
issuesCreated,
|
|
126
|
-
issuesUpdated,
|
|
127
|
-
duplicatesDetected
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Validate sync results - check for duplicate issues
|
|
132
|
-
*
|
|
133
|
-
* Searches GitHub for all issues with the Epic ID and detects duplicates
|
|
134
|
-
* (multiple issues with the same title).
|
|
135
|
-
*
|
|
136
|
-
* @param epicId - Epic ID (e.g., FS-031)
|
|
137
|
-
* @returns Validation result with duplicate count and groups
|
|
138
|
-
*/
|
|
139
|
-
async validateSync(epicId) {
|
|
140
|
-
try {
|
|
141
|
-
const titlePattern = `[${epicId}]`;
|
|
142
|
-
const result = await execFileNoThrow("gh", [
|
|
143
|
-
"issue",
|
|
144
|
-
"list",
|
|
145
|
-
"--search",
|
|
146
|
-
`"${titlePattern}" in:title`,
|
|
147
|
-
"--json",
|
|
148
|
-
"number,title,state",
|
|
149
|
-
"--limit",
|
|
150
|
-
"100",
|
|
151
|
-
// Check up to 100 issues
|
|
152
|
-
"--state",
|
|
153
|
-
"all"
|
|
154
|
-
// Include both open and closed
|
|
155
|
-
]);
|
|
156
|
-
if (result.exitCode !== 0 || !result.stdout) {
|
|
157
|
-
console.warn(` \u26A0\uFE0F Validation failed: ${result.stderr || "unknown error"}`);
|
|
158
|
-
return { totalIssues: 0, duplicatesFound: 0, duplicateGroups: [] };
|
|
159
|
-
}
|
|
160
|
-
const issues = JSON.parse(result.stdout);
|
|
161
|
-
const titleGroups = /* @__PURE__ */ new Map();
|
|
162
|
-
for (const issue of issues) {
|
|
163
|
-
const title = issue.title;
|
|
164
|
-
if (!titleGroups.has(title)) {
|
|
165
|
-
titleGroups.set(title, []);
|
|
166
|
-
}
|
|
167
|
-
titleGroups.get(title).push(issue.number);
|
|
168
|
-
}
|
|
169
|
-
const duplicateGroups = [];
|
|
170
|
-
for (const [title, numbers] of titleGroups.entries()) {
|
|
171
|
-
if (numbers.length > 1) {
|
|
172
|
-
duplicateGroups.push([title, numbers]);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return {
|
|
176
|
-
totalIssues: issues.length,
|
|
177
|
-
duplicatesFound: duplicateGroups.length,
|
|
178
|
-
duplicateGroups
|
|
179
|
-
};
|
|
180
|
-
} catch (error) {
|
|
181
|
-
console.warn(` \u26A0\uFE0F Validation error: ${error}`);
|
|
182
|
-
return { totalIssues: 0, duplicatesFound: 0, duplicateGroups: [] };
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Find existing GitHub issue for increment (duplicate detection!)
|
|
187
|
-
*
|
|
188
|
-
* Searches GitHub for issues matching the Epic ID and Increment ID.
|
|
189
|
-
* This prevents creating duplicates when frontmatter is lost/corrupted.
|
|
190
|
-
*
|
|
191
|
-
* @param epicId - Epic ID (e.g., FS-031)
|
|
192
|
-
* @param incrementId - Increment ID (e.g., 0031-feature-name)
|
|
193
|
-
* @returns GitHub issue number if found, null otherwise
|
|
194
|
-
*/
|
|
195
|
-
async findExistingIssue(epicId, incrementId) {
|
|
196
|
-
try {
|
|
197
|
-
const titlePattern = `[${epicId}]`;
|
|
198
|
-
const result = await execFileNoThrow("gh", [
|
|
199
|
-
"issue",
|
|
200
|
-
"list",
|
|
201
|
-
"--search",
|
|
202
|
-
`"${titlePattern}" in:title`,
|
|
203
|
-
"--json",
|
|
204
|
-
"number,title,body",
|
|
205
|
-
"--limit",
|
|
206
|
-
"50"
|
|
207
|
-
// Check up to 50 issues (should cover most Epics)
|
|
208
|
-
]);
|
|
209
|
-
if (result.exitCode !== 0 || !result.stdout) {
|
|
210
|
-
console.warn(` \u26A0\uFE0F GitHub search failed: ${result.stderr || "unknown error"}`);
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
const issues = JSON.parse(result.stdout);
|
|
214
|
-
if (issues.length === 0) {
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
for (const issue of issues) {
|
|
218
|
-
if (issue.body && issue.body.includes(`**Increment**: ${incrementId}`)) {
|
|
219
|
-
console.log(
|
|
220
|
-
` \u{1F517} Found existing issue #${issue.number} for ${incrementId}`
|
|
221
|
-
);
|
|
222
|
-
return issue.number;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
for (const issue of issues) {
|
|
226
|
-
if (issue.title.toLowerCase().includes(incrementId.toLowerCase())) {
|
|
227
|
-
console.log(
|
|
228
|
-
` \u{1F517} Found existing issue #${issue.number} for ${incrementId} (title match)`
|
|
229
|
-
);
|
|
230
|
-
return issue.number;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return null;
|
|
234
|
-
} catch (error) {
|
|
235
|
-
console.warn(` \u26A0\uFE0F Error searching for existing issue: ${error}`);
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Find Epic folder by ID (FS-001 or just 001)
|
|
241
|
-
*/
|
|
242
|
-
async findEpicFolder(epicId) {
|
|
243
|
-
const normalizedId = epicId.startsWith("FS-") ? epicId : `FS-${epicId.padStart(3, "0")}`;
|
|
244
|
-
const folders = await readdir(this.specsDir);
|
|
245
|
-
for (const folder of folders) {
|
|
246
|
-
if (folder.startsWith(normalizedId)) {
|
|
247
|
-
return path.join(this.specsDir, folder);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Parse Epic FEATURE.md to extract frontmatter
|
|
254
|
-
*/
|
|
255
|
-
async parseEpicReadme(readmePath) {
|
|
256
|
-
const content = await readFile(readmePath, "utf-8");
|
|
257
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
258
|
-
if (!match) {
|
|
259
|
-
throw new Error("Epic FEATURE.md missing YAML frontmatter");
|
|
260
|
-
}
|
|
261
|
-
const frontmatter = yaml.parse(match[1]);
|
|
262
|
-
return frontmatter;
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Parse increment file to extract title and overview
|
|
266
|
-
*/
|
|
267
|
-
async parseIncrementFile(filePath) {
|
|
268
|
-
const content = await readFile(filePath, "utf-8");
|
|
269
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
270
|
-
let frontmatter = { id: "", epic: "" };
|
|
271
|
-
let bodyContent = content;
|
|
272
|
-
if (match) {
|
|
273
|
-
frontmatter = yaml.parse(match[1]);
|
|
274
|
-
bodyContent = content.slice(match[0].length).trim();
|
|
275
|
-
}
|
|
276
|
-
const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
|
|
277
|
-
const title = titleMatch ? titleMatch[1].trim() : frontmatter.id || path.basename(filePath, ".md");
|
|
278
|
-
const overviewMatch = bodyContent.match(/^#[^\n]+\n+([^\n]+)/);
|
|
279
|
-
const overview = overviewMatch ? overviewMatch[1].trim() : "No overview available";
|
|
280
|
-
return {
|
|
281
|
-
id: frontmatter.id,
|
|
282
|
-
title,
|
|
283
|
-
overview,
|
|
284
|
-
content: bodyContent,
|
|
285
|
-
frontmatter
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Create GitHub Milestone
|
|
290
|
-
*/
|
|
291
|
-
async createMilestone(epic) {
|
|
292
|
-
const title = `[${epic.id}] ${epic.title}`;
|
|
293
|
-
const description = `Epic: ${epic.title}
|
|
294
|
-
|
|
295
|
-
Progress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})
|
|
296
|
-
|
|
297
|
-
Priority: ${epic.priority}
|
|
298
|
-
Status: ${epic.status}`;
|
|
299
|
-
const state = epic.status === "complete" ? "closed" : "open";
|
|
300
|
-
const result = await execFileNoThrow("gh", [
|
|
301
|
-
"api",
|
|
302
|
-
"/repos/{owner}/{repo}/milestones",
|
|
303
|
-
"-X",
|
|
304
|
-
"POST",
|
|
305
|
-
"-f",
|
|
306
|
-
`title=${title}`,
|
|
307
|
-
"-f",
|
|
308
|
-
`description=${description}`,
|
|
309
|
-
"-f",
|
|
310
|
-
`state=${state}`
|
|
311
|
-
]);
|
|
312
|
-
if (result.exitCode !== 0) {
|
|
313
|
-
throw new Error(
|
|
314
|
-
`Failed to create GitHub Milestone: ${result.stderr || result.stdout}`
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
const milestone = JSON.parse(result.stdout);
|
|
318
|
-
return {
|
|
319
|
-
number: milestone.number,
|
|
320
|
-
url: milestone.html_url
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Update GitHub Milestone
|
|
325
|
-
*/
|
|
326
|
-
async updateMilestone(milestoneNumber, epic) {
|
|
327
|
-
const title = `[${epic.id}] ${epic.title}`;
|
|
328
|
-
const description = `Epic: ${epic.title}
|
|
329
|
-
|
|
330
|
-
Progress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})
|
|
331
|
-
|
|
332
|
-
Priority: ${epic.priority}
|
|
333
|
-
Status: ${epic.status}`;
|
|
334
|
-
const state = epic.status === "complete" ? "closed" : "open";
|
|
335
|
-
const result = await execFileNoThrow("gh", [
|
|
336
|
-
"api",
|
|
337
|
-
`/repos/{owner}/{repo}/milestones/${milestoneNumber}`,
|
|
338
|
-
"-X",
|
|
339
|
-
"PATCH",
|
|
340
|
-
"-f",
|
|
341
|
-
`title=${title}`,
|
|
342
|
-
"-f",
|
|
343
|
-
`description=${description}`,
|
|
344
|
-
"-f",
|
|
345
|
-
`state=${state}`
|
|
346
|
-
]);
|
|
347
|
-
if (result.exitCode !== 0) {
|
|
348
|
-
throw new Error(
|
|
349
|
-
`Failed to update GitHub Milestone: ${result.stderr || result.stdout}`
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Create GitHub Issue for Epic with hierarchical content (US → Tasks)
|
|
355
|
-
*/
|
|
356
|
-
async createIssue(epicId, increment, milestoneNumber) {
|
|
357
|
-
const title = `[${epicId}] ${increment.title}`;
|
|
358
|
-
const epicFolder = await this.findEpicFolder(epicId);
|
|
359
|
-
if (!epicFolder) {
|
|
360
|
-
throw new Error(`Epic folder not found for ${epicId}`);
|
|
361
|
-
}
|
|
362
|
-
const contentBuilder = new EpicContentBuilder(
|
|
363
|
-
epicFolder,
|
|
364
|
-
path.dirname(this.specsDir)
|
|
365
|
-
// Project root
|
|
366
|
-
);
|
|
367
|
-
const body = await contentBuilder.buildIssueBody();
|
|
368
|
-
try {
|
|
369
|
-
const result = await DuplicateDetector.createWithProtection({
|
|
370
|
-
title,
|
|
371
|
-
body,
|
|
372
|
-
titlePattern: `[${epicId}]`,
|
|
373
|
-
incrementId: increment.id,
|
|
374
|
-
// For body matching
|
|
375
|
-
labels: ["increment", "epic-sync"],
|
|
376
|
-
milestone: milestoneNumber.toString()
|
|
377
|
-
});
|
|
378
|
-
if (result.wasReused) {
|
|
379
|
-
console.log(` \u267B\uFE0F Reused existing issue #${result.issue.number} (duplicate prevention)`);
|
|
380
|
-
}
|
|
381
|
-
if (result.duplicatesFound > 0) {
|
|
382
|
-
console.log(` \u{1F6E1}\uFE0F Duplicates: ${result.duplicatesFound} found, ${result.duplicatesClosed} closed`);
|
|
383
|
-
}
|
|
384
|
-
return result.issue.number;
|
|
385
|
-
} catch (error) {
|
|
386
|
-
throw new Error(`Failed to create GitHub Issue: ${error.message}`);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Update GitHub Issue for Epic with hierarchical content (US → Tasks)
|
|
391
|
-
*/
|
|
392
|
-
async updateIssue(epicId, issueNumber, increment, milestoneNumber) {
|
|
393
|
-
const title = `[${epicId}] ${increment.title}`;
|
|
394
|
-
const epicFolder = await this.findEpicFolder(epicId);
|
|
395
|
-
if (!epicFolder) {
|
|
396
|
-
throw new Error(`Epic folder not found for ${epicId}`);
|
|
397
|
-
}
|
|
398
|
-
const contentBuilder = new EpicContentBuilder(
|
|
399
|
-
epicFolder,
|
|
400
|
-
path.dirname(this.specsDir)
|
|
401
|
-
// Project root
|
|
402
|
-
);
|
|
403
|
-
const body = await contentBuilder.buildIssueBody();
|
|
404
|
-
const result = await execFileNoThrow("gh", [
|
|
405
|
-
"issue",
|
|
406
|
-
"edit",
|
|
407
|
-
issueNumber.toString(),
|
|
408
|
-
"--title",
|
|
409
|
-
title,
|
|
410
|
-
"--body",
|
|
411
|
-
body,
|
|
412
|
-
"--milestone",
|
|
413
|
-
milestoneNumber.toString()
|
|
414
|
-
]);
|
|
415
|
-
if (result.exitCode !== 0) {
|
|
416
|
-
throw new Error(
|
|
417
|
-
`Failed to update GitHub Issue: ${result.stderr || result.stdout}`
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Update Epic FEATURE.md with GitHub Milestone ID
|
|
423
|
-
*/
|
|
424
|
-
async updateEpicReadme(readmePath, github) {
|
|
425
|
-
const content = await readFile(readmePath, "utf-8");
|
|
426
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
427
|
-
if (!match) {
|
|
428
|
-
throw new Error("Epic FEATURE.md missing YAML frontmatter");
|
|
429
|
-
}
|
|
430
|
-
const frontmatter = yaml.parse(match[1]);
|
|
431
|
-
frontmatter.external_tools.github = github;
|
|
432
|
-
const newFrontmatter = yaml.stringify(frontmatter);
|
|
433
|
-
const newContent = content.replace(
|
|
434
|
-
/^---\n[\s\S]*?\n---/,
|
|
435
|
-
`---
|
|
436
|
-
${newFrontmatter}---`
|
|
437
|
-
);
|
|
438
|
-
await writeFile(readmePath, newContent, "utf-8");
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Update increment external link in both Epic README and increment file
|
|
442
|
-
*/
|
|
443
|
-
async updateIncrementExternalLink(readmePath, incrementFile, incrementId, issueNumber) {
|
|
444
|
-
const issueUrl = `https://github.com/{owner}/{repo}/issues/${issueNumber}`;
|
|
445
|
-
const readmeContent = await readFile(readmePath, "utf-8");
|
|
446
|
-
const readmeMatch = readmeContent.match(/^---\n([\s\S]*?)\n---/);
|
|
447
|
-
if (readmeMatch) {
|
|
448
|
-
const frontmatter = yaml.parse(readmeMatch[1]);
|
|
449
|
-
const increment = frontmatter.increments.find(
|
|
450
|
-
(inc) => inc.id === incrementId
|
|
451
|
-
);
|
|
452
|
-
if (increment) {
|
|
453
|
-
increment.external.github = issueNumber;
|
|
454
|
-
const newFrontmatter = yaml.stringify(frontmatter);
|
|
455
|
-
const newContent = readmeContent.replace(
|
|
456
|
-
/^---\n[\s\S]*?\n---/,
|
|
457
|
-
`---
|
|
458
|
-
${newFrontmatter}---`
|
|
459
|
-
);
|
|
460
|
-
await writeFile(readmePath, newContent, "utf-8");
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
const incrementContent = await readFile(incrementFile, "utf-8");
|
|
464
|
-
const incrementMatch = incrementContent.match(/^---\n([\s\S]*?)\n---/);
|
|
465
|
-
if (incrementMatch) {
|
|
466
|
-
const frontmatter = yaml.parse(
|
|
467
|
-
incrementMatch[1]
|
|
468
|
-
);
|
|
469
|
-
if (!frontmatter.external) {
|
|
470
|
-
frontmatter.external = {};
|
|
471
|
-
}
|
|
472
|
-
frontmatter.external.github = {
|
|
473
|
-
issue: issueNumber,
|
|
474
|
-
url: issueUrl
|
|
475
|
-
};
|
|
476
|
-
const newFrontmatter = yaml.stringify(frontmatter);
|
|
477
|
-
const newContent = incrementContent.replace(
|
|
478
|
-
/^---\n[\s\S]*?\n---/,
|
|
479
|
-
`---
|
|
480
|
-
${newFrontmatter}---`
|
|
481
|
-
);
|
|
482
|
-
await writeFile(incrementFile, newContent, "utf-8");
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
export {
|
|
487
|
-
GitHubEpicSync
|
|
488
|
-
};
|