specweave 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils/external-resource-validator.d.ts.map +1 -1
- package/dist/utils/external-resource-validator.js +101 -59
- package/dist/utils/external-resource-validator.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/post-increment-change.sh +94 -0
- package/plugins/specweave/hooks/post-increment-status-change.sh +143 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.ts +57 -16
- package/plugins/specweave-github/commands/specweave-github-sync-from.md +147 -0
- package/plugins/specweave-github/lib/cli-sync-increment-changes.ts +33 -0
- package/plugins/specweave-github/lib/github-issue-updater.ts +449 -0
- package/plugins/specweave-github/lib/github-sync-bidirectional.ts +342 -0
- package/plugins/specweave-github/lib/github-sync-increment-changes.ts +380 -0
- package/src/templates/AGENTS.md.template +55 -9
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: specweave-github:sync-from
|
|
3
|
+
description: Sync state from GitHub to SpecWeave (bidirectional sync). Fetches issue state, comments, and detects conflicts.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sync from GitHub to SpecWeave
|
|
7
|
+
|
|
8
|
+
Bidirectional sync - pull changes from GitHub issue back to SpecWeave increment.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
/specweave-github:sync-from <incrementId>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What It Does
|
|
17
|
+
|
|
18
|
+
1. **Fetches GitHub Issue State**:
|
|
19
|
+
- Issue status (open/closed)
|
|
20
|
+
- Comments
|
|
21
|
+
- Labels, assignees, milestones
|
|
22
|
+
- Last updated timestamp
|
|
23
|
+
|
|
24
|
+
2. **Detects Conflicts**:
|
|
25
|
+
- GitHub closed but SpecWeave active
|
|
26
|
+
- SpecWeave completed but GitHub open
|
|
27
|
+
- GitHub abandoned but issue open
|
|
28
|
+
|
|
29
|
+
3. **Syncs Comments**:
|
|
30
|
+
- Saves GitHub comments to `.specweave/increments/<id>/logs/github-comments.md`
|
|
31
|
+
- Only syncs new comments (tracks IDs)
|
|
32
|
+
|
|
33
|
+
4. **Updates Metadata**:
|
|
34
|
+
- Updates `metadata.json` with latest GitHub state
|
|
35
|
+
- Tracks last sync timestamp
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
### Basic Sync
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
/specweave-github:sync-from 0015-hierarchical-sync
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Output**:
|
|
46
|
+
```
|
|
47
|
+
🔄 Syncing from GitHub for increment: 0015-hierarchical-sync
|
|
48
|
+
Syncing from anton-abyzov/specweave#29
|
|
49
|
+
✅ No conflicts - GitHub and SpecWeave in sync
|
|
50
|
+
📝 Syncing 3 new comment(s)
|
|
51
|
+
✅ Comments saved to: logs/github-comments.md
|
|
52
|
+
✅ Metadata updated
|
|
53
|
+
✅ Bidirectional sync complete
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Conflict Detection
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
/specweave-github:sync-from 0015-hierarchical-sync
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Output**:
|
|
63
|
+
```
|
|
64
|
+
🔄 Syncing from GitHub for increment: 0015-hierarchical-sync
|
|
65
|
+
Syncing from anton-abyzov/specweave#29
|
|
66
|
+
⚠️ Detected 1 conflict(s)
|
|
67
|
+
|
|
68
|
+
⚠️ Conflict detected: status
|
|
69
|
+
GitHub: closed
|
|
70
|
+
SpecWeave: active
|
|
71
|
+
|
|
72
|
+
⚠️ **CONFLICT**: GitHub issue closed but SpecWeave increment still active!
|
|
73
|
+
Recommendation: Run /specweave:done 0015-hierarchical-sync to close increment
|
|
74
|
+
Or reopen issue on GitHub if work is not complete
|
|
75
|
+
|
|
76
|
+
✅ Bidirectional sync complete
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## When to Use
|
|
80
|
+
|
|
81
|
+
Use this command when:
|
|
82
|
+
- ✅ Someone closed/reopened the GitHub issue
|
|
83
|
+
- ✅ Comments were added on GitHub (want to import them)
|
|
84
|
+
- ✅ Want to check if GitHub and SpecWeave are in sync
|
|
85
|
+
- ✅ Resolving conflicts between GitHub and SpecWeave state
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- GitHub CLI (`gh`) installed and authenticated
|
|
90
|
+
- GitHub issue linked in metadata.json
|
|
91
|
+
- Repository has GitHub remote configured
|
|
92
|
+
|
|
93
|
+
## Conflict Resolution
|
|
94
|
+
|
|
95
|
+
### GitHub Closed, SpecWeave Active
|
|
96
|
+
|
|
97
|
+
**Resolution**: Close SpecWeave increment
|
|
98
|
+
```bash
|
|
99
|
+
/specweave:done 0015-hierarchical-sync
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Or**: Reopen GitHub issue if work not complete
|
|
103
|
+
```bash
|
|
104
|
+
gh issue reopen 29
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### SpecWeave Completed, GitHub Open
|
|
108
|
+
|
|
109
|
+
**Resolution**: Close GitHub issue
|
|
110
|
+
```bash
|
|
111
|
+
gh issue close 29 --comment "Increment completed in SpecWeave"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### SpecWeave Abandoned, GitHub Open
|
|
115
|
+
|
|
116
|
+
**Resolution**: Close GitHub issue with reason
|
|
117
|
+
```bash
|
|
118
|
+
gh issue close 29 --comment "Increment abandoned: Requirements changed"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Files Modified
|
|
122
|
+
|
|
123
|
+
- `.specweave/increments/<id>/logs/github-comments.md` - GitHub comments
|
|
124
|
+
- `.specweave/increments/<id>/metadata.json` - Sync metadata
|
|
125
|
+
|
|
126
|
+
## Related Commands
|
|
127
|
+
|
|
128
|
+
- `/specweave-github:sync` - One-way sync (SpecWeave → GitHub)
|
|
129
|
+
- `/specweave-github:create-issue` - Create GitHub issue
|
|
130
|
+
- `/specweave-github:close-issue` - Close GitHub issue
|
|
131
|
+
- `/specweave-github:status` - Check sync status
|
|
132
|
+
|
|
133
|
+
## Automation
|
|
134
|
+
|
|
135
|
+
For automatic bidirectional sync, add to cron or CI/CD:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Sync all active increments hourly
|
|
139
|
+
0 * * * * cd /path/to/project && \
|
|
140
|
+
for inc in $(ls .specweave/increments/ | grep -v _backlog); do \
|
|
141
|
+
/specweave-github:sync-from $inc; \
|
|
142
|
+
done
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Implementation
|
|
146
|
+
|
|
147
|
+
Invokes `github-sync-bidirectional` agent with conflict detection and resolution logic.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI wrapper for syncing increment changes to GitHub
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node dist/plugins/specweave-github/lib/cli-sync-increment-changes.js <incrementId> <changedFile>
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* node dist/plugins/specweave-github/lib/cli-sync-increment-changes.js 0015-hierarchical-sync spec.md
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { syncIncrementChanges } from './github-sync-increment-changes.js';
|
|
14
|
+
|
|
15
|
+
const incrementId = process.argv[2];
|
|
16
|
+
const changedFile = process.argv[3] as 'spec.md' | 'plan.md' | 'tasks.md';
|
|
17
|
+
|
|
18
|
+
if (!incrementId || !changedFile) {
|
|
19
|
+
console.error('❌ Usage: cli-sync-increment-changes <incrementId> <changedFile>');
|
|
20
|
+
console.error(' Example: cli-sync-increment-changes 0015-hierarchical-sync spec.md');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!['spec.md', 'plan.md', 'tasks.md'].includes(changedFile)) {
|
|
25
|
+
console.error(`❌ Invalid file: ${changedFile}`);
|
|
26
|
+
console.error(' Must be one of: spec.md, plan.md, tasks.md');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
syncIncrementChanges(incrementId, changedFile).catch((error) => {
|
|
31
|
+
console.error('❌ Fatal error:', error);
|
|
32
|
+
// Don't exit with error code - best-effort sync
|
|
33
|
+
});
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Issue Updater for Living Docs Sync
|
|
3
|
+
*
|
|
4
|
+
* Handles updating GitHub issues with living documentation links and content.
|
|
5
|
+
* Used by post-task-completion hook to keep GitHub issues in sync with SpecWeave docs.
|
|
6
|
+
*
|
|
7
|
+
* @module github-issue-updater
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
|
|
13
|
+
|
|
14
|
+
export interface LivingDocsSection {
|
|
15
|
+
specs: string[];
|
|
16
|
+
architecture: string[];
|
|
17
|
+
diagrams: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IncrementMetadata {
|
|
21
|
+
id: string;
|
|
22
|
+
status: string;
|
|
23
|
+
type: string;
|
|
24
|
+
github?: {
|
|
25
|
+
issue: number;
|
|
26
|
+
url: string;
|
|
27
|
+
synced?: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update GitHub issue with living docs section
|
|
33
|
+
*/
|
|
34
|
+
export async function updateIssueLivingDocs(
|
|
35
|
+
issueNumber: number,
|
|
36
|
+
livingDocs: LivingDocsSection,
|
|
37
|
+
owner: string,
|
|
38
|
+
repo: string
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
console.log(`📝 Updating GitHub issue #${issueNumber} with living docs...`);
|
|
41
|
+
|
|
42
|
+
// 1. Get current issue body
|
|
43
|
+
const currentBody = await getIssueBody(issueNumber, owner, repo);
|
|
44
|
+
|
|
45
|
+
// 2. Build living docs section
|
|
46
|
+
const livingDocsSection = buildLivingDocsSection(livingDocs, owner, repo);
|
|
47
|
+
|
|
48
|
+
// 3. Update or append living docs section
|
|
49
|
+
const updatedBody = updateBodyWithLivingDocs(currentBody, livingDocsSection);
|
|
50
|
+
|
|
51
|
+
// 4. Update issue
|
|
52
|
+
await updateIssueBody(issueNumber, updatedBody, owner, repo);
|
|
53
|
+
|
|
54
|
+
console.log(`✅ Living docs section updated in issue #${issueNumber}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Post comment about ADR/HLD/diagram creation
|
|
59
|
+
*/
|
|
60
|
+
export async function postArchitectureComment(
|
|
61
|
+
issueNumber: number,
|
|
62
|
+
docPath: string,
|
|
63
|
+
owner: string,
|
|
64
|
+
repo: string
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const docType = getDocType(docPath);
|
|
67
|
+
const docName = path.basename(docPath, '.md');
|
|
68
|
+
const docUrl = `https://github.com/${owner}/${repo}/blob/develop/${docPath}`;
|
|
69
|
+
|
|
70
|
+
const comment = `
|
|
71
|
+
🏗️ **${docType} Created**
|
|
72
|
+
|
|
73
|
+
**Document**: [${docName}](${docUrl})
|
|
74
|
+
|
|
75
|
+
**Path**: \`${docPath}\`
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
🤖 Auto-updated by SpecWeave
|
|
79
|
+
`.trim();
|
|
80
|
+
|
|
81
|
+
await postComment(issueNumber, comment, owner, repo);
|
|
82
|
+
console.log(`✅ Posted ${docType} comment to issue #${issueNumber}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Post scope change comment
|
|
87
|
+
*/
|
|
88
|
+
export async function postScopeChangeComment(
|
|
89
|
+
issueNumber: number,
|
|
90
|
+
changes: {
|
|
91
|
+
added?: string[];
|
|
92
|
+
removed?: string[];
|
|
93
|
+
modified?: string[];
|
|
94
|
+
reason?: string;
|
|
95
|
+
impact?: string;
|
|
96
|
+
},
|
|
97
|
+
owner: string,
|
|
98
|
+
repo: string
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const parts: string[] = ['**Scope Change Detected**', ''];
|
|
101
|
+
|
|
102
|
+
if (changes.added && changes.added.length > 0) {
|
|
103
|
+
parts.push('**Added**:');
|
|
104
|
+
changes.added.forEach(item => parts.push(`- ✅ ${item}`));
|
|
105
|
+
parts.push('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (changes.removed && changes.removed.length > 0) {
|
|
109
|
+
parts.push('**Removed**:');
|
|
110
|
+
changes.removed.forEach(item => parts.push(`- ❌ ${item}`));
|
|
111
|
+
parts.push('');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (changes.modified && changes.modified.length > 0) {
|
|
115
|
+
parts.push('**Modified**:');
|
|
116
|
+
changes.modified.forEach(item => parts.push(`- 🔄 ${item}`));
|
|
117
|
+
parts.push('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (changes.reason) {
|
|
121
|
+
parts.push(`**Reason**: ${changes.reason}`);
|
|
122
|
+
parts.push('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (changes.impact) {
|
|
126
|
+
parts.push(`**Impact**: ${changes.impact}`);
|
|
127
|
+
parts.push('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
parts.push('---');
|
|
131
|
+
parts.push('🤖 Auto-updated by SpecWeave');
|
|
132
|
+
|
|
133
|
+
await postComment(issueNumber, parts.join('\n'), owner, repo);
|
|
134
|
+
console.log(`✅ Posted scope change comment to issue #${issueNumber}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Post status change comment (pause/resume/abandon)
|
|
139
|
+
*/
|
|
140
|
+
export async function postStatusChangeComment(
|
|
141
|
+
issueNumber: number,
|
|
142
|
+
status: 'paused' | 'resumed' | 'abandoned',
|
|
143
|
+
reason: string,
|
|
144
|
+
owner: string,
|
|
145
|
+
repo: string
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
const emoji = {
|
|
148
|
+
paused: '⏸️',
|
|
149
|
+
resumed: '▶️',
|
|
150
|
+
abandoned: '🗑️'
|
|
151
|
+
}[status];
|
|
152
|
+
|
|
153
|
+
const title = {
|
|
154
|
+
paused: 'Increment Paused',
|
|
155
|
+
resumed: 'Increment Resumed',
|
|
156
|
+
abandoned: 'Increment Abandoned'
|
|
157
|
+
}[status];
|
|
158
|
+
|
|
159
|
+
const comment = `
|
|
160
|
+
${emoji} **${title}**
|
|
161
|
+
|
|
162
|
+
**Reason**: ${reason}
|
|
163
|
+
|
|
164
|
+
**Timestamp**: ${new Date().toISOString()}
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
🤖 Auto-updated by SpecWeave
|
|
168
|
+
`.trim();
|
|
169
|
+
|
|
170
|
+
await postComment(issueNumber, comment, owner, repo);
|
|
171
|
+
console.log(`✅ Posted ${status} comment to issue #${issueNumber}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Collect living docs for an increment
|
|
176
|
+
*/
|
|
177
|
+
export async function collectLivingDocs(incrementId: string): Promise<LivingDocsSection> {
|
|
178
|
+
const livingDocs: LivingDocsSection = {
|
|
179
|
+
specs: [],
|
|
180
|
+
architecture: [],
|
|
181
|
+
diagrams: []
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// 1. Find specs
|
|
185
|
+
const specsDir = path.join(process.cwd(), '.specweave/docs/internal/specs');
|
|
186
|
+
if (await fs.pathExists(specsDir)) {
|
|
187
|
+
const specFiles = await fs.readdir(specsDir);
|
|
188
|
+
for (const file of specFiles) {
|
|
189
|
+
if (file.endsWith('.md') && !file.startsWith('README')) {
|
|
190
|
+
livingDocs.specs.push(path.join('.specweave/docs/internal/specs', file));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 2. Find architecture docs (ADRs, HLDs)
|
|
196
|
+
const archDir = path.join(process.cwd(), '.specweave/docs/internal/architecture');
|
|
197
|
+
if (await fs.pathExists(archDir)) {
|
|
198
|
+
// ADRs
|
|
199
|
+
const adrDir = path.join(archDir, 'adr');
|
|
200
|
+
if (await fs.pathExists(adrDir)) {
|
|
201
|
+
const adrFiles = await fs.readdir(adrDir);
|
|
202
|
+
for (const file of adrFiles) {
|
|
203
|
+
if (file.endsWith('.md')) {
|
|
204
|
+
livingDocs.architecture.push(path.join('.specweave/docs/internal/architecture/adr', file));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// HLDs
|
|
210
|
+
const hldFiles = await fs.readdir(archDir);
|
|
211
|
+
for (const file of hldFiles) {
|
|
212
|
+
if (file.startsWith('hld-') && file.endsWith('.md')) {
|
|
213
|
+
livingDocs.architecture.push(path.join('.specweave/docs/internal/architecture', file));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 3. Find diagrams
|
|
219
|
+
const diagramsDir = path.join(process.cwd(), '.specweave/docs/internal/architecture/diagrams');
|
|
220
|
+
if (await fs.pathExists(diagramsDir)) {
|
|
221
|
+
const diagramFiles = await fs.readdir(diagramsDir);
|
|
222
|
+
for (const file of diagramFiles) {
|
|
223
|
+
if (file.endsWith('.mmd') || file.endsWith('.svg')) {
|
|
224
|
+
livingDocs.diagrams.push(path.join('.specweave/docs/internal/architecture/diagrams', file));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return livingDocs;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Load increment metadata
|
|
234
|
+
*/
|
|
235
|
+
export async function loadIncrementMetadata(incrementId: string): Promise<IncrementMetadata | null> {
|
|
236
|
+
const metadataPath = path.join(
|
|
237
|
+
process.cwd(),
|
|
238
|
+
'.specweave/increments',
|
|
239
|
+
incrementId,
|
|
240
|
+
'metadata.json'
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (!await fs.pathExists(metadataPath)) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return await fs.readJson(metadataPath);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Detect repository owner and name from git remote
|
|
252
|
+
*/
|
|
253
|
+
export async function detectRepo(): Promise<{ owner: string; repo: string } | null> {
|
|
254
|
+
try {
|
|
255
|
+
const result = await execFileNoThrow('git', ['remote', 'get-url', 'origin']);
|
|
256
|
+
|
|
257
|
+
if (result.status !== 0) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const remote = result.stdout.trim();
|
|
262
|
+
const match = remote.match(/github\.com[:/](.+)\/(.+?)(?:\.git)?$/);
|
|
263
|
+
|
|
264
|
+
if (!match) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
owner: match[1],
|
|
270
|
+
repo: match[2]
|
|
271
|
+
};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// =============================================================================
|
|
278
|
+
// PRIVATE HELPERS
|
|
279
|
+
// =============================================================================
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get current issue body
|
|
283
|
+
*/
|
|
284
|
+
async function getIssueBody(
|
|
285
|
+
issueNumber: number,
|
|
286
|
+
owner: string,
|
|
287
|
+
repo: string
|
|
288
|
+
): Promise<string> {
|
|
289
|
+
const result = await execFileNoThrow('gh', [
|
|
290
|
+
'issue',
|
|
291
|
+
'view',
|
|
292
|
+
String(issueNumber),
|
|
293
|
+
'--repo',
|
|
294
|
+
`${owner}/${repo}`,
|
|
295
|
+
'--json',
|
|
296
|
+
'body',
|
|
297
|
+
'-q',
|
|
298
|
+
'.body'
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
if (result.status !== 0) {
|
|
302
|
+
throw new Error(`Failed to get issue body: ${result.stderr}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return result.stdout.trim();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Update issue body
|
|
310
|
+
*/
|
|
311
|
+
async function updateIssueBody(
|
|
312
|
+
issueNumber: number,
|
|
313
|
+
body: string,
|
|
314
|
+
owner: string,
|
|
315
|
+
repo: string
|
|
316
|
+
): Promise<void> {
|
|
317
|
+
const result = await execFileNoThrow('gh', [
|
|
318
|
+
'issue',
|
|
319
|
+
'edit',
|
|
320
|
+
String(issueNumber),
|
|
321
|
+
'--repo',
|
|
322
|
+
`${owner}/${repo}`,
|
|
323
|
+
'--body',
|
|
324
|
+
body
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
if (result.status !== 0) {
|
|
328
|
+
throw new Error(`Failed to update issue body: ${result.stderr}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Post comment to issue
|
|
334
|
+
*/
|
|
335
|
+
async function postComment(
|
|
336
|
+
issueNumber: number,
|
|
337
|
+
comment: string,
|
|
338
|
+
owner: string,
|
|
339
|
+
repo: string
|
|
340
|
+
): Promise<void> {
|
|
341
|
+
const result = await execFileNoThrow('gh', [
|
|
342
|
+
'issue',
|
|
343
|
+
'comment',
|
|
344
|
+
String(issueNumber),
|
|
345
|
+
'--repo',
|
|
346
|
+
`${owner}/${repo}`,
|
|
347
|
+
'--body',
|
|
348
|
+
comment
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
if (result.status !== 0) {
|
|
352
|
+
throw new Error(`Failed to post comment: ${result.stderr}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Build living docs section for issue body
|
|
358
|
+
*/
|
|
359
|
+
function buildLivingDocsSection(
|
|
360
|
+
livingDocs: LivingDocsSection,
|
|
361
|
+
owner: string,
|
|
362
|
+
repo: string
|
|
363
|
+
): string {
|
|
364
|
+
const parts: string[] = ['## 📚 Living Documentation', ''];
|
|
365
|
+
|
|
366
|
+
// Specs
|
|
367
|
+
if (livingDocs.specs.length > 0) {
|
|
368
|
+
parts.push('**Specifications**:');
|
|
369
|
+
livingDocs.specs.forEach(spec => {
|
|
370
|
+
const name = path.basename(spec, '.md');
|
|
371
|
+
const url = `https://github.com/${owner}/${repo}/blob/develop/${spec}`;
|
|
372
|
+
parts.push(`- [${name}](${url})`);
|
|
373
|
+
});
|
|
374
|
+
parts.push('');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Architecture
|
|
378
|
+
if (livingDocs.architecture.length > 0) {
|
|
379
|
+
parts.push('**Architecture**:');
|
|
380
|
+
livingDocs.architecture.forEach(doc => {
|
|
381
|
+
const name = path.basename(doc, '.md');
|
|
382
|
+
const url = `https://github.com/${owner}/${repo}/blob/develop/${doc}`;
|
|
383
|
+
parts.push(`- [${name}](${url})`);
|
|
384
|
+
});
|
|
385
|
+
parts.push('');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Diagrams
|
|
389
|
+
if (livingDocs.diagrams.length > 0) {
|
|
390
|
+
parts.push('**Diagrams**:');
|
|
391
|
+
livingDocs.diagrams.forEach(diagram => {
|
|
392
|
+
const name = path.basename(diagram);
|
|
393
|
+
const url = `https://github.com/${owner}/${repo}/blob/develop/${diagram}`;
|
|
394
|
+
parts.push(`- [${name}](${url})`);
|
|
395
|
+
});
|
|
396
|
+
parts.push('');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
parts.push('---');
|
|
400
|
+
|
|
401
|
+
return parts.join('\n');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Update body with living docs section
|
|
406
|
+
*/
|
|
407
|
+
function updateBodyWithLivingDocs(
|
|
408
|
+
currentBody: string,
|
|
409
|
+
livingDocsSection: string
|
|
410
|
+
): string {
|
|
411
|
+
// Check if living docs section already exists
|
|
412
|
+
const marker = '## 📚 Living Documentation';
|
|
413
|
+
|
|
414
|
+
if (currentBody.includes(marker)) {
|
|
415
|
+
// Replace existing section
|
|
416
|
+
const beforeMarker = currentBody.substring(0, currentBody.indexOf(marker));
|
|
417
|
+
const afterMarker = currentBody.substring(currentBody.indexOf(marker));
|
|
418
|
+
|
|
419
|
+
// Find end of section (next ## header or end of body)
|
|
420
|
+
const nextSection = afterMarker.indexOf('\n## ', 1);
|
|
421
|
+
const replacement = nextSection > 0
|
|
422
|
+
? afterMarker.substring(0, nextSection)
|
|
423
|
+
: afterMarker;
|
|
424
|
+
|
|
425
|
+
return beforeMarker + livingDocsSection + (nextSection > 0 ? afterMarker.substring(nextSection) : '');
|
|
426
|
+
} else {
|
|
427
|
+
// Append at end
|
|
428
|
+
return currentBody + '\n\n' + livingDocsSection;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Determine document type from path
|
|
434
|
+
*/
|
|
435
|
+
function getDocType(docPath: string): string {
|
|
436
|
+
if (docPath.includes('/adr/')) {
|
|
437
|
+
return 'Architecture Decision Record (ADR)';
|
|
438
|
+
}
|
|
439
|
+
if (docPath.includes('/diagrams/')) {
|
|
440
|
+
return 'Architecture Diagram';
|
|
441
|
+
}
|
|
442
|
+
if (docPath.startsWith('hld-')) {
|
|
443
|
+
return 'High-Level Design (HLD)';
|
|
444
|
+
}
|
|
445
|
+
if (docPath.includes('/specs/')) {
|
|
446
|
+
return 'Specification';
|
|
447
|
+
}
|
|
448
|
+
return 'Documentation';
|
|
449
|
+
}
|