specweave 1.0.235 → 1.0.239
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 +89 -193
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts +37 -0
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +176 -0
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts +36 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.js +115 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts +37 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js +56 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts +68 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.js +102 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts +64 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +162 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.d.ts +50 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.js +107 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +53 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.js +138 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts +40 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.js +50 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts +30 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.js +75 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts +94 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.js +232 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.d.ts +50 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.js +114 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts +53 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.js +109 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts +21 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +161 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +46 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +99 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts +43 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +153 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -0
- package/dist/plugins/specweave-github/lib/index.d.ts +1 -4
- package/dist/plugins/specweave-github/lib/index.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/index.js +1 -4
- package/dist/plugins/specweave-github/lib/index.js.map +1 -1
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +7 -0
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js +15 -0
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts +10 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js +36 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts +25 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js +57 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts +7 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.js +17 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.js.map +1 -0
- package/dist/src/cli/commands/auto.d.ts.map +1 -1
- package/dist/src/cli/commands/auto.js +1 -2
- package/dist/src/cli/commands/auto.js.map +1 -1
- package/dist/src/cli/commands/cancel-auto.js +1 -2
- package/dist/src/cli/commands/cancel-auto.js.map +1 -1
- package/dist/src/cli/commands/living-docs.js +2 -2
- package/dist/src/cli/commands/living-docs.js.map +1 -1
- package/dist/src/cli/commands/update.d.ts.map +1 -1
- package/dist/src/cli/commands/update.js +1 -2
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/core/config/types.d.ts +8 -0
- package/dist/src/core/config/types.d.ts.map +1 -1
- package/dist/src/core/config/types.js +3 -0
- package/dist/src/core/config/types.js.map +1 -1
- package/dist/src/core/types/sync-profile.d.ts +72 -0
- package/dist/src/core/types/sync-profile.d.ts.map +1 -1
- package/dist/src/core/types/sync-profile.js +6 -0
- package/dist/src/core/types/sync-profile.js.map +1 -1
- package/package.json +2 -2
- package/plugins/specweave/hooks/hooks.json +2 -2
- package/plugins/specweave/hooks/startup-health-check.sh +1 -1
- package/plugins/specweave/hooks/stop-auto-v5.sh +166 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +10 -0
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +21 -1
- package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -1
- package/plugins/specweave/skills/auto/SKILL.md +71 -251
- package/plugins/specweave/skills/team-build/SKILL.md +370 -0
- package/plugins/specweave/skills/team-merge/SKILL.md +123 -0
- package/plugins/specweave/skills/team-orchestrate/SKILL.md +800 -0
- package/plugins/specweave/skills/team-status/SKILL.md +89 -0
- package/plugins/specweave-github/MULTI-PROJECT-SYNC-ARCHITECTURE.md +94 -8
- package/plugins/specweave-github/commands/sync.md +17 -3
- package/plugins/specweave-github/hooks/github-ac-sync-handler.sh +255 -0
- package/plugins/specweave-github/hooks/github-auto-create-handler.sh +455 -0
- package/plugins/specweave-github/lib/github-ac-comment-poster.js +150 -0
- package/plugins/specweave-github/lib/github-ac-comment-poster.ts +245 -0
- package/plugins/specweave-github/lib/github-batch-sync.js +93 -0
- package/plugins/specweave-github/lib/github-batch-sync.ts +152 -0
- package/plugins/specweave-github/lib/github-board-resolver-v2.js +47 -0
- package/plugins/specweave-github/lib/github-board-resolver-v2.ts +73 -0
- package/plugins/specweave-github/lib/github-conflict-resolver.js +90 -0
- package/plugins/specweave-github/lib/github-conflict-resolver.ts +154 -0
- package/plugins/specweave-github/lib/github-cross-repo-sync.js +168 -0
- package/plugins/specweave-github/lib/github-cross-repo-sync.ts +252 -0
- package/plugins/specweave-github/lib/github-field-sync.js +116 -0
- package/plugins/specweave-github/lib/github-field-sync.ts +165 -0
- package/plugins/specweave-github/lib/github-graphql-client.js +129 -0
- package/plugins/specweave-github/lib/github-graphql-client.ts +181 -0
- package/plugins/specweave-github/lib/github-issue-body-generator.js +30 -0
- package/plugins/specweave-github/lib/github-issue-body-generator.ts +76 -0
- package/plugins/specweave-github/lib/github-issue-body-parser.js +55 -0
- package/plugins/specweave-github/lib/github-issue-body-parser.ts +92 -0
- package/plugins/specweave-github/lib/github-pull-sync.js +185 -0
- package/plugins/specweave-github/lib/github-pull-sync.ts +343 -0
- package/plugins/specweave-github/lib/github-push-sync.js +119 -0
- package/plugins/specweave-github/lib/github-push-sync.ts +174 -0
- package/plugins/specweave-github/lib/github-rate-limiter.js +96 -0
- package/plugins/specweave-github/lib/github-rate-limiter.ts +143 -0
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +117 -0
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +180 -0
- package/plugins/specweave-github/lib/github-sync-orchestrator.js +84 -0
- package/plugins/specweave-github/lib/github-sync-orchestrator.ts +156 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.js +134 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.ts +226 -0
- package/plugins/specweave-github/lib/index.js +1 -7
- package/plugins/specweave-github/lib/index.ts +1 -4
- package/plugins/specweave-github/skills/github-sync/SKILL.md +76 -4
- package/plugins/specweave-testing/commands/e2e-setup.md +18 -0
- package/plugins/specweave-testing/commands/ui-automate.md +2 -0
- package/plugins/specweave-testing/commands/ui-inspect.md +8 -0
- package/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +6 -0
- package/plugins/specweave-testing/lib/playwright-ci-defaults.js +14 -0
- package/plugins/specweave-testing/lib/playwright-ci-defaults.ts +24 -0
- package/plugins/specweave-testing/lib/playwright-cli-detector.js +33 -0
- package/plugins/specweave-testing/lib/playwright-cli-detector.ts +48 -0
- package/plugins/specweave-testing/lib/playwright-cli-runner.js +58 -0
- package/plugins/specweave-testing/lib/playwright-cli-runner.ts +80 -0
- package/plugins/specweave-testing/lib/playwright-routing.js +16 -0
- package/plugins/specweave-testing/lib/playwright-routing.ts +38 -0
- package/plugins/specweave-testing/skills/e2e-testing/SKILL.md +38 -0
- package/src/templates/CLAUDE.md.template +7 -0
- package/src/templates/config.json.template +9 -1
- package/dist/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
- package/dist/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/subtask-sync.js +0 -147
- package/dist/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
- package/dist/plugins/specweave-github/lib/task-parser.d.ts +0 -37
- package/dist/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/task-parser.js +0 -211
- package/dist/plugins/specweave-github/lib/task-parser.js.map +0 -1
- package/dist/plugins/specweave-github/lib/task-sync.d.ts +0 -56
- package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/task-sync.js +0 -375
- package/dist/plugins/specweave-github/lib/task-sync.js.map +0 -1
- package/plugins/specweave/hooks/validate-completion-conditions.sh +0 -474
- package/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
- package/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
- package/plugins/specweave-github/lib/subtask-sync.js +0 -154
- package/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
- package/plugins/specweave-github/lib/subtask-sync.ts +0 -225
- package/plugins/specweave-github/lib/task-parser.d.js +0 -0
- package/plugins/specweave-github/lib/task-parser.d.ts +0 -37
- package/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
- package/plugins/specweave-github/lib/task-parser.js +0 -195
- package/plugins/specweave-github/lib/task-parser.js.map +0 -1
- package/plugins/specweave-github/lib/task-parser.ts +0 -246
- package/plugins/specweave-github/lib/task-sync.d.js +0 -0
- package/plugins/specweave-github/lib/task-sync.d.ts +0 -51
- package/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
- package/plugins/specweave-github/lib/task-sync.js +0 -415
- package/plugins/specweave-github/lib/task-sync.js.map +0 -1
- package/plugins/specweave-github/lib/task-sync.ts +0 -451
- package/plugins/specweave-github/skills/github-issue-tracker/SKILL.md +0 -496
- /package/plugins/specweave/hooks/{stop-auto.sh → _archive/stop-auto-v4-legacy.sh} +0 -0
- /package/plugins/{specweave-github/lib/subtask-sync.d.js → specweave-testing/lib/playwright-ci-defaults.d.js} +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Spec Frontmatter Updater
|
|
3
|
+
*
|
|
4
|
+
* Updates spec.md YAML frontmatter after push sync to record
|
|
5
|
+
* GitHub issue links for each user story.
|
|
6
|
+
*
|
|
7
|
+
* @module github-spec-frontmatter-updater
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
11
|
+
import type { PushSyncResult } from './github-push-sync.js';
|
|
12
|
+
import type { GitHubSyncMetadata, GitHubUserStoryLink } from '../../../src/core/types/sync-profile.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Update spec.md frontmatter with GitHub sync results.
|
|
16
|
+
*
|
|
17
|
+
* Reads the spec file, parses YAML frontmatter, merges sync results
|
|
18
|
+
* into the externalLinks.github section, and writes back.
|
|
19
|
+
*/
|
|
20
|
+
export async function updateSpecFrontmatter(
|
|
21
|
+
specPath: string,
|
|
22
|
+
syncResult: PushSyncResult,
|
|
23
|
+
options?: { projectV2Id?: string; projectV2Number?: number },
|
|
24
|
+
): Promise<GitHubSyncMetadata> {
|
|
25
|
+
const content = await readFile(specPath, 'utf-8');
|
|
26
|
+
|
|
27
|
+
// Parse frontmatter
|
|
28
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
29
|
+
let frontmatter: Record<string, unknown> = {};
|
|
30
|
+
let body = content;
|
|
31
|
+
|
|
32
|
+
if (fmMatch) {
|
|
33
|
+
frontmatter = parseYamlSimple(fmMatch[1]);
|
|
34
|
+
body = content.slice(fmMatch[0].length);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get existing externalLinks
|
|
38
|
+
const externalLinks = (frontmatter.externalLinks ?? {}) as Record<string, unknown>;
|
|
39
|
+
const existingGithub = (externalLinks.github ?? {}) as Record<string, unknown>;
|
|
40
|
+
const existingUserStories = (existingGithub.userStories ?? {}) as Record<string, GitHubUserStoryLink>;
|
|
41
|
+
|
|
42
|
+
// Build updated user stories map (preserve existing)
|
|
43
|
+
const userStories: Record<string, GitHubUserStoryLink> = { ...existingUserStories };
|
|
44
|
+
const now = new Date().toISOString();
|
|
45
|
+
|
|
46
|
+
// Merge created issues
|
|
47
|
+
for (const item of syncResult.created) {
|
|
48
|
+
userStories[item.userStoryId] = {
|
|
49
|
+
issueNumber: item.issueNumber,
|
|
50
|
+
issueUrl: item.issueUrl,
|
|
51
|
+
issueNodeId: item.issueNodeId,
|
|
52
|
+
syncedAt: now,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Merge updated issues (preserve existing nodeId)
|
|
57
|
+
for (const item of syncResult.updated) {
|
|
58
|
+
const existing = userStories[item.userStoryId];
|
|
59
|
+
userStories[item.userStoryId] = {
|
|
60
|
+
issueNumber: item.issueNumber,
|
|
61
|
+
issueUrl: item.issueUrl,
|
|
62
|
+
issueNodeId: existing?.issueNodeId,
|
|
63
|
+
syncedAt: now,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Determine sync status
|
|
68
|
+
const syncStatus: GitHubSyncMetadata['syncStatus'] =
|
|
69
|
+
syncResult.errors.length > 0 ? 'dirty' : 'synced';
|
|
70
|
+
|
|
71
|
+
// Build metadata result
|
|
72
|
+
const metadata: GitHubSyncMetadata = {
|
|
73
|
+
syncStatus,
|
|
74
|
+
userStories,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ProjectV2 info: prefer options, fall back to existing
|
|
78
|
+
if (options?.projectV2Id) {
|
|
79
|
+
metadata.projectV2Id = options.projectV2Id;
|
|
80
|
+
} else if (existingGithub.projectV2Id) {
|
|
81
|
+
metadata.projectV2Id = existingGithub.projectV2Id as string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (options?.projectV2Number) {
|
|
85
|
+
metadata.projectV2Number = options.projectV2Number;
|
|
86
|
+
} else if (existingGithub.projectV2Number) {
|
|
87
|
+
metadata.projectV2Number = existingGithub.projectV2Number as number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Update frontmatter
|
|
91
|
+
externalLinks.github = metadata;
|
|
92
|
+
frontmatter.externalLinks = externalLinks;
|
|
93
|
+
|
|
94
|
+
// Write back
|
|
95
|
+
const newFrontmatter = stringifyYaml(frontmatter);
|
|
96
|
+
const newContent = `---\n${newFrontmatter}\n---${body}`;
|
|
97
|
+
await writeFile(specPath, newContent, 'utf-8');
|
|
98
|
+
|
|
99
|
+
return metadata;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Simple YAML parser for spec frontmatter.
|
|
104
|
+
* Handles nested objects, strings, numbers, booleans, null.
|
|
105
|
+
*/
|
|
106
|
+
function parseYamlSimple(yaml: string): Record<string, unknown> {
|
|
107
|
+
const result: Record<string, unknown> = {};
|
|
108
|
+
const lines = yaml.split('\n');
|
|
109
|
+
const stack: Array<{ obj: Record<string, unknown>; indent: number }> = [
|
|
110
|
+
{ obj: result, indent: -1 },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
115
|
+
|
|
116
|
+
const indent = line.search(/\S/);
|
|
117
|
+
const trimmed = line.trim();
|
|
118
|
+
const colonIdx = trimmed.indexOf(':');
|
|
119
|
+
if (colonIdx === -1) continue;
|
|
120
|
+
|
|
121
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
122
|
+
const rawValue = trimmed.slice(colonIdx + 1).trim();
|
|
123
|
+
|
|
124
|
+
// Pop stack to correct indent level
|
|
125
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
126
|
+
stack.pop();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const parent = stack[stack.length - 1].obj;
|
|
130
|
+
|
|
131
|
+
if (rawValue === '' || rawValue === undefined) {
|
|
132
|
+
// Nested object
|
|
133
|
+
const child: Record<string, unknown> = {};
|
|
134
|
+
parent[key] = child;
|
|
135
|
+
stack.push({ obj: child, indent });
|
|
136
|
+
} else {
|
|
137
|
+
parent[key] = parseYamlValue(rawValue);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseYamlValue(raw: string): unknown {
|
|
145
|
+
if (raw === 'null') return null;
|
|
146
|
+
if (raw === 'true') return true;
|
|
147
|
+
if (raw === 'false') return false;
|
|
148
|
+
if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
|
|
149
|
+
if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
|
|
150
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) ||
|
|
151
|
+
(raw.startsWith("'") && raw.endsWith("'"))) {
|
|
152
|
+
return raw.slice(1, -1);
|
|
153
|
+
}
|
|
154
|
+
return raw;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Simple YAML stringifier.
|
|
159
|
+
*/
|
|
160
|
+
function stringifyYaml(obj: Record<string, unknown>, indent = 0): string {
|
|
161
|
+
const prefix = ' '.repeat(indent);
|
|
162
|
+
const parts: string[] = [];
|
|
163
|
+
|
|
164
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
165
|
+
if (value === null || value === undefined) {
|
|
166
|
+
parts.push(`${prefix}${key}: null`);
|
|
167
|
+
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
168
|
+
parts.push(`${prefix}${key}:`);
|
|
169
|
+
parts.push(stringifyYaml(value as Record<string, unknown>, indent + 1));
|
|
170
|
+
} else if (typeof value === 'string') {
|
|
171
|
+
parts.push(`${prefix}${key}: "${value}"`);
|
|
172
|
+
} else if (typeof value === 'boolean' || typeof value === 'number') {
|
|
173
|
+
parts.push(`${prefix}${key}: ${value}`);
|
|
174
|
+
} else {
|
|
175
|
+
parts.push(`${prefix}${key}: ${JSON.stringify(value)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return parts.join('\n');
|
|
180
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { pushSyncUserStories } from "./github-push-sync.js";
|
|
2
|
+
import { GitHubBoardResolverV2 } from "./github-board-resolver-v2.js";
|
|
3
|
+
import { GitHubFieldSync } from "./github-field-sync.js";
|
|
4
|
+
import { updateSpecFrontmatter } from "./github-spec-frontmatter-updater.js";
|
|
5
|
+
import { GitHubGraphQLClient } from "./github-graphql-client.js";
|
|
6
|
+
class GitHubSyncOrchestrator {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run the full sync flow for a spec.
|
|
12
|
+
*/
|
|
13
|
+
async syncSpec(specPath, userStories) {
|
|
14
|
+
const pushResult = await pushSyncUserStories(userStories, {
|
|
15
|
+
owner: this.config.owner,
|
|
16
|
+
repo: this.config.repo,
|
|
17
|
+
token: this.config.token,
|
|
18
|
+
dryRun: this.config.dryRun
|
|
19
|
+
});
|
|
20
|
+
let projectV2Result;
|
|
21
|
+
if (this.config.projectV2Enabled && !this.config.dryRun) {
|
|
22
|
+
projectV2Result = await this.syncProjectV2(pushResult, userStories);
|
|
23
|
+
}
|
|
24
|
+
const frontmatterOptions = {};
|
|
25
|
+
if (projectV2Result) {
|
|
26
|
+
frontmatterOptions.projectV2Id = projectV2Result.projectId;
|
|
27
|
+
frontmatterOptions.projectV2Number = projectV2Result.projectNumber;
|
|
28
|
+
}
|
|
29
|
+
const frontmatterResult = await updateSpecFrontmatter(
|
|
30
|
+
specPath,
|
|
31
|
+
pushResult,
|
|
32
|
+
frontmatterOptions
|
|
33
|
+
);
|
|
34
|
+
return {
|
|
35
|
+
pushResult,
|
|
36
|
+
projectV2Result,
|
|
37
|
+
frontmatterResult
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async syncProjectV2(pushResult, userStories) {
|
|
41
|
+
const graphqlClient = new GitHubGraphQLClient(this.config.token);
|
|
42
|
+
const boardResolver = new GitHubBoardResolverV2(graphqlClient, {
|
|
43
|
+
owner: this.config.owner,
|
|
44
|
+
projectV2Number: this.config.projectV2Number,
|
|
45
|
+
projectV2Id: this.config.projectV2Id
|
|
46
|
+
});
|
|
47
|
+
const project = await boardResolver.findOrCreateProject("SpecWeave Sync Board");
|
|
48
|
+
const nodeIds = pushResult.created.filter((item) => item.issueNodeId).map((item) => item.issueNodeId);
|
|
49
|
+
const itemIds = await boardResolver.addIssuesToProject(project.id, nodeIds);
|
|
50
|
+
const nodeIdToStory = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const created of pushResult.created) {
|
|
52
|
+
if (created.issueNodeId) {
|
|
53
|
+
const story = userStories.find((s) => s.id === created.userStoryId);
|
|
54
|
+
if (story) {
|
|
55
|
+
nodeIdToStory.set(created.issueNodeId, story);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const fieldSync = new GitHubFieldSync(graphqlClient, {
|
|
60
|
+
projectId: project.id,
|
|
61
|
+
statusFieldMapping: this.config.statusFieldMapping,
|
|
62
|
+
priorityFieldMapping: this.config.priorityFieldMapping
|
|
63
|
+
});
|
|
64
|
+
const fieldSyncItems = itemIds.map((itemId, idx) => {
|
|
65
|
+
const nodeId = nodeIds[idx];
|
|
66
|
+
const story = nodeId ? nodeIdToStory.get(nodeId) : void 0;
|
|
67
|
+
return {
|
|
68
|
+
itemId,
|
|
69
|
+
status: story?.status,
|
|
70
|
+
priority: story?.priority
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
const fieldSyncResult = await fieldSync.syncItemFields(fieldSyncItems);
|
|
74
|
+
return {
|
|
75
|
+
projectId: project.id,
|
|
76
|
+
projectNumber: project.number,
|
|
77
|
+
itemIds,
|
|
78
|
+
fieldSyncResult
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
GitHubSyncOrchestrator
|
|
84
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Sync Orchestrator — Wires push sync, Projects V2, and frontmatter updates
|
|
3
|
+
*
|
|
4
|
+
* Composes all sync sub-components into a single flow:
|
|
5
|
+
* 1. Push user stories → GitHub issues
|
|
6
|
+
* 2. (Optional) Add issues to Projects V2 board
|
|
7
|
+
* 3. (Optional) Set Status/Priority fields on V2 items
|
|
8
|
+
* 4. Update spec frontmatter with sync results
|
|
9
|
+
*
|
|
10
|
+
* @module github-sync-orchestrator
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { pushSyncUserStories } from './github-push-sync.js';
|
|
14
|
+
import { GitHubBoardResolverV2 } from './github-board-resolver-v2.js';
|
|
15
|
+
import { GitHubFieldSync } from './github-field-sync.js';
|
|
16
|
+
import { updateSpecFrontmatter } from './github-spec-frontmatter-updater.js';
|
|
17
|
+
import { GitHubGraphQLClient } from './github-graphql-client.js';
|
|
18
|
+
import type { UserStoryForSync, PushSyncResult } from './github-push-sync.js';
|
|
19
|
+
import type { FieldSyncResult } from './github-field-sync.js';
|
|
20
|
+
import type { GitHubSyncMetadata } from '../../../src/core/types/sync-profile.js';
|
|
21
|
+
|
|
22
|
+
export interface SyncOrchestratorConfig {
|
|
23
|
+
owner: string;
|
|
24
|
+
repo: string;
|
|
25
|
+
token?: string;
|
|
26
|
+
dryRun?: boolean;
|
|
27
|
+
projectV2Enabled?: boolean;
|
|
28
|
+
projectV2Number?: number;
|
|
29
|
+
projectV2Id?: string;
|
|
30
|
+
statusFieldMapping?: Record<string, string>;
|
|
31
|
+
priorityFieldMapping?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProjectV2Result {
|
|
35
|
+
projectId: string;
|
|
36
|
+
projectNumber: number;
|
|
37
|
+
itemIds: string[];
|
|
38
|
+
fieldSyncResult: FieldSyncResult;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SyncOrchestratorResult {
|
|
42
|
+
pushResult: PushSyncResult;
|
|
43
|
+
projectV2Result?: ProjectV2Result;
|
|
44
|
+
frontmatterResult: GitHubSyncMetadata;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class GitHubSyncOrchestrator {
|
|
48
|
+
private config: SyncOrchestratorConfig;
|
|
49
|
+
|
|
50
|
+
constructor(config: SyncOrchestratorConfig) {
|
|
51
|
+
this.config = config;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run the full sync flow for a spec.
|
|
56
|
+
*/
|
|
57
|
+
async syncSpec(
|
|
58
|
+
specPath: string,
|
|
59
|
+
userStories: UserStoryForSync[],
|
|
60
|
+
): Promise<SyncOrchestratorResult> {
|
|
61
|
+
// Step 1: Push sync — create/update GitHub issues
|
|
62
|
+
const pushResult = await pushSyncUserStories(userStories, {
|
|
63
|
+
owner: this.config.owner,
|
|
64
|
+
repo: this.config.repo,
|
|
65
|
+
token: this.config.token,
|
|
66
|
+
dryRun: this.config.dryRun,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Step 2: Projects V2 (optional, skip in dry run)
|
|
70
|
+
let projectV2Result: ProjectV2Result | undefined;
|
|
71
|
+
|
|
72
|
+
if (this.config.projectV2Enabled && !this.config.dryRun) {
|
|
73
|
+
projectV2Result = await this.syncProjectV2(pushResult, userStories);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 3: Update spec frontmatter
|
|
77
|
+
const frontmatterOptions: { projectV2Id?: string; projectV2Number?: number } = {};
|
|
78
|
+
if (projectV2Result) {
|
|
79
|
+
frontmatterOptions.projectV2Id = projectV2Result.projectId;
|
|
80
|
+
frontmatterOptions.projectV2Number = projectV2Result.projectNumber;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const frontmatterResult = await updateSpecFrontmatter(
|
|
84
|
+
specPath,
|
|
85
|
+
pushResult,
|
|
86
|
+
frontmatterOptions,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
pushResult,
|
|
91
|
+
projectV2Result,
|
|
92
|
+
frontmatterResult,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async syncProjectV2(
|
|
97
|
+
pushResult: PushSyncResult,
|
|
98
|
+
userStories: UserStoryForSync[],
|
|
99
|
+
): Promise<ProjectV2Result> {
|
|
100
|
+
const graphqlClient = new GitHubGraphQLClient(this.config.token);
|
|
101
|
+
|
|
102
|
+
// Find or create the Projects V2 board
|
|
103
|
+
const boardResolver = new GitHubBoardResolverV2(graphqlClient, {
|
|
104
|
+
owner: this.config.owner,
|
|
105
|
+
projectV2Number: this.config.projectV2Number,
|
|
106
|
+
projectV2Id: this.config.projectV2Id,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const project = await boardResolver.findOrCreateProject('SpecWeave Sync Board');
|
|
110
|
+
|
|
111
|
+
// Collect issue node IDs from created issues
|
|
112
|
+
const nodeIds = pushResult.created
|
|
113
|
+
.filter(item => item.issueNodeId)
|
|
114
|
+
.map(item => item.issueNodeId);
|
|
115
|
+
|
|
116
|
+
// Add issues to project
|
|
117
|
+
const itemIds = await boardResolver.addIssuesToProject(project.id, nodeIds);
|
|
118
|
+
|
|
119
|
+
// Build a map from nodeId → user story for field sync
|
|
120
|
+
const nodeIdToStory = new Map<string, UserStoryForSync>();
|
|
121
|
+
for (const created of pushResult.created) {
|
|
122
|
+
if (created.issueNodeId) {
|
|
123
|
+
const story = userStories.find(s => s.id === created.userStoryId);
|
|
124
|
+
if (story) {
|
|
125
|
+
nodeIdToStory.set(created.issueNodeId, story);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Sync Status and Priority fields
|
|
131
|
+
const fieldSync = new GitHubFieldSync(graphqlClient, {
|
|
132
|
+
projectId: project.id,
|
|
133
|
+
statusFieldMapping: this.config.statusFieldMapping,
|
|
134
|
+
priorityFieldMapping: this.config.priorityFieldMapping,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const fieldSyncItems = itemIds.map((itemId, idx) => {
|
|
138
|
+
const nodeId = nodeIds[idx];
|
|
139
|
+
const story = nodeId ? nodeIdToStory.get(nodeId) : undefined;
|
|
140
|
+
return {
|
|
141
|
+
itemId,
|
|
142
|
+
status: story?.status,
|
|
143
|
+
priority: story?.priority,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const fieldSyncResult = await fieldSync.syncItemFields(fieldSyncItems);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
projectId: project.id,
|
|
151
|
+
projectNumber: project.number,
|
|
152
|
+
itemIds,
|
|
153
|
+
fieldSyncResult,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
|
|
3
|
+
async function autoCloseCompletedUserStories(incrementId, affectedUSIds, specPath, options) {
|
|
4
|
+
const result = { closed: [], skipped: [], errors: [] };
|
|
5
|
+
if (affectedUSIds.length === 0) {
|
|
6
|
+
return result;
|
|
7
|
+
}
|
|
8
|
+
let content;
|
|
9
|
+
try {
|
|
10
|
+
content = await readFile(specPath, "utf-8");
|
|
11
|
+
} catch (err) {
|
|
12
|
+
result.errors.push({
|
|
13
|
+
usId: affectedUSIds[0],
|
|
14
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15
|
+
});
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
const issueLinks = parseIssueLinks(content);
|
|
19
|
+
const repoSlug = `${options.owner}/${options.repo}`;
|
|
20
|
+
const env = options.token ? { GH_TOKEN: options.token } : void 0;
|
|
21
|
+
const execOpts = env ? { env } : {};
|
|
22
|
+
for (const usId of affectedUSIds) {
|
|
23
|
+
const acStates = parseACStatesForUS(content, usId);
|
|
24
|
+
if (acStates.length === 0 || acStates.some((ac) => !ac.completed)) {
|
|
25
|
+
result.skipped.push({ usId, reason: "incomplete-acs" });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const link = issueLinks[usId];
|
|
29
|
+
if (!link) {
|
|
30
|
+
result.skipped.push({ usId, reason: "no-issue-link" });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const issueNum = String(link.issueNumber);
|
|
34
|
+
const viewResult = await execFileNoThrow(
|
|
35
|
+
"gh",
|
|
36
|
+
["issue", "view", issueNum, "--json", "state", "-R", repoSlug],
|
|
37
|
+
execOpts
|
|
38
|
+
);
|
|
39
|
+
if (viewResult.success) {
|
|
40
|
+
try {
|
|
41
|
+
const issueState = JSON.parse(viewResult.stdout);
|
|
42
|
+
if (issueState.state === "CLOSED") {
|
|
43
|
+
result.skipped.push({ usId, reason: "already-closed" });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const commentBody = buildCompletionComment(incrementId, usId, acStates);
|
|
50
|
+
await execFileNoThrow(
|
|
51
|
+
"gh",
|
|
52
|
+
["issue", "comment", issueNum, "--body", commentBody, "-R", repoSlug],
|
|
53
|
+
execOpts
|
|
54
|
+
);
|
|
55
|
+
const closeResult = await execFileNoThrow(
|
|
56
|
+
"gh",
|
|
57
|
+
["issue", "close", issueNum, "-R", repoSlug],
|
|
58
|
+
execOpts
|
|
59
|
+
);
|
|
60
|
+
if (closeResult.success) {
|
|
61
|
+
result.closed.push({ usId, issueNumber: link.issueNumber });
|
|
62
|
+
} else {
|
|
63
|
+
result.errors.push({
|
|
64
|
+
usId,
|
|
65
|
+
error: closeResult.stderr || "Unknown error closing issue"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function buildCompletionComment(incrementId, usId, acStates) {
|
|
72
|
+
const total = acStates.length;
|
|
73
|
+
let comment = `**All acceptance criteria completed** \u2014 ${usId} (Increment ${incrementId})
|
|
74
|
+
|
|
75
|
+
`;
|
|
76
|
+
comment += `**Status**: ${total}/${total} ACs complete (100%)
|
|
77
|
+
|
|
78
|
+
`;
|
|
79
|
+
comment += `**Completed**:
|
|
80
|
+
`;
|
|
81
|
+
for (const ac of acStates) {
|
|
82
|
+
comment += `- [x] **${ac.id}**: ${ac.description}
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
comment += "\n";
|
|
86
|
+
comment += `---
|
|
87
|
+
`;
|
|
88
|
+
comment += `Auto-closed by SpecWeave | ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
89
|
+
`;
|
|
90
|
+
return comment;
|
|
91
|
+
}
|
|
92
|
+
function parseIssueLinks(content) {
|
|
93
|
+
const links = {};
|
|
94
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
95
|
+
if (!fmMatch) return links;
|
|
96
|
+
const frontmatter = fmMatch[1];
|
|
97
|
+
const usBlockMatch = frontmatter.match(/userStories:\s*\n((?:\s{6,}[\s\S]*?)(?=\n\s{0,3}\S|$))/);
|
|
98
|
+
if (!usBlockMatch) return links;
|
|
99
|
+
const usBlock = usBlockMatch[1];
|
|
100
|
+
const usEntries = usBlock.match(/^\s+(US-\d+):\s*\n((?:\s+\w[\s\S]*?)(?=\n\s+US-|\s*$))/gm);
|
|
101
|
+
if (!usEntries) return links;
|
|
102
|
+
for (const entry of usEntries) {
|
|
103
|
+
const idMatch = entry.match(/(US-\d+):/);
|
|
104
|
+
const numMatch = entry.match(/issueNumber:\s*(\d+)/);
|
|
105
|
+
const urlMatch = entry.match(/issueUrl:\s*"([^"]+)"/);
|
|
106
|
+
if (idMatch && numMatch) {
|
|
107
|
+
links[idMatch[1]] = {
|
|
108
|
+
issueNumber: parseInt(numMatch[1], 10),
|
|
109
|
+
issueUrl: urlMatch ? urlMatch[1] : ""
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return links;
|
|
114
|
+
}
|
|
115
|
+
function parseACStatesForUS(content, usId) {
|
|
116
|
+
const states = [];
|
|
117
|
+
const usNum = String(parseInt(usId.replace("US-", ""), 10));
|
|
118
|
+
const acPattern = new RegExp(
|
|
119
|
+
`- \\[([ x])\\] \\*\\*AC-US${usNum}-(\\d+)\\*\\*:\\s*(.+)`,
|
|
120
|
+
"g"
|
|
121
|
+
);
|
|
122
|
+
let match;
|
|
123
|
+
while ((match = acPattern.exec(content)) !== null) {
|
|
124
|
+
states.push({
|
|
125
|
+
id: `AC-US${usNum}-${match[2]}`,
|
|
126
|
+
description: match[3].trim(),
|
|
127
|
+
completed: match[1] === "x"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return states;
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
autoCloseCompletedUserStories
|
|
134
|
+
};
|