specweave 0.28.0 → 0.28.3
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/README.md +62 -74
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +6 -2
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +28 -8
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts +21 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +166 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -0
- package/dist/src/core/repo-structure/platform-registry.d.ts.map +1 -1
- package/dist/src/core/repo-structure/platform-registry.js +3 -2
- package/dist/src/core/repo-structure/platform-registry.js.map +1 -1
- package/dist/src/core/repo-structure/prompt-consolidator.d.ts.map +1 -1
- package/dist/src/core/repo-structure/prompt-consolidator.js +4 -2
- package/dist/src/core/repo-structure/prompt-consolidator.js.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +23 -43
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/dist/src/core/sync/label-detector.d.ts.map +1 -1
- package/dist/src/core/sync/label-detector.js +22 -9
- package/dist/src/core/sync/label-detector.js.map +1 -1
- package/dist/src/metrics/calculators/deployment-frequency.d.ts +12 -8
- package/dist/src/metrics/calculators/deployment-frequency.d.ts.map +1 -1
- package/dist/src/metrics/calculators/deployment-frequency.js +16 -12
- package/dist/src/metrics/calculators/deployment-frequency.js.map +1 -1
- package/dist/src/metrics/dora-calculator.d.ts +2 -1
- package/dist/src/metrics/dora-calculator.d.ts.map +1 -1
- package/dist/src/metrics/dora-calculator.js +9 -4
- package/dist/src/metrics/dora-calculator.js.map +1 -1
- package/dist/src/metrics/github-client.d.ts +12 -0
- package/dist/src/metrics/github-client.d.ts.map +1 -1
- package/dist/src/metrics/github-client.js +30 -0
- package/dist/src/metrics/github-client.js.map +1 -1
- package/dist/src/sync/sync-coordinator.d.ts +33 -0
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +203 -2
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/dist/src/utils/auth-helpers.d.ts.map +1 -1
- package/dist/src/utils/auth-helpers.js +13 -1
- package/dist/src/utils/auth-helpers.js.map +1 -1
- package/dist/src/utils/env-file-generator.d.ts +8 -23
- package/dist/src/utils/env-file-generator.d.ts.map +1 -1
- package/dist/src/utils/env-file-generator.js +31 -71
- package/dist/src/utils/env-file-generator.js.map +1 -1
- package/package.json +7 -2
- package/plugins/specweave/agents/architect/AGENT.md +2 -2
- package/plugins/specweave/agents/docs-writer/AGENT.md +2 -2
- package/plugins/specweave/agents/pm/AGENT.md +2 -2
- package/plugins/specweave/agents/qa-lead/AGENT.md +2 -2
- package/plugins/specweave/agents/security/AGENT.md +2 -2
- package/plugins/specweave/agents/tdd-orchestrator/AGENT.md +2 -2
- package/plugins/specweave/agents/tech-lead/AGENT.md +2 -2
- package/plugins/specweave/agents/test-aware-planner/AGENT.md +2 -2
- package/plugins/specweave/hooks/hooks.json +10 -0
- package/plugins/specweave/hooks/post-increment-completion.sh +84 -0
- package/plugins/specweave/hooks/post-increment-planning.sh +114 -7
- package/plugins/specweave/hooks/post-metadata-change.sh +18 -0
- package/plugins/specweave/lib/hooks/sync-increment-closure.js +66 -0
- package/plugins/specweave/lib/hooks/sync-increment-closure.ts +111 -0
- package/plugins/specweave-github/lib/github-client-v2.js +32 -8
- package/plugins/specweave-github/lib/github-client-v2.ts +31 -9
- package/plugins/specweave-github/lib/github-feature-sync-cli.js +135 -0
- package/plugins/specweave-github/lib/github-feature-sync-cli.ts +194 -0
- package/plugins/specweave-github/skills/github-issue-standard/SKILL.md +43 -0
- package/plugins/specweave-infrastructure/agents/devops/AGENT.md +2 -2
- package/src/templates/.env.example +9 -26
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { SyncCoordinator } from "../../../../dist/src/sync/sync-coordinator.js";
|
|
3
|
+
import { consoleLogger } from "../vendor/utils/logger.js";
|
|
4
|
+
async function syncIncrementClosure(incrementId) {
|
|
5
|
+
try {
|
|
6
|
+
console.log("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
7
|
+
console.log(`\u{1F512} SYNC INCREMENT CLOSURE: ${incrementId}`);
|
|
8
|
+
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
9
|
+
const projectRoot = process.cwd();
|
|
10
|
+
const coordinator = new SyncCoordinator({
|
|
11
|
+
projectRoot,
|
|
12
|
+
incrementId,
|
|
13
|
+
logger: consoleLogger
|
|
14
|
+
});
|
|
15
|
+
const result = await coordinator.syncIncrementClosure();
|
|
16
|
+
console.log("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
17
|
+
if (result.success) {
|
|
18
|
+
console.log("\u2705 INCREMENT CLOSURE SYNC COMPLETED");
|
|
19
|
+
console.log(` Issues closed: ${result.closedIssues.length}`);
|
|
20
|
+
if (result.closedIssues.length > 0) {
|
|
21
|
+
console.log(` Issue numbers: ${result.closedIssues.join(", ")}`);
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
console.log("\u26A0\uFE0F INCREMENT CLOSURE SYNC HAD ERRORS");
|
|
25
|
+
console.log(` Issues closed: ${result.closedIssues.length}`);
|
|
26
|
+
console.log(` Errors: ${result.errors.length}`);
|
|
27
|
+
}
|
|
28
|
+
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
29
|
+
return {
|
|
30
|
+
success: result.success,
|
|
31
|
+
closedIssues: result.closedIssues,
|
|
32
|
+
errors: result.errors
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("\n\u274C FATAL ERROR in sync increment closure:", error.message);
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
closedIssues: [],
|
|
39
|
+
errors: [error.message]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
44
|
+
if (isMainModule) {
|
|
45
|
+
const incrementId = process.argv[2];
|
|
46
|
+
if (!incrementId) {
|
|
47
|
+
console.error("Usage: node sync-increment-closure.js <increment-id>");
|
|
48
|
+
console.error("Example: node sync-increment-closure.js 0059-progressive-plugin");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
syncIncrementClosure(incrementId).then((result) => {
|
|
52
|
+
if (result.success) {
|
|
53
|
+
console.log("\n\u2705 Increment closure sync completed successfully");
|
|
54
|
+
process.exit(0);
|
|
55
|
+
} else {
|
|
56
|
+
console.error("\n\u26A0\uFE0F Increment closure sync had errors (non-blocking)");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
}).catch((error) => {
|
|
60
|
+
console.error("\n\u274C Fatal error:", error);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
syncIncrementClosure
|
|
66
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sync Increment Closure Hook (NEW in v0.28.1)
|
|
4
|
+
*
|
|
5
|
+
* This script handles the CLOSURE flow for increments - closing GitHub issues
|
|
6
|
+
* when an increment is marked as "completed".
|
|
7
|
+
*
|
|
8
|
+
* CRITICAL FIX: Previously, User Story GitHub issues were CREATED but NEVER CLOSED.
|
|
9
|
+
* This script ensures proper closure of all related issues.
|
|
10
|
+
*
|
|
11
|
+
* Called by: post-increment-completion.sh (after increment status → "completed")
|
|
12
|
+
*
|
|
13
|
+
* Flow:
|
|
14
|
+
* 1. Load SyncCoordinator for the increment
|
|
15
|
+
* 2. Call syncIncrementClosure() which:
|
|
16
|
+
* - Ensures all GitHub issues exist (idempotent)
|
|
17
|
+
* - Closes all User Story issues with completion comment
|
|
18
|
+
* 3. Report results
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node sync-increment-closure.js <increment-id>
|
|
22
|
+
*
|
|
23
|
+
* Environment:
|
|
24
|
+
* GITHUB_TOKEN - Required for GitHub API access
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { SyncCoordinator } from '../../../../dist/src/sync/sync-coordinator.js';
|
|
28
|
+
import { consoleLogger } from '../vendor/utils/logger.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Main function to sync increment closure
|
|
32
|
+
*/
|
|
33
|
+
async function syncIncrementClosure(incrementId: string): Promise<{
|
|
34
|
+
success: boolean;
|
|
35
|
+
closedIssues: number[];
|
|
36
|
+
errors: string[];
|
|
37
|
+
}> {
|
|
38
|
+
try {
|
|
39
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
40
|
+
console.log(`🔒 SYNC INCREMENT CLOSURE: ${incrementId}`);
|
|
41
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
42
|
+
|
|
43
|
+
const projectRoot = process.cwd();
|
|
44
|
+
|
|
45
|
+
const coordinator = new SyncCoordinator({
|
|
46
|
+
projectRoot,
|
|
47
|
+
incrementId,
|
|
48
|
+
logger: consoleLogger
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const result = await coordinator.syncIncrementClosure();
|
|
52
|
+
|
|
53
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
54
|
+
if (result.success) {
|
|
55
|
+
console.log('✅ INCREMENT CLOSURE SYNC COMPLETED');
|
|
56
|
+
console.log(` Issues closed: ${result.closedIssues.length}`);
|
|
57
|
+
if (result.closedIssues.length > 0) {
|
|
58
|
+
console.log(` Issue numbers: ${result.closedIssues.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
console.log('⚠️ INCREMENT CLOSURE SYNC HAD ERRORS');
|
|
62
|
+
console.log(` Issues closed: ${result.closedIssues.length}`);
|
|
63
|
+
console.log(` Errors: ${result.errors.length}`);
|
|
64
|
+
}
|
|
65
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
success: result.success,
|
|
69
|
+
closedIssues: result.closedIssues,
|
|
70
|
+
errors: result.errors
|
|
71
|
+
};
|
|
72
|
+
} catch (error: any) {
|
|
73
|
+
console.error('\n❌ FATAL ERROR in sync increment closure:', error.message);
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
closedIssues: [],
|
|
77
|
+
errors: [error.message]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// CLI Interface
|
|
83
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
84
|
+
if (isMainModule) {
|
|
85
|
+
const incrementId = process.argv[2];
|
|
86
|
+
|
|
87
|
+
if (!incrementId) {
|
|
88
|
+
console.error('Usage: node sync-increment-closure.js <increment-id>');
|
|
89
|
+
console.error('Example: node sync-increment-closure.js 0059-progressive-plugin');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
syncIncrementClosure(incrementId)
|
|
94
|
+
.then((result) => {
|
|
95
|
+
if (result.success) {
|
|
96
|
+
console.log('\n✅ Increment closure sync completed successfully');
|
|
97
|
+
process.exit(0);
|
|
98
|
+
} else {
|
|
99
|
+
console.error('\n⚠️ Increment closure sync had errors (non-blocking)');
|
|
100
|
+
// Exit 0 to not block the hook chain
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
.catch((error) => {
|
|
105
|
+
console.error('\n❌ Fatal error:', error);
|
|
106
|
+
// Exit 0 to not block the hook chain
|
|
107
|
+
process.exit(0);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { syncIncrementClosure };
|
|
@@ -138,27 +138,51 @@ class GitHubClientV2 {
|
|
|
138
138
|
* Validate issue title format
|
|
139
139
|
*
|
|
140
140
|
* CRITICAL: Enforces correct data flow architecture
|
|
141
|
-
* - ✅ CORRECT: "US-
|
|
141
|
+
* - ✅ CORRECT: "[FS-XXX][US-YYY] Title" (User Story - STANDARD)
|
|
142
|
+
* - ✅ CORRECT: "[FS-XXX] Title" (Feature-level, rare)
|
|
142
143
|
* - ❌ WRONG: "[Increment XXXX] Title" (deprecated old format)
|
|
144
|
+
* - ❌ WRONG: "[BUG] Title" (type prefixes are labels, not title)
|
|
145
|
+
* - ❌ WRONG: "[HOTFIX] Title" (type prefixes are labels, not title)
|
|
146
|
+
* - ❌ WRONG: "[FEATURE] Title" (type prefixes are labels, not title)
|
|
143
147
|
*
|
|
144
|
-
* @throws Error if title uses
|
|
148
|
+
* @throws Error if title uses invalid format
|
|
145
149
|
*/
|
|
146
150
|
validateIssueTitle(title) {
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
151
|
+
const deprecatedIncrementPattern = /\[Increment\s+\d+\]/i;
|
|
152
|
+
if (deprecatedIncrementPattern.test(title)) {
|
|
149
153
|
throw new Error(
|
|
150
154
|
`\u274C DEPRECATED FORMAT DETECTED: "${title}"
|
|
151
155
|
|
|
152
156
|
GitHub issues MUST use living docs format:
|
|
153
|
-
\u2705 CORRECT: "US-
|
|
154
|
-
\u2705 CORRECT: "FS-YY-MM-DD: Title" (Feature Spec)
|
|
157
|
+
\u2705 CORRECT: "[FS-XXX][US-YYY] Title" (User Story)
|
|
155
158
|
\u274C WRONG: "[Increment XXXX] Title" (old format)
|
|
156
159
|
|
|
157
160
|
WHY: Correct data flow is: Increment \u2192 Living Docs \u2192 GitHub
|
|
158
161
|
Living docs are the source of truth for GitHub sync.
|
|
159
162
|
|
|
160
|
-
FIX: Use /specweave:sync-docs to generate living docs, then sync to GitHub
|
|
161
|
-
|
|
163
|
+
FIX: Use /specweave:sync-docs to generate living docs, then sync to GitHub.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const typePrefixPattern = /^\[(BUG|HOTFIX|FEATURE|DOCS|REFACTOR|CHORE|EXPERIMENT|Bug|Hotfix|Feature|Docs|Refactor|Chore|Experiment)\]/i;
|
|
167
|
+
if (typePrefixPattern.test(title)) {
|
|
168
|
+
const match = title.match(typePrefixPattern);
|
|
169
|
+
const badPrefix = match ? match[0] : "[TYPE]";
|
|
170
|
+
throw new Error(
|
|
171
|
+
`\u274C INVALID TITLE FORMAT: "${title}"
|
|
172
|
+
|
|
173
|
+
Type prefixes like ${badPrefix} belong as LABELS, not in the title!
|
|
174
|
+
|
|
175
|
+
GitHub issues MUST use this format:
|
|
176
|
+
\u2705 CORRECT: "[FS-XXX][US-YYY] Title" (User Story)
|
|
177
|
+
\u274C WRONG: "${badPrefix} Title" (use 'bug' label instead)
|
|
178
|
+
|
|
179
|
+
WHY: All SpecWeave issues follow [FS-XXX][US-YYY] format for traceability.
|
|
180
|
+
Use GitHub labels for categorization (bug, enhancement, etc.).
|
|
181
|
+
|
|
182
|
+
FIX:
|
|
183
|
+
1. Link this work to a Feature (FS-XXX) and User Story (US-YYY)
|
|
184
|
+
2. Use /specweave-github:sync to create issue with correct format
|
|
185
|
+
3. Add '${match ? match[1].toLowerCase() : "bug"}' as a label instead`
|
|
162
186
|
);
|
|
163
187
|
}
|
|
164
188
|
}
|
|
@@ -196,26 +196,48 @@ export class GitHubClientV2 {
|
|
|
196
196
|
* Validate issue title format
|
|
197
197
|
*
|
|
198
198
|
* CRITICAL: Enforces correct data flow architecture
|
|
199
|
-
* - ✅ CORRECT: "US-
|
|
199
|
+
* - ✅ CORRECT: "[FS-XXX][US-YYY] Title" (User Story - STANDARD)
|
|
200
|
+
* - ✅ CORRECT: "[FS-XXX] Title" (Feature-level, rare)
|
|
200
201
|
* - ❌ WRONG: "[Increment XXXX] Title" (deprecated old format)
|
|
202
|
+
* - ❌ WRONG: "[BUG] Title" (type prefixes are labels, not title)
|
|
203
|
+
* - ❌ WRONG: "[HOTFIX] Title" (type prefixes are labels, not title)
|
|
204
|
+
* - ❌ WRONG: "[FEATURE] Title" (type prefixes are labels, not title)
|
|
201
205
|
*
|
|
202
|
-
* @throws Error if title uses
|
|
206
|
+
* @throws Error if title uses invalid format
|
|
203
207
|
*/
|
|
204
208
|
private validateIssueTitle(title: string): void {
|
|
205
209
|
// Check for deprecated [Increment XXXX] format
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
if (deprecatedPattern.test(title)) {
|
|
210
|
+
const deprecatedIncrementPattern = /\[Increment\s+\d+\]/i;
|
|
211
|
+
if (deprecatedIncrementPattern.test(title)) {
|
|
209
212
|
throw new Error(
|
|
210
213
|
`❌ DEPRECATED FORMAT DETECTED: "${title}"\n\n` +
|
|
211
214
|
`GitHub issues MUST use living docs format:\n` +
|
|
212
|
-
` ✅ CORRECT: "US-
|
|
213
|
-
` ✅ CORRECT: "FS-YY-MM-DD: Title" (Feature Spec)\n` +
|
|
215
|
+
` ✅ CORRECT: "[FS-XXX][US-YYY] Title" (User Story)\n` +
|
|
214
216
|
` ❌ WRONG: "[Increment XXXX] Title" (old format)\n\n` +
|
|
215
217
|
`WHY: Correct data flow is: Increment → Living Docs → GitHub\n` +
|
|
216
218
|
` Living docs are the source of truth for GitHub sync.\n\n` +
|
|
217
|
-
`FIX: Use /specweave:sync-docs to generate living docs, then sync to GitHub
|
|
218
|
-
|
|
219
|
+
`FIX: Use /specweave:sync-docs to generate living docs, then sync to GitHub.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check for type-based prefixes (CRITICAL FIX for issue #749)
|
|
224
|
+
// These prefixes belong as LABELS, not in the issue title!
|
|
225
|
+
const typePrefixPattern = /^\[(BUG|HOTFIX|FEATURE|DOCS|REFACTOR|CHORE|EXPERIMENT|Bug|Hotfix|Feature|Docs|Refactor|Chore|Experiment)\]/i;
|
|
226
|
+
if (typePrefixPattern.test(title)) {
|
|
227
|
+
const match = title.match(typePrefixPattern);
|
|
228
|
+
const badPrefix = match ? match[0] : '[TYPE]';
|
|
229
|
+
throw new Error(
|
|
230
|
+
`❌ INVALID TITLE FORMAT: "${title}"\n\n` +
|
|
231
|
+
`Type prefixes like ${badPrefix} belong as LABELS, not in the title!\n\n` +
|
|
232
|
+
`GitHub issues MUST use this format:\n` +
|
|
233
|
+
` ✅ CORRECT: "[FS-XXX][US-YYY] Title" (User Story)\n` +
|
|
234
|
+
` ❌ WRONG: "${badPrefix} Title" (use 'bug' label instead)\n\n` +
|
|
235
|
+
`WHY: All SpecWeave issues follow [FS-XXX][US-YYY] format for traceability.\n` +
|
|
236
|
+
` Use GitHub labels for categorization (bug, enhancement, etc.).\n\n` +
|
|
237
|
+
`FIX:\n` +
|
|
238
|
+
` 1. Link this work to a Feature (FS-XXX) and User Story (US-YYY)\n` +
|
|
239
|
+
` 2. Use /specweave-github:sync to create issue with correct format\n` +
|
|
240
|
+
` 3. Add '${match ? match[1].toLowerCase() : 'bug'}' as a label instead`
|
|
219
241
|
);
|
|
220
242
|
}
|
|
221
243
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { GitHubFeatureSync } from "./github-feature-sync.js";
|
|
5
|
+
import { GitHubClientV2 } from "./github-client-v2.js";
|
|
6
|
+
async function loadGitHubConfig() {
|
|
7
|
+
const projectRoot = process.cwd();
|
|
8
|
+
const configPath = path.join(projectRoot, ".specweave/config.json");
|
|
9
|
+
let owner = process.env.GITHUB_OWNER || "";
|
|
10
|
+
let repo = process.env.GITHUB_REPO || "";
|
|
11
|
+
const token = process.env.GITHUB_TOKEN || "";
|
|
12
|
+
if (existsSync(configPath)) {
|
|
13
|
+
try {
|
|
14
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
15
|
+
if (config.sync?.github?.owner && config.sync?.github?.repo) {
|
|
16
|
+
owner = config.sync.github.owner;
|
|
17
|
+
repo = config.sync.github.repo;
|
|
18
|
+
} else if (config.multiProject?.enabled && config.multiProject?.activeProject) {
|
|
19
|
+
const activeProject = config.multiProject.activeProject;
|
|
20
|
+
const projectConfig = config.multiProject.projects?.[activeProject];
|
|
21
|
+
if (projectConfig?.externalTools?.github?.repository) {
|
|
22
|
+
const parts = projectConfig.externalTools.github.repository.split("/");
|
|
23
|
+
if (parts.length === 2) {
|
|
24
|
+
owner = parts[0];
|
|
25
|
+
repo = parts[1];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else if (config.sync?.activeProfile && config.sync?.profiles) {
|
|
29
|
+
const profile = config.sync.profiles[config.sync.activeProfile];
|
|
30
|
+
if (profile?.config?.owner && profile?.config?.repo) {
|
|
31
|
+
owner = profile.config.owner;
|
|
32
|
+
repo = profile.config.repo;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("\u26A0\uFE0F Failed to parse config.json:", error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!owner || !repo) {
|
|
40
|
+
try {
|
|
41
|
+
const { execSync } = await import("child_process");
|
|
42
|
+
const remoteUrl = execSync("git remote get-url origin 2>/dev/null", {
|
|
43
|
+
encoding: "utf-8",
|
|
44
|
+
cwd: projectRoot
|
|
45
|
+
}).trim();
|
|
46
|
+
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
47
|
+
if (match) {
|
|
48
|
+
owner = owner || match[1];
|
|
49
|
+
repo = repo || match[2];
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!token) {
|
|
55
|
+
console.error("\u274C GITHUB_TOKEN not set");
|
|
56
|
+
console.error(" Set it in .env file or export GITHUB_TOKEN=ghp_xxx");
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (!owner || !repo) {
|
|
60
|
+
console.error("\u274C Could not detect GitHub owner/repo");
|
|
61
|
+
console.error(" Set sync.github.owner and sync.github.repo in .specweave/config.json");
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return { owner, repo, token };
|
|
65
|
+
}
|
|
66
|
+
async function main() {
|
|
67
|
+
const args = process.argv.slice(2);
|
|
68
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
69
|
+
console.log("Usage: node github-feature-sync-cli.js <feature-id>");
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log("Arguments:");
|
|
72
|
+
console.log(" feature-id Feature ID (e.g., FS-062)");
|
|
73
|
+
console.log("");
|
|
74
|
+
console.log("Environment:");
|
|
75
|
+
console.log(" GITHUB_TOKEN Required - GitHub personal access token");
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log("Example:");
|
|
78
|
+
console.log(" GITHUB_TOKEN=ghp_xxx node github-feature-sync-cli.js FS-062");
|
|
79
|
+
process.exit(args.length === 0 ? 1 : 0);
|
|
80
|
+
}
|
|
81
|
+
const featureId = args[0];
|
|
82
|
+
if (!featureId.match(/^FS-\d+$/i)) {
|
|
83
|
+
console.error(`\u274C Invalid feature ID: ${featureId}`);
|
|
84
|
+
console.error(" Expected format: FS-XXX (e.g., FS-062)");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log(`
|
|
88
|
+
\u{1F419} GitHub Feature Sync CLI`);
|
|
89
|
+
console.log(` Feature: ${featureId}`);
|
|
90
|
+
const config = await loadGitHubConfig();
|
|
91
|
+
if (!config) {
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
console.log(` Repository: ${config.owner}/${config.repo}`);
|
|
95
|
+
const projectRoot = process.cwd();
|
|
96
|
+
const specsDir = path.join(projectRoot, ".specweave/docs/internal/specs");
|
|
97
|
+
const profile = {
|
|
98
|
+
provider: "github",
|
|
99
|
+
displayName: "GitHub",
|
|
100
|
+
config: {
|
|
101
|
+
owner: config.owner,
|
|
102
|
+
repo: config.repo,
|
|
103
|
+
token: config.token
|
|
104
|
+
},
|
|
105
|
+
timeRange: {
|
|
106
|
+
default: "1M",
|
|
107
|
+
max: "3M"
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const client = new GitHubClientV2(profile);
|
|
111
|
+
const sync = new GitHubFeatureSync(client, specsDir, projectRoot);
|
|
112
|
+
try {
|
|
113
|
+
console.log(`
|
|
114
|
+
\u{1F504} Syncing ${featureId} to GitHub...`);
|
|
115
|
+
const result = await sync.syncFeatureToGitHub(featureId);
|
|
116
|
+
console.log(`
|
|
117
|
+
\u2705 Sync complete!`);
|
|
118
|
+
console.log(` \u{1F3AF} Milestone: #${result.milestoneNumber}`);
|
|
119
|
+
console.log(` \u{1F4DD} Issues created: ${result.issuesCreated}`);
|
|
120
|
+
console.log(` \u{1F504} Issues updated: ${result.issuesUpdated}`);
|
|
121
|
+
console.log(` \u{1F4DA} User stories processed: ${result.userStoriesProcessed}`);
|
|
122
|
+
if (result.milestoneUrl) {
|
|
123
|
+
console.log(` \u{1F517} ${result.milestoneUrl}`);
|
|
124
|
+
}
|
|
125
|
+
process.exit(0);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`
|
|
128
|
+
\u274C Sync failed:`, error);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
main().catch((error) => {
|
|
133
|
+
console.error("Fatal error:", error);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Feature Sync CLI
|
|
4
|
+
*
|
|
5
|
+
* CLI wrapper for GitHubFeatureSync.syncFeatureToGitHub()
|
|
6
|
+
* Called by post-increment-planning.sh hook to create GitHub issues
|
|
7
|
+
* after increment creation.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node github-feature-sync-cli.js <feature-id>
|
|
11
|
+
* node github-feature-sync-cli.js FS-062
|
|
12
|
+
*
|
|
13
|
+
* Environment:
|
|
14
|
+
* GITHUB_TOKEN - Required
|
|
15
|
+
* GITHUB_OWNER - Optional (detected from config.json or git remote)
|
|
16
|
+
* GITHUB_REPO - Optional (detected from config.json or git remote)
|
|
17
|
+
*
|
|
18
|
+
* @see ADR-0139 (Unified Post-Increment Sync)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { GitHubFeatureSync } from './github-feature-sync.js';
|
|
24
|
+
import { GitHubClientV2 } from './github-client-v2.js';
|
|
25
|
+
|
|
26
|
+
interface GitHubConfig {
|
|
27
|
+
owner: string;
|
|
28
|
+
repo: string;
|
|
29
|
+
token: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadGitHubConfig(): Promise<GitHubConfig | null> {
|
|
33
|
+
const projectRoot = process.cwd();
|
|
34
|
+
const configPath = path.join(projectRoot, '.specweave/config.json');
|
|
35
|
+
|
|
36
|
+
let owner = process.env.GITHUB_OWNER || '';
|
|
37
|
+
let repo = process.env.GITHUB_REPO || '';
|
|
38
|
+
const token = process.env.GITHUB_TOKEN || '';
|
|
39
|
+
|
|
40
|
+
// Try to load from config.json
|
|
41
|
+
if (existsSync(configPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
44
|
+
|
|
45
|
+
// Method 1: sync.github
|
|
46
|
+
if (config.sync?.github?.owner && config.sync?.github?.repo) {
|
|
47
|
+
owner = config.sync.github.owner;
|
|
48
|
+
repo = config.sync.github.repo;
|
|
49
|
+
}
|
|
50
|
+
// Method 2: multiProject.projects[activeProject].externalTools.github
|
|
51
|
+
else if (config.multiProject?.enabled && config.multiProject?.activeProject) {
|
|
52
|
+
const activeProject = config.multiProject.activeProject;
|
|
53
|
+
const projectConfig = config.multiProject.projects?.[activeProject];
|
|
54
|
+
if (projectConfig?.externalTools?.github?.repository) {
|
|
55
|
+
const parts = projectConfig.externalTools.github.repository.split('/');
|
|
56
|
+
if (parts.length === 2) {
|
|
57
|
+
owner = parts[0];
|
|
58
|
+
repo = parts[1];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Method 3: sync.profiles[activeProfile]
|
|
63
|
+
else if (config.sync?.activeProfile && config.sync?.profiles) {
|
|
64
|
+
const profile = config.sync.profiles[config.sync.activeProfile];
|
|
65
|
+
if (profile?.config?.owner && profile?.config?.repo) {
|
|
66
|
+
owner = profile.config.owner;
|
|
67
|
+
repo = profile.config.repo;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('⚠️ Failed to parse config.json:', error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fallback: detect from git remote
|
|
76
|
+
if (!owner || !repo) {
|
|
77
|
+
try {
|
|
78
|
+
const { execSync } = await import('child_process');
|
|
79
|
+
const remoteUrl = execSync('git remote get-url origin 2>/dev/null', {
|
|
80
|
+
encoding: 'utf-8',
|
|
81
|
+
cwd: projectRoot
|
|
82
|
+
}).trim();
|
|
83
|
+
|
|
84
|
+
// Parse GitHub URL (HTTPS or SSH)
|
|
85
|
+
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
86
|
+
if (match) {
|
|
87
|
+
owner = owner || match[1];
|
|
88
|
+
repo = repo || match[2];
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Git detection failed, continue with what we have
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!token) {
|
|
96
|
+
console.error('❌ GITHUB_TOKEN not set');
|
|
97
|
+
console.error(' Set it in .env file or export GITHUB_TOKEN=ghp_xxx');
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!owner || !repo) {
|
|
102
|
+
console.error('❌ Could not detect GitHub owner/repo');
|
|
103
|
+
console.error(' Set sync.github.owner and sync.github.repo in .specweave/config.json');
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { owner, repo, token };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
const args = process.argv.slice(2);
|
|
112
|
+
|
|
113
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
114
|
+
console.log('Usage: node github-feature-sync-cli.js <feature-id>');
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log('Arguments:');
|
|
117
|
+
console.log(' feature-id Feature ID (e.g., FS-062)');
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log('Environment:');
|
|
120
|
+
console.log(' GITHUB_TOKEN Required - GitHub personal access token');
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log('Example:');
|
|
123
|
+
console.log(' GITHUB_TOKEN=ghp_xxx node github-feature-sync-cli.js FS-062');
|
|
124
|
+
process.exit(args.length === 0 ? 1 : 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const featureId = args[0];
|
|
128
|
+
|
|
129
|
+
// Validate feature ID format
|
|
130
|
+
if (!featureId.match(/^FS-\d+$/i)) {
|
|
131
|
+
console.error(`❌ Invalid feature ID: ${featureId}`);
|
|
132
|
+
console.error(' Expected format: FS-XXX (e.g., FS-062)');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(`\n🐙 GitHub Feature Sync CLI`);
|
|
137
|
+
console.log(` Feature: ${featureId}`);
|
|
138
|
+
|
|
139
|
+
// Load config
|
|
140
|
+
const config = await loadGitHubConfig();
|
|
141
|
+
if (!config) {
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(` Repository: ${config.owner}/${config.repo}`);
|
|
146
|
+
|
|
147
|
+
// Create client and sync
|
|
148
|
+
const projectRoot = process.cwd();
|
|
149
|
+
const specsDir = path.join(projectRoot, '.specweave/docs/internal/specs');
|
|
150
|
+
|
|
151
|
+
const profile = {
|
|
152
|
+
provider: 'github' as const,
|
|
153
|
+
displayName: 'GitHub',
|
|
154
|
+
config: {
|
|
155
|
+
owner: config.owner,
|
|
156
|
+
repo: config.repo,
|
|
157
|
+
token: config.token
|
|
158
|
+
},
|
|
159
|
+
timeRange: {
|
|
160
|
+
default: '1M' as const,
|
|
161
|
+
max: '3M' as const
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const client = new GitHubClientV2(profile);
|
|
166
|
+
const sync = new GitHubFeatureSync(client, specsDir, projectRoot);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
console.log(`\n🔄 Syncing ${featureId} to GitHub...`);
|
|
170
|
+
|
|
171
|
+
const result = await sync.syncFeatureToGitHub(featureId);
|
|
172
|
+
|
|
173
|
+
console.log(`\n✅ Sync complete!`);
|
|
174
|
+
console.log(` 🎯 Milestone: #${result.milestoneNumber}`);
|
|
175
|
+
console.log(` 📝 Issues created: ${result.issuesCreated}`);
|
|
176
|
+
console.log(` 🔄 Issues updated: ${result.issuesUpdated}`);
|
|
177
|
+
console.log(` 📚 User stories processed: ${result.userStoriesProcessed}`);
|
|
178
|
+
|
|
179
|
+
if (result.milestoneUrl) {
|
|
180
|
+
console.log(` 🔗 ${result.milestoneUrl}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
process.exit(0);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(`\n❌ Sync failed:`, error);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Run CLI
|
|
191
|
+
main().catch(error => {
|
|
192
|
+
console.error('Fatal error:', error);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|
|
@@ -11,6 +11,49 @@ description: Standard format for ALL GitHub issues created by SpecWeave. Ensures
|
|
|
11
11
|
- Increments (0001-* folders)
|
|
12
12
|
- Specs (spec-*.md files)
|
|
13
13
|
|
|
14
|
+
## Issue Title Format (MANDATORY)
|
|
15
|
+
|
|
16
|
+
### ✅ ONLY Allowed Title Formats
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
[FS-XXX][US-YYY] User Story Title ← STANDARD (User Stories)
|
|
20
|
+
[FS-XXX] Feature Title ← Rare (Feature-level only)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Examples**:
|
|
24
|
+
- ✅ `[FS-059][US-003] Hook Optimization (P0)`
|
|
25
|
+
- ✅ `[FS-054][US-001] Fix Reopen Desync Bug (P0)`
|
|
26
|
+
- ✅ `[FS-048] Smart Pagination Feature`
|
|
27
|
+
|
|
28
|
+
### ❌ PROHIBITED Title Formats (NEVER USE)
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
[BUG] Title ← WRONG! Bug is a LABEL, not title prefix
|
|
32
|
+
[HOTFIX] Title ← WRONG! Hotfix is a LABEL
|
|
33
|
+
[FEATURE] Title ← WRONG! Feature is a LABEL
|
|
34
|
+
[DOCS] Title ← WRONG! Docs is a LABEL
|
|
35
|
+
[Increment XXXX] Title ← DEPRECATED! Old format
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Why?** Type-based prefixes like `[BUG]` break traceability:
|
|
39
|
+
- Cannot link to Feature Spec (FS-XXX)
|
|
40
|
+
- Cannot link to User Story (US-YYY)
|
|
41
|
+
- Violates SpecWeave's data flow: `Increment → Living Docs → GitHub`
|
|
42
|
+
|
|
43
|
+
**What to do instead?**
|
|
44
|
+
1. Link work to a Feature (FS-XXX) in living docs
|
|
45
|
+
2. Create User Story (US-YYY) under that feature
|
|
46
|
+
3. Use GitHub **labels** for categorization: `bug`, `enhancement`, `hotfix`
|
|
47
|
+
|
|
48
|
+
### Validation
|
|
49
|
+
|
|
50
|
+
The GitHub client (`github-client-v2.ts`) enforces this:
|
|
51
|
+
- Rejects titles starting with `[BUG]`, `[HOTFIX]`, `[FEATURE]`, etc.
|
|
52
|
+
- Rejects deprecated `[Increment XXXX]` format
|
|
53
|
+
- Only allows `[FS-XXX][US-YYY]` or `[FS-XXX]` formats
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
14
57
|
## The Standard Format
|
|
15
58
|
|
|
16
59
|
### ✅ Required Elements
|