specweave 1.0.356 → 1.0.357
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 +21 -1
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/duplicate-detector.js +18 -2
- package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +10 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +17 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +85 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
- package/dist/src/adapters/agents-md-generator.js +1 -1
- package/dist/src/adapters/agents-md-generator.js.map +1 -1
- package/dist/src/adapters/claude-md-generator.js +2 -2
- package/dist/src/adapters/claude-md-generator.js.map +1 -1
- package/dist/src/cli/commands/check-discipline.d.ts.map +1 -1
- package/dist/src/cli/commands/check-discipline.js +2 -1
- package/dist/src/cli/commands/check-discipline.js.map +1 -1
- package/dist/src/cli/commands/create-increment.d.ts.map +1 -1
- package/dist/src/cli/commands/create-increment.js +4 -1
- package/dist/src/cli/commands/create-increment.js.map +1 -1
- package/dist/src/cli/commands/health.d.ts +34 -0
- package/dist/src/cli/commands/health.d.ts.map +1 -0
- package/dist/src/cli/commands/health.js +349 -0
- package/dist/src/cli/commands/health.js.map +1 -0
- package/dist/src/cli/commands/migrate-to-umbrella.d.ts.map +1 -1
- package/dist/src/cli/commands/migrate-to-umbrella.js +38 -0
- package/dist/src/cli/commands/migrate-to-umbrella.js.map +1 -1
- package/dist/src/cli/commands/save.js +1 -1
- package/dist/src/cli/commands/save.js.map +1 -1
- package/dist/src/cli/commands/sync-living-docs.d.ts.map +1 -1
- package/dist/src/cli/commands/sync-living-docs.js +2 -1
- package/dist/src/cli/commands/sync-living-docs.js.map +1 -1
- package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
- package/dist/src/cli/commands/sync-progress.js +208 -8
- package/dist/src/cli/commands/sync-progress.js.map +1 -1
- package/dist/src/core/ac-progress-sync.d.ts.map +1 -1
- package/dist/src/core/ac-progress-sync.js +13 -4
- package/dist/src/core/ac-progress-sync.js.map +1 -1
- package/dist/src/core/config/types.d.ts +33 -0
- package/dist/src/core/config/types.d.ts.map +1 -1
- package/dist/src/core/config/types.js.map +1 -1
- package/dist/src/core/doctor/checkers/increments-checker.js +2 -2
- package/dist/src/core/doctor/checkers/increments-checker.js.map +1 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts.map +1 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.js +6 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.js.map +1 -1
- package/dist/src/core/increment/completion-validator.d.ts.map +1 -1
- package/dist/src/core/increment/completion-validator.js +8 -7
- package/dist/src/core/increment/completion-validator.js.map +1 -1
- package/dist/src/core/increment/increment-reopener.d.ts.map +1 -1
- package/dist/src/core/increment/increment-reopener.js +5 -4
- package/dist/src/core/increment/increment-reopener.js.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts +5 -6
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +12 -13
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/spec-frontmatter-updater.d.ts.map +1 -1
- package/dist/src/core/increment/spec-frontmatter-updater.js +3 -2
- package/dist/src/core/increment/spec-frontmatter-updater.js.map +1 -1
- package/dist/src/core/increment/status-auto-transition.d.ts.map +1 -1
- package/dist/src/core/increment/status-auto-transition.js +5 -4
- package/dist/src/core/increment/status-auto-transition.js.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.js +3 -2
- package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
- package/dist/src/core/increment/status-commands.d.ts.map +1 -1
- package/dist/src/core/increment/status-commands.js +4 -3
- package/dist/src/core/increment/status-commands.js.map +1 -1
- package/dist/src/core/increment/template-creator.d.ts.map +1 -1
- package/dist/src/core/increment/template-creator.js +49 -5
- package/dist/src/core/increment/template-creator.js.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.js +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +16 -6
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/migration/consolidation-engine.d.ts +59 -0
- package/dist/src/core/migration/consolidation-engine.d.ts.map +1 -0
- package/dist/src/core/migration/consolidation-engine.js +177 -0
- package/dist/src/core/migration/consolidation-engine.js.map +1 -0
- package/dist/src/core/migration/types.d.ts +2 -0
- package/dist/src/core/migration/types.d.ts.map +1 -1
- package/dist/src/core/project/project-resolution.d.ts.map +1 -1
- package/dist/src/core/project/project-resolution.js +20 -2
- package/dist/src/core/project/project-resolution.js.map +1 -1
- package/dist/src/generators/spec/spec-parser.js +2 -2
- package/dist/src/generators/spec/spec-parser.js.map +1 -1
- package/dist/src/integrations/jira/jira-client.d.ts.map +1 -1
- package/dist/src/integrations/jira/jira-client.js +83 -30
- package/dist/src/integrations/jira/jira-client.js.map +1 -1
- package/dist/src/sync/external-issue-auto-creator.d.ts +6 -2
- package/dist/src/sync/external-issue-auto-creator.d.ts.map +1 -1
- package/dist/src/sync/external-issue-auto-creator.js +75 -19
- package/dist/src/sync/external-issue-auto-creator.js.map +1 -1
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +20 -4
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/dist/src/sync/sync-target-resolver.d.ts +36 -0
- package/dist/src/sync/sync-target-resolver.d.ts.map +1 -0
- package/dist/src/sync/sync-target-resolver.js +72 -0
- package/dist/src/sync/sync-target-resolver.js.map +1 -0
- package/dist/src/utils/external-tool-drift-detector.js +4 -4
- package/dist/src/utils/external-tool-drift-detector.js.map +1 -1
- package/dist/src/utils/find-project-root.d.ts +26 -0
- package/dist/src/utils/find-project-root.d.ts.map +1 -1
- package/dist/src/utils/find-project-root.js +78 -0
- package/dist/src/utils/find-project-root.js.map +1 -1
- package/dist/src/utils/multi-project-detector.js +1 -1
- package/dist/src/utils/multi-project-detector.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/PLUGIN.md +1 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +5 -6
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +12 -13
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/lib/vendor/core/increment/status-auto-transition.js +5 -4
- package/plugins/specweave/lib/vendor/core/increment/status-auto-transition.js.map +1 -1
- package/plugins/specweave/skills/brainstorm/SKILL.md +619 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
- package/plugins/specweave-docs/PLUGIN.md +1 -1
- package/plugins/specweave-github/lib/duplicate-detector.js +21 -1
- package/plugins/specweave-github/lib/duplicate-detector.ts +18 -2
- package/plugins/specweave-github/lib/github-feature-sync.js +17 -0
- package/plugins/specweave-github/lib/github-feature-sync.ts +11 -0
- package/plugins/specweave-jira/lib/jira-status-sync.js +75 -0
- package/plugins/specweave-jira/lib/jira-status-sync.ts +91 -1
- package/src/templates/CLAUDE.md.template +3 -1
- package/plugins/specweave/hooks/.specweave/logs/auto-iterations.log +0 -1
- package/plugins/specweave/hooks/.specweave/logs/auto-stop-reasons.log +0 -1
- package/plugins/specweave/skills/.specweave/logs/reflect/auto-reflect.log +0 -15
- package/plugins/specweave/skills/.specweave/logs/reflect/reflect.log +0 -3
- package/plugins/specweave/skills/.specweave/logs/stop-auto.log +0 -1
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +0 -180
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +0 -1266
- package/plugins/specweave-github/lib/enhanced-github-sync.js +0 -249
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +0 -150
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -1260
|
@@ -622,9 +622,26 @@ Created: ${featureData.created}`;
|
|
|
622
622
|
* - Prevents premature closure on creation
|
|
623
623
|
*/
|
|
624
624
|
async createUserStoryIssue(issueContent, milestoneTitle, userStoryPath) {
|
|
625
|
+
const repoSlug = `${this.client.getOwner()}/${this.client.getRepo()}`;
|
|
626
|
+
for (const label of issueContent.labels) {
|
|
627
|
+
await execFileNoThrow("gh", [
|
|
628
|
+
"label",
|
|
629
|
+
"create",
|
|
630
|
+
label,
|
|
631
|
+
"--repo",
|
|
632
|
+
repoSlug,
|
|
633
|
+
"--color",
|
|
634
|
+
"ededed",
|
|
635
|
+
"--description",
|
|
636
|
+
"SpecWeave auto-label",
|
|
637
|
+
"--force"
|
|
638
|
+
], { env: this.getGhEnv() });
|
|
639
|
+
}
|
|
625
640
|
const result = await execFileNoThrow("gh", [
|
|
626
641
|
"issue",
|
|
627
642
|
"create",
|
|
643
|
+
"--repo",
|
|
644
|
+
repoSlug,
|
|
628
645
|
"--title",
|
|
629
646
|
issueContent.title,
|
|
630
647
|
"--body",
|
|
@@ -907,10 +907,21 @@ export class GitHubFeatureSync {
|
|
|
907
907
|
milestoneTitle: string,
|
|
908
908
|
userStoryPath: string
|
|
909
909
|
): Promise<number> {
|
|
910
|
+
// Step 0: Ensure all required labels exist in the target repo
|
|
911
|
+
const repoSlug = `${this.client.getOwner()}/${this.client.getRepo()}`;
|
|
912
|
+
for (const label of issueContent.labels) {
|
|
913
|
+
await execFileNoThrow('gh', [
|
|
914
|
+
'label', 'create', label, '--repo', repoSlug,
|
|
915
|
+
'--color', 'ededed', '--description', 'SpecWeave auto-label', '--force'
|
|
916
|
+
], { env: this.getGhEnv() });
|
|
917
|
+
}
|
|
918
|
+
|
|
910
919
|
// Step 1: Create issue (always open initially - gh CLI limitation)
|
|
911
920
|
const result = await execFileNoThrow('gh', [
|
|
912
921
|
'issue',
|
|
913
922
|
'create',
|
|
923
|
+
'--repo',
|
|
924
|
+
repoSlug,
|
|
914
925
|
'--title',
|
|
915
926
|
issueContent.title,
|
|
916
927
|
'--body',
|
|
@@ -92,6 +92,81 @@ _Synced from SpecWeave_`;
|
|
|
92
92
|
body
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Post AC progress comment with proper ADF formatting and dedup.
|
|
97
|
+
*
|
|
98
|
+
* Builds native ADF with:
|
|
99
|
+
* - Bold header showing completion percentage
|
|
100
|
+
* - Bullet list with checkmark/cross emojis per AC
|
|
101
|
+
* - Fingerprint marker to prevent duplicate comments
|
|
102
|
+
*
|
|
103
|
+
* @param issueKey - JIRA issue key (e.g., PROJ-123)
|
|
104
|
+
* @param acStates - Array of AC states with id, description, completed
|
|
105
|
+
* @returns true if comment was posted, false if skipped (duplicate)
|
|
106
|
+
*/
|
|
107
|
+
async postProgressComment(issueKey, acStates) {
|
|
108
|
+
const total = acStates.length;
|
|
109
|
+
const completed = acStates.filter((ac) => ac.completed).length;
|
|
110
|
+
const percentage = Math.round(completed / total * 100);
|
|
111
|
+
const fingerprint = `sw-progress:${completed}/${total}`;
|
|
112
|
+
try {
|
|
113
|
+
const commentsResp = await this.client.get(`/issue/${issueKey}/comment`, {
|
|
114
|
+
params: { orderBy: "-created", maxResults: 1 }
|
|
115
|
+
});
|
|
116
|
+
const lastComment = commentsResp.data?.comments?.[0];
|
|
117
|
+
if (lastComment) {
|
|
118
|
+
const lastText = extractAdfText(lastComment.body);
|
|
119
|
+
if (lastText.includes(fingerprint)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
const listItems = acStates.map((ac) => ({
|
|
126
|
+
type: "listItem",
|
|
127
|
+
content: [{
|
|
128
|
+
type: "paragraph",
|
|
129
|
+
content: [
|
|
130
|
+
{ type: "text", text: `${ac.completed ? "\u2705" : "\u274C"} ${ac.id}: ${ac.description}` }
|
|
131
|
+
]
|
|
132
|
+
}]
|
|
133
|
+
}));
|
|
134
|
+
const body = {
|
|
135
|
+
type: "doc",
|
|
136
|
+
version: 1,
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "heading",
|
|
140
|
+
attrs: { level: 3 },
|
|
141
|
+
content: [{ type: "text", text: `Progress: ${completed}/${total} ACs (${percentage}%)` }]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: "bulletList",
|
|
145
|
+
content: listItems
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: "paragraph",
|
|
149
|
+
content: [
|
|
150
|
+
{ type: "text", text: `${fingerprint} | Synced from SpecWeave`, marks: [{ type: "em" }] }
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
await this.client.post(`/issue/${issueKey}/comment`, { body });
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function extractAdfText(adf) {
|
|
160
|
+
if (!adf) return "";
|
|
161
|
+
if (typeof adf === "string") return adf;
|
|
162
|
+
let text = "";
|
|
163
|
+
if (adf.text) text += adf.text;
|
|
164
|
+
if (Array.isArray(adf.content)) {
|
|
165
|
+
for (const child of adf.content) {
|
|
166
|
+
text += extractAdfText(child);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return text;
|
|
95
170
|
}
|
|
96
171
|
export {
|
|
97
172
|
JiraStatusSync
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import axios, { AxiosInstance } from 'axios';
|
|
15
15
|
import { detectDeploymentType, getApiBaseUrl } from './jira-deployment-detector.js';
|
|
16
|
-
import { toCommentBody } from './content-format-adapter.js';
|
|
16
|
+
import { toCommentBody, type AdfDocument, type AdfNode } from './content-format-adapter.js';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* External status representation (JIRA-specific)
|
|
@@ -160,4 +160,94 @@ export class JiraStatusSync {
|
|
|
160
160
|
body
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Post AC progress comment with proper ADF formatting and dedup.
|
|
166
|
+
*
|
|
167
|
+
* Builds native ADF with:
|
|
168
|
+
* - Bold header showing completion percentage
|
|
169
|
+
* - Bullet list with checkmark/cross emojis per AC
|
|
170
|
+
* - Fingerprint marker to prevent duplicate comments
|
|
171
|
+
*
|
|
172
|
+
* @param issueKey - JIRA issue key (e.g., PROJ-123)
|
|
173
|
+
* @param acStates - Array of AC states with id, description, completed
|
|
174
|
+
* @returns true if comment was posted, false if skipped (duplicate)
|
|
175
|
+
*/
|
|
176
|
+
async postProgressComment(
|
|
177
|
+
issueKey: string,
|
|
178
|
+
acStates: Array<{ id: string; description: string; completed: boolean }>,
|
|
179
|
+
): Promise<boolean> {
|
|
180
|
+
const total = acStates.length;
|
|
181
|
+
const completed = acStates.filter(ac => ac.completed).length;
|
|
182
|
+
const percentage = Math.round((completed / total) * 100);
|
|
183
|
+
const fingerprint = `sw-progress:${completed}/${total}`;
|
|
184
|
+
|
|
185
|
+
// Dedup: check last comment for same fingerprint
|
|
186
|
+
try {
|
|
187
|
+
const commentsResp = await this.client.get(`/issue/${issueKey}/comment`, {
|
|
188
|
+
params: { orderBy: '-created', maxResults: 1 },
|
|
189
|
+
});
|
|
190
|
+
const lastComment = commentsResp.data?.comments?.[0];
|
|
191
|
+
if (lastComment) {
|
|
192
|
+
const lastText = extractAdfText(lastComment.body);
|
|
193
|
+
if (lastText.includes(fingerprint)) {
|
|
194
|
+
return false; // Already posted for this state
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// If comment fetch fails, proceed with posting
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Build ADF body directly — ✅ for done, ❌ for pending
|
|
202
|
+
const listItems: AdfNode[] = acStates.map(ac => ({
|
|
203
|
+
type: 'listItem',
|
|
204
|
+
content: [{
|
|
205
|
+
type: 'paragraph',
|
|
206
|
+
content: [
|
|
207
|
+
{ type: 'text', text: `${ac.completed ? '\u2705' : '\u274C'} ${ac.id}: ${ac.description}` },
|
|
208
|
+
],
|
|
209
|
+
}],
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
const body: AdfDocument = {
|
|
213
|
+
type: 'doc',
|
|
214
|
+
version: 1,
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: 'heading',
|
|
218
|
+
attrs: { level: 3 },
|
|
219
|
+
content: [{ type: 'text', text: `Progress: ${completed}/${total} ACs (${percentage}%)` }],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: 'bulletList',
|
|
223
|
+
content: listItems,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
type: 'paragraph',
|
|
227
|
+
content: [
|
|
228
|
+
{ type: 'text', text: `${fingerprint} | Synced from SpecWeave`, marks: [{ type: 'em' }] },
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
await this.client.post(`/issue/${issueKey}/comment`, { body });
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Extract plain text from an ADF document (for dedup comparison).
|
|
241
|
+
*/
|
|
242
|
+
function extractAdfText(adf: any): string {
|
|
243
|
+
if (!adf) return '';
|
|
244
|
+
if (typeof adf === 'string') return adf;
|
|
245
|
+
let text = '';
|
|
246
|
+
if (adf.text) text += adf.text;
|
|
247
|
+
if (Array.isArray(adf.content)) {
|
|
248
|
+
for (const child of adf.content) {
|
|
249
|
+
text += extractAdfText(child);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return text;
|
|
163
253
|
}
|
|
@@ -64,7 +64,9 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
64
64
|
|
|
65
65
|
**Signals** (5+ = auto-route): Project name | Features list (3+) | Tech stack | Timeline/MVP | Problem statement | Business model
|
|
66
66
|
|
|
67
|
-
**Opt-out phrases**: "
|
|
67
|
+
**Opt-out phrases**: "Don't plan yet" | "Quick discussion" | "Let's explore ideas"
|
|
68
|
+
|
|
69
|
+
**Brainstorm routing**: "Just brainstorm first" | "brainstorm" | "ideate" | "what are our options" → routes to `/sw:brainstorm`
|
|
68
70
|
|
|
69
71
|
**NOT opt-out phrases**: "simple" | "quick" | "basic" | "small" — these still require `/sw:increment`
|
|
70
72
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"timestamp":"2026-01-04T16:11:43Z","event":"session_stop","reason":"No auto session active","details":"approve_called:main","success":false,"iteration":0,"increment":"none"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"timestamp":"2026-01-04T16:11:43Z","sessionId":"unknown","reason":"No auto session active","details":"approve_called:main","success":false,"iteration":0,"increment":"none","testsRun":false,"testsPassed":0,"testsFailed":0}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"ran": false,
|
|
3
|
-
"inputSummary": {
|
|
4
|
-
"transcriptLines": 266
|
|
5
|
-
},
|
|
6
|
-
"extracted": {
|
|
7
|
-
"skillLearnings": []
|
|
8
|
-
},
|
|
9
|
-
"written": {
|
|
10
|
-
"learningsAdded": 0,
|
|
11
|
-
"learningsSkippedDuplicate": 0
|
|
12
|
-
},
|
|
13
|
-
"durationMs": 1.4448749999999997,
|
|
14
|
-
"reason": "CLAUDE.md not found in project"
|
|
15
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[2026-02-03T22:03:06Z] APPROVE: No increments directory
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { AdoClientV2 } from "./ado-client-v2.js";
|
|
2
|
-
import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
|
|
3
|
-
import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
|
|
4
|
-
import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import fs from "fs/promises";
|
|
7
|
-
async function syncSpecToAdoWithEnhancedContent(options) {
|
|
8
|
-
const { specPath, organization, project, dryRun = false, verbose = false } = options;
|
|
9
|
-
try {
|
|
10
|
-
const baseSpec = await parseSpecContent(specPath);
|
|
11
|
-
if (!baseSpec) {
|
|
12
|
-
return {
|
|
13
|
-
success: false,
|
|
14
|
-
action: "error",
|
|
15
|
-
error: "Failed to parse spec content"
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
if (verbose) {
|
|
19
|
-
console.log(`\u{1F4C4} Parsed spec: ${baseSpec.identifier.compact}`);
|
|
20
|
-
}
|
|
21
|
-
const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
|
|
22
|
-
const rootDir = await findSpecWeaveRoot(specPath);
|
|
23
|
-
const mapper = new SpecIncrementMapper(rootDir);
|
|
24
|
-
const mapping = await mapper.mapSpecToIncrements(specId);
|
|
25
|
-
if (verbose) {
|
|
26
|
-
console.log(`\u{1F517} Found ${mapping.increments.length} related increments`);
|
|
27
|
-
}
|
|
28
|
-
const taskMapping = buildTaskMapping(mapping.increments, organization, project);
|
|
29
|
-
const architectureDocs = await findArchitectureDocs(rootDir, specId);
|
|
30
|
-
const enhancedSpec = {
|
|
31
|
-
...baseSpec,
|
|
32
|
-
summary: baseSpec.description,
|
|
33
|
-
taskMapping,
|
|
34
|
-
architectureDocs
|
|
35
|
-
};
|
|
36
|
-
const builder = new EnhancedContentBuilder();
|
|
37
|
-
const description = builder.buildExternalDescription(enhancedSpec);
|
|
38
|
-
if (verbose) {
|
|
39
|
-
console.log(`\u{1F4DD} Generated description: ${description.length} characters`);
|
|
40
|
-
}
|
|
41
|
-
if (dryRun) {
|
|
42
|
-
console.log("\u{1F50D} DRY RUN - Would create/update feature with:");
|
|
43
|
-
console.log(` Title: ${baseSpec.title}`);
|
|
44
|
-
console.log(` Description length: ${description.length}`);
|
|
45
|
-
return {
|
|
46
|
-
success: true,
|
|
47
|
-
action: "no-change",
|
|
48
|
-
tasksLinked: taskMapping?.tasks.length || 0
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
if (!organization || !project) {
|
|
52
|
-
return {
|
|
53
|
-
success: false,
|
|
54
|
-
action: "error",
|
|
55
|
-
error: "Azure DevOps organization/project not specified"
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
const profile = {
|
|
59
|
-
provider: "ado",
|
|
60
|
-
displayName: `${organization}/${project}`,
|
|
61
|
-
config: {
|
|
62
|
-
organization,
|
|
63
|
-
project
|
|
64
|
-
},
|
|
65
|
-
timeRange: { default: "1M", max: "6M" }
|
|
66
|
-
};
|
|
67
|
-
const pat = process.env.AZURE_DEVOPS_PAT || "";
|
|
68
|
-
const client = new AdoClientV2(profile, pat);
|
|
69
|
-
const existingFeature = await findExistingFeature(client, baseSpec.identifier.compact);
|
|
70
|
-
let result;
|
|
71
|
-
if (existingFeature) {
|
|
72
|
-
await client.updateWorkItem(existingFeature.id, {
|
|
73
|
-
title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
74
|
-
description
|
|
75
|
-
});
|
|
76
|
-
result = {
|
|
77
|
-
success: true,
|
|
78
|
-
action: "updated",
|
|
79
|
-
featureId: existingFeature.id,
|
|
80
|
-
featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${existingFeature.id}`,
|
|
81
|
-
tasksLinked: taskMapping?.tasks.length || 0
|
|
82
|
-
};
|
|
83
|
-
} else {
|
|
84
|
-
const feature = await client.createEpic({
|
|
85
|
-
title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
86
|
-
description,
|
|
87
|
-
tags: ["spec", "external-tool-sync"]
|
|
88
|
-
});
|
|
89
|
-
result = {
|
|
90
|
-
success: true,
|
|
91
|
-
action: "created",
|
|
92
|
-
featureId: feature.id,
|
|
93
|
-
featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${feature.id}`,
|
|
94
|
-
tasksLinked: taskMapping?.tasks.length || 0
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
if (verbose) {
|
|
98
|
-
console.log(`\u2705 ${result.action === "created" ? "Created" : "Updated"} feature #${result.featureId}`);
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
} catch (error) {
|
|
102
|
-
return {
|
|
103
|
-
success: false,
|
|
104
|
-
action: "error",
|
|
105
|
-
error: error.message
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
async function findSpecWeaveRoot(specPath) {
|
|
110
|
-
let currentDir = path.dirname(specPath);
|
|
111
|
-
while (true) {
|
|
112
|
-
const specweaveDir = path.join(currentDir, ".specweave");
|
|
113
|
-
try {
|
|
114
|
-
await fs.access(specweaveDir);
|
|
115
|
-
return currentDir;
|
|
116
|
-
} catch {
|
|
117
|
-
const parentDir = path.dirname(currentDir);
|
|
118
|
-
if (parentDir === currentDir) {
|
|
119
|
-
throw new Error(".specweave directory not found");
|
|
120
|
-
}
|
|
121
|
-
currentDir = parentDir;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function buildTaskMapping(increments, organization, project) {
|
|
126
|
-
if (increments.length === 0) return void 0;
|
|
127
|
-
const firstIncrement = increments[0];
|
|
128
|
-
const tasks = firstIncrement.tasks.map((task) => ({
|
|
129
|
-
id: task.id,
|
|
130
|
-
title: task.title,
|
|
131
|
-
userStories: task.userStories
|
|
132
|
-
}));
|
|
133
|
-
// Derive repository name from git remote or fall back to project name
|
|
134
|
-
let repoName = project;
|
|
135
|
-
try {
|
|
136
|
-
const { execSync } = require("child_process");
|
|
137
|
-
const remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
138
|
-
const match = remoteUrl.match(/\/([^/]+?)(?:\.git)?$/);
|
|
139
|
-
if (match) repoName = match[1];
|
|
140
|
-
} catch {
|
|
141
|
-
// Fallback to project name if git is unavailable
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
incrementId: firstIncrement.id,
|
|
145
|
-
tasks,
|
|
146
|
-
tasksUrl: `https://dev.azure.com/${organization}/${project}/_git/${repoName}?path=/.specweave/increments/${firstIncrement.id}/tasks.md`
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
async function findArchitectureDocs(rootDir, specId) {
|
|
150
|
-
const docs = [];
|
|
151
|
-
const archDir = path.join(rootDir, ".specweave/docs/internal/architecture");
|
|
152
|
-
try {
|
|
153
|
-
const adrDir = path.join(archDir, "adr");
|
|
154
|
-
try {
|
|
155
|
-
const adrs = await fs.readdir(adrDir);
|
|
156
|
-
const relatedAdrs = adrs.filter((file) => file.includes(specId.replace("spec-", "")));
|
|
157
|
-
for (const adr of relatedAdrs) {
|
|
158
|
-
docs.push({
|
|
159
|
-
type: "adr",
|
|
160
|
-
path: path.join(adrDir, adr),
|
|
161
|
-
title: adr.replace(".md", "").replace(/-/g, " ")
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
} catch {
|
|
165
|
-
}
|
|
166
|
-
} catch {
|
|
167
|
-
}
|
|
168
|
-
return docs;
|
|
169
|
-
}
|
|
170
|
-
async function findExistingFeature(client, specId) {
|
|
171
|
-
try {
|
|
172
|
-
const features = await client.queryWorkItems(`[System.Title] Contains '[${specId}]' AND [System.WorkItemType] = 'Feature'`);
|
|
173
|
-
return features[0] || null;
|
|
174
|
-
} catch {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
export {
|
|
179
|
-
syncSpecToAdoWithEnhancedContent
|
|
180
|
-
};
|