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.
Files changed (196) hide show
  1. package/README.md +89 -193
  2. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts +37 -0
  3. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -0
  4. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +176 -0
  5. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -0
  6. package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts +36 -0
  7. package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts.map +1 -0
  8. package/dist/plugins/specweave-github/lib/github-batch-sync.js +115 -0
  9. package/dist/plugins/specweave-github/lib/github-batch-sync.js.map +1 -0
  10. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts +37 -0
  11. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts.map +1 -0
  12. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js +56 -0
  13. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js.map +1 -0
  14. package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts +68 -0
  15. package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts.map +1 -0
  16. package/dist/plugins/specweave-github/lib/github-conflict-resolver.js +102 -0
  17. package/dist/plugins/specweave-github/lib/github-conflict-resolver.js.map +1 -0
  18. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts +64 -0
  19. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts.map +1 -0
  20. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +162 -0
  21. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -0
  22. package/dist/plugins/specweave-github/lib/github-field-sync.d.ts +50 -0
  23. package/dist/plugins/specweave-github/lib/github-field-sync.d.ts.map +1 -0
  24. package/dist/plugins/specweave-github/lib/github-field-sync.js +107 -0
  25. package/dist/plugins/specweave-github/lib/github-field-sync.js.map +1 -0
  26. package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +53 -0
  27. package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -0
  28. package/dist/plugins/specweave-github/lib/github-graphql-client.js +138 -0
  29. package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -0
  30. package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts +40 -0
  31. package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts.map +1 -0
  32. package/dist/plugins/specweave-github/lib/github-issue-body-generator.js +50 -0
  33. package/dist/plugins/specweave-github/lib/github-issue-body-generator.js.map +1 -0
  34. package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts +30 -0
  35. package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts.map +1 -0
  36. package/dist/plugins/specweave-github/lib/github-issue-body-parser.js +75 -0
  37. package/dist/plugins/specweave-github/lib/github-issue-body-parser.js.map +1 -0
  38. package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts +94 -0
  39. package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts.map +1 -0
  40. package/dist/plugins/specweave-github/lib/github-pull-sync.js +232 -0
  41. package/dist/plugins/specweave-github/lib/github-pull-sync.js.map +1 -0
  42. package/dist/plugins/specweave-github/lib/github-push-sync.d.ts +50 -0
  43. package/dist/plugins/specweave-github/lib/github-push-sync.d.ts.map +1 -0
  44. package/dist/plugins/specweave-github/lib/github-push-sync.js +114 -0
  45. package/dist/plugins/specweave-github/lib/github-push-sync.js.map +1 -0
  46. package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts +53 -0
  47. package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts.map +1 -0
  48. package/dist/plugins/specweave-github/lib/github-rate-limiter.js +109 -0
  49. package/dist/plugins/specweave-github/lib/github-rate-limiter.js.map +1 -0
  50. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts +21 -0
  51. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts.map +1 -0
  52. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +161 -0
  53. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -0
  54. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +46 -0
  55. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -0
  56. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +99 -0
  57. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -0
  58. package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts +43 -0
  59. package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -0
  60. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +153 -0
  61. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -0
  62. package/dist/plugins/specweave-github/lib/index.d.ts +1 -4
  63. package/dist/plugins/specweave-github/lib/index.d.ts.map +1 -1
  64. package/dist/plugins/specweave-github/lib/index.js +1 -4
  65. package/dist/plugins/specweave-github/lib/index.js.map +1 -1
  66. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +7 -0
  67. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts.map +1 -0
  68. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js +15 -0
  69. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js.map +1 -0
  70. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts +10 -0
  71. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts.map +1 -0
  72. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js +36 -0
  73. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js.map +1 -0
  74. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts +25 -0
  75. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts.map +1 -0
  76. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js +57 -0
  77. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js.map +1 -0
  78. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts +7 -0
  79. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts.map +1 -0
  80. package/dist/plugins/specweave-testing/lib/playwright-routing.js +17 -0
  81. package/dist/plugins/specweave-testing/lib/playwright-routing.js.map +1 -0
  82. package/dist/src/cli/commands/auto.d.ts.map +1 -1
  83. package/dist/src/cli/commands/auto.js +1 -2
  84. package/dist/src/cli/commands/auto.js.map +1 -1
  85. package/dist/src/cli/commands/cancel-auto.js +1 -2
  86. package/dist/src/cli/commands/cancel-auto.js.map +1 -1
  87. package/dist/src/cli/commands/living-docs.js +2 -2
  88. package/dist/src/cli/commands/living-docs.js.map +1 -1
  89. package/dist/src/cli/commands/update.d.ts.map +1 -1
  90. package/dist/src/cli/commands/update.js +1 -2
  91. package/dist/src/cli/commands/update.js.map +1 -1
  92. package/dist/src/core/config/types.d.ts +8 -0
  93. package/dist/src/core/config/types.d.ts.map +1 -1
  94. package/dist/src/core/config/types.js +3 -0
  95. package/dist/src/core/config/types.js.map +1 -1
  96. package/dist/src/core/types/sync-profile.d.ts +72 -0
  97. package/dist/src/core/types/sync-profile.d.ts.map +1 -1
  98. package/dist/src/core/types/sync-profile.js +6 -0
  99. package/dist/src/core/types/sync-profile.js.map +1 -1
  100. package/package.json +2 -2
  101. package/plugins/specweave/hooks/hooks.json +2 -2
  102. package/plugins/specweave/hooks/startup-health-check.sh +1 -1
  103. package/plugins/specweave/hooks/stop-auto-v5.sh +166 -0
  104. package/plugins/specweave/hooks/user-prompt-submit.sh +10 -0
  105. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +21 -1
  106. package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -1
  107. package/plugins/specweave/skills/auto/SKILL.md +71 -251
  108. package/plugins/specweave/skills/team-build/SKILL.md +370 -0
  109. package/plugins/specweave/skills/team-merge/SKILL.md +123 -0
  110. package/plugins/specweave/skills/team-orchestrate/SKILL.md +800 -0
  111. package/plugins/specweave/skills/team-status/SKILL.md +89 -0
  112. package/plugins/specweave-github/MULTI-PROJECT-SYNC-ARCHITECTURE.md +94 -8
  113. package/plugins/specweave-github/commands/sync.md +17 -3
  114. package/plugins/specweave-github/hooks/github-ac-sync-handler.sh +255 -0
  115. package/plugins/specweave-github/hooks/github-auto-create-handler.sh +455 -0
  116. package/plugins/specweave-github/lib/github-ac-comment-poster.js +150 -0
  117. package/plugins/specweave-github/lib/github-ac-comment-poster.ts +245 -0
  118. package/plugins/specweave-github/lib/github-batch-sync.js +93 -0
  119. package/plugins/specweave-github/lib/github-batch-sync.ts +152 -0
  120. package/plugins/specweave-github/lib/github-board-resolver-v2.js +47 -0
  121. package/plugins/specweave-github/lib/github-board-resolver-v2.ts +73 -0
  122. package/plugins/specweave-github/lib/github-conflict-resolver.js +90 -0
  123. package/plugins/specweave-github/lib/github-conflict-resolver.ts +154 -0
  124. package/plugins/specweave-github/lib/github-cross-repo-sync.js +168 -0
  125. package/plugins/specweave-github/lib/github-cross-repo-sync.ts +252 -0
  126. package/plugins/specweave-github/lib/github-field-sync.js +116 -0
  127. package/plugins/specweave-github/lib/github-field-sync.ts +165 -0
  128. package/plugins/specweave-github/lib/github-graphql-client.js +129 -0
  129. package/plugins/specweave-github/lib/github-graphql-client.ts +181 -0
  130. package/plugins/specweave-github/lib/github-issue-body-generator.js +30 -0
  131. package/plugins/specweave-github/lib/github-issue-body-generator.ts +76 -0
  132. package/plugins/specweave-github/lib/github-issue-body-parser.js +55 -0
  133. package/plugins/specweave-github/lib/github-issue-body-parser.ts +92 -0
  134. package/plugins/specweave-github/lib/github-pull-sync.js +185 -0
  135. package/plugins/specweave-github/lib/github-pull-sync.ts +343 -0
  136. package/plugins/specweave-github/lib/github-push-sync.js +119 -0
  137. package/plugins/specweave-github/lib/github-push-sync.ts +174 -0
  138. package/plugins/specweave-github/lib/github-rate-limiter.js +96 -0
  139. package/plugins/specweave-github/lib/github-rate-limiter.ts +143 -0
  140. package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +117 -0
  141. package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +180 -0
  142. package/plugins/specweave-github/lib/github-sync-orchestrator.js +84 -0
  143. package/plugins/specweave-github/lib/github-sync-orchestrator.ts +156 -0
  144. package/plugins/specweave-github/lib/github-us-auto-closer.js +134 -0
  145. package/plugins/specweave-github/lib/github-us-auto-closer.ts +226 -0
  146. package/plugins/specweave-github/lib/index.js +1 -7
  147. package/plugins/specweave-github/lib/index.ts +1 -4
  148. package/plugins/specweave-github/skills/github-sync/SKILL.md +76 -4
  149. package/plugins/specweave-testing/commands/e2e-setup.md +18 -0
  150. package/plugins/specweave-testing/commands/ui-automate.md +2 -0
  151. package/plugins/specweave-testing/commands/ui-inspect.md +8 -0
  152. package/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +6 -0
  153. package/plugins/specweave-testing/lib/playwright-ci-defaults.js +14 -0
  154. package/plugins/specweave-testing/lib/playwright-ci-defaults.ts +24 -0
  155. package/plugins/specweave-testing/lib/playwright-cli-detector.js +33 -0
  156. package/plugins/specweave-testing/lib/playwright-cli-detector.ts +48 -0
  157. package/plugins/specweave-testing/lib/playwright-cli-runner.js +58 -0
  158. package/plugins/specweave-testing/lib/playwright-cli-runner.ts +80 -0
  159. package/plugins/specweave-testing/lib/playwright-routing.js +16 -0
  160. package/plugins/specweave-testing/lib/playwright-routing.ts +38 -0
  161. package/plugins/specweave-testing/skills/e2e-testing/SKILL.md +38 -0
  162. package/src/templates/CLAUDE.md.template +7 -0
  163. package/src/templates/config.json.template +9 -1
  164. package/dist/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
  165. package/dist/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
  166. package/dist/plugins/specweave-github/lib/subtask-sync.js +0 -147
  167. package/dist/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
  168. package/dist/plugins/specweave-github/lib/task-parser.d.ts +0 -37
  169. package/dist/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
  170. package/dist/plugins/specweave-github/lib/task-parser.js +0 -211
  171. package/dist/plugins/specweave-github/lib/task-parser.js.map +0 -1
  172. package/dist/plugins/specweave-github/lib/task-sync.d.ts +0 -56
  173. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
  174. package/dist/plugins/specweave-github/lib/task-sync.js +0 -375
  175. package/dist/plugins/specweave-github/lib/task-sync.js.map +0 -1
  176. package/plugins/specweave/hooks/validate-completion-conditions.sh +0 -474
  177. package/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
  178. package/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
  179. package/plugins/specweave-github/lib/subtask-sync.js +0 -154
  180. package/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
  181. package/plugins/specweave-github/lib/subtask-sync.ts +0 -225
  182. package/plugins/specweave-github/lib/task-parser.d.js +0 -0
  183. package/plugins/specweave-github/lib/task-parser.d.ts +0 -37
  184. package/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
  185. package/plugins/specweave-github/lib/task-parser.js +0 -195
  186. package/plugins/specweave-github/lib/task-parser.js.map +0 -1
  187. package/plugins/specweave-github/lib/task-parser.ts +0 -246
  188. package/plugins/specweave-github/lib/task-sync.d.js +0 -0
  189. package/plugins/specweave-github/lib/task-sync.d.ts +0 -51
  190. package/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
  191. package/plugins/specweave-github/lib/task-sync.js +0 -415
  192. package/plugins/specweave-github/lib/task-sync.js.map +0 -1
  193. package/plugins/specweave-github/lib/task-sync.ts +0 -451
  194. package/plugins/specweave-github/skills/github-issue-tracker/SKILL.md +0 -496
  195. /package/plugins/specweave/hooks/{stop-auto.sh → _archive/stop-auto-v4-legacy.sh} +0 -0
  196. /package/plugins/{specweave-github/lib/subtask-sync.d.js → specweave-testing/lib/playwright-ci-defaults.d.js} +0 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * GitHub Issue Body Parser
3
+ *
4
+ * Parses AC checkbox state and sync metadata from a GitHub issue body.
5
+ * Used by the pull sync to detect changes made on GitHub.
6
+ *
7
+ * @module github-issue-body-parser
8
+ */
9
+
10
+ export interface ParsedIssueBody {
11
+ /** AC ID → checked state and description */
12
+ acceptanceCriteria: Record<string, { checked: boolean; description: string }>;
13
+ /** Sync marker metadata (if present) */
14
+ syncMarker?: { specId: string; userStoryId: string };
15
+ }
16
+
17
+ // Regex to match AC checkboxes: - [x] **AC-US1-01**: Description
18
+ const AC_CHECKBOX_RE = /^-\s+\[([ xX])\]\s+\*\*(?<acId>AC-[A-Z0-9]+-\d+)\*\*:\s*(?<desc>.+)$/;
19
+
20
+ // Regex to match sync footer marker
21
+ const SYNC_MARKER_RE = /<!--\s*specweave:sync\s+(?:spec=(?<specId>[^\s]+)\s+)?us=(?<usId>[^\s]+)\s*-->/;
22
+
23
+ /**
24
+ * Parse a GitHub issue body to extract AC checkbox states and sync metadata.
25
+ *
26
+ * Strategy:
27
+ * 1. Try to find ACs within "## Acceptance Criteria" section
28
+ * 2. Fallback: scan entire body for AC-pattern checkboxes
29
+ * 3. Parse sync footer marker if present
30
+ */
31
+ export function parseIssueBody(body: string): ParsedIssueBody {
32
+ const result: ParsedIssueBody = {
33
+ acceptanceCriteria: {},
34
+ };
35
+
36
+ if (!body || !body.trim()) {
37
+ return result;
38
+ }
39
+
40
+ const lines = body.split('\n');
41
+
42
+ // Try to find the "## Acceptance Criteria" section
43
+ let inAcSection = false;
44
+ let foundAcSection = false;
45
+
46
+ for (const line of lines) {
47
+ // Detect section boundaries
48
+ if (/^##\s+Acceptance Criteria/i.test(line)) {
49
+ inAcSection = true;
50
+ foundAcSection = true;
51
+ continue;
52
+ }
53
+ if (inAcSection && /^##\s+/.test(line)) {
54
+ inAcSection = false;
55
+ continue;
56
+ }
57
+
58
+ if (inAcSection) {
59
+ const match = line.match(AC_CHECKBOX_RE);
60
+ if (match?.groups) {
61
+ result.acceptanceCriteria[match.groups.acId] = {
62
+ checked: match[1] !== ' ',
63
+ description: match.groups.desc.trim(),
64
+ };
65
+ }
66
+ }
67
+ }
68
+
69
+ // Fallback: if no AC section found, scan the entire body
70
+ if (!foundAcSection) {
71
+ for (const line of lines) {
72
+ const match = line.match(AC_CHECKBOX_RE);
73
+ if (match?.groups) {
74
+ result.acceptanceCriteria[match.groups.acId] = {
75
+ checked: match[1] !== ' ',
76
+ description: match.groups.desc.trim(),
77
+ };
78
+ }
79
+ }
80
+ }
81
+
82
+ // Parse sync footer marker
83
+ const syncMatch = body.match(SYNC_MARKER_RE);
84
+ if (syncMatch?.groups?.usId) {
85
+ result.syncMarker = {
86
+ specId: syncMatch.groups.specId || '',
87
+ userStoryId: syncMatch.groups.usId,
88
+ };
89
+ }
90
+
91
+ return result;
92
+ }
@@ -0,0 +1,185 @@
1
+ import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
2
+ import { parseIssueBody } from "./github-issue-body-parser.js";
3
+ async function pullSyncFromGitHub(userStoryLinks, specAcceptanceCriteria, options) {
4
+ const result = { changes: [], conflicts: [], errors: [] };
5
+ const entries = Object.entries(userStoryLinks);
6
+ if (entries.length === 0) {
7
+ return result;
8
+ }
9
+ const env = getEnv(options.token);
10
+ const repoSlug = `${options.owner}/${options.repo}`;
11
+ for (const [usId, link] of entries) {
12
+ try {
13
+ const issueData = await fetchIssue(link.issueNumber, repoSlug, env);
14
+ const parsed = parseIssueBody(issueData.body || "");
15
+ const specACs = specAcceptanceCriteria[usId] || [];
16
+ compareACStates(usId, specACs, parsed.acceptanceCriteria, options.dryRun ?? false, result);
17
+ compareIssueState(usId, issueData.state, specACs, result, options.dryRun ?? false);
18
+ } catch (err) {
19
+ result.errors.push({
20
+ userStoryId: usId,
21
+ error: err instanceof Error ? err.message : String(err)
22
+ });
23
+ }
24
+ }
25
+ return result;
26
+ }
27
+ function compareACStates(usId, specACs, githubACs, dryRun, result) {
28
+ for (const specAC of specACs) {
29
+ const ghAC = githubACs[specAC.id];
30
+ if (!ghAC) continue;
31
+ const specValue = String(specAC.completed);
32
+ const githubValue = String(ghAC.checked);
33
+ if (specValue !== githubValue) {
34
+ if (specAC.completed && !ghAC.checked) {
35
+ result.conflicts.push({
36
+ userStoryId: usId,
37
+ field: specAC.id,
38
+ specValue,
39
+ githubValue
40
+ });
41
+ } else {
42
+ result.changes.push({
43
+ userStoryId: usId,
44
+ field: specAC.id,
45
+ specValue,
46
+ githubValue,
47
+ applied: !dryRun
48
+ });
49
+ }
50
+ }
51
+ }
52
+ }
53
+ function compareIssueState(usId, githubState, specACs, result, dryRun) {
54
+ const normalizedState = githubState.toLowerCase() === "closed" ? "closed" : "open";
55
+ const allDone = specACs.length > 0 && specACs.every((ac) => ac.completed);
56
+ const specState = allDone ? "closed" : "open";
57
+ if (specState !== normalizedState) {
58
+ result.changes.push({
59
+ userStoryId: usId,
60
+ field: "status",
61
+ specValue: specState,
62
+ githubValue: normalizedState,
63
+ applied: !dryRun
64
+ });
65
+ }
66
+ }
67
+ async function fetchIssue(issueNumber, repoSlug, env) {
68
+ const res = await execFileNoThrow("gh", [
69
+ "issue",
70
+ "view",
71
+ String(issueNumber),
72
+ "--repo",
73
+ repoSlug,
74
+ "--json",
75
+ "title,body,state,labels"
76
+ ], { env });
77
+ if (!res.success) {
78
+ throw new Error(res.stderr || "Failed to fetch issue");
79
+ }
80
+ return JSON.parse(res.stdout);
81
+ }
82
+ function getEnv(token) {
83
+ if (token) {
84
+ return { ...process.env, GH_TOKEN: token };
85
+ }
86
+ return process.env;
87
+ }
88
+ async function pullSyncMultiRepo(repos, userStoryLinks, specAcceptanceCriteria, options) {
89
+ const result = {
90
+ changes: [],
91
+ conflicts: [],
92
+ disagreements: [],
93
+ errors: []
94
+ };
95
+ if (repos.length === 0) {
96
+ return result;
97
+ }
98
+ const env = getEnv(options.token);
99
+ const perRepoACStates = {};
100
+ const failedRepos = /* @__PURE__ */ new Set();
101
+ for (const repo of repos) {
102
+ const repoSlug = `${repo.owner}/${repo.repo}`;
103
+ for (const usId of repo.relevantStories) {
104
+ const links = userStoryLinks[usId];
105
+ if (!links || !links[repoSlug]) continue;
106
+ const issueNumber = links[repoSlug].issueNumber;
107
+ try {
108
+ const issueData = await fetchIssue(issueNumber, repoSlug, env);
109
+ const parsed = parseIssueBody(issueData.body || "");
110
+ if (!perRepoACStates[usId]) {
111
+ perRepoACStates[usId] = {};
112
+ }
113
+ const specACs = specAcceptanceCriteria[usId] || [];
114
+ for (const specAC of specACs) {
115
+ const ghAC = parsed.acceptanceCriteria[specAC.id];
116
+ if (!ghAC) continue;
117
+ if (!perRepoACStates[usId][specAC.id]) {
118
+ perRepoACStates[usId][specAC.id] = {};
119
+ }
120
+ perRepoACStates[usId][specAC.id][repoSlug] = ghAC.checked;
121
+ }
122
+ } catch (err) {
123
+ failedRepos.add(repoSlug);
124
+ result.errors.push({
125
+ userStoryId: usId,
126
+ repo: repoSlug,
127
+ error: err instanceof Error ? err.message : String(err)
128
+ });
129
+ }
130
+ }
131
+ }
132
+ for (const [usId, acMap] of Object.entries(perRepoACStates)) {
133
+ const specACs = specAcceptanceCriteria[usId] || [];
134
+ const usLinks = userStoryLinks[usId] || {};
135
+ const expectedRepos = Object.keys(usLinks).filter((r) => !failedRepos.has(r));
136
+ for (const specAC of specACs) {
137
+ const repoStates = acMap[specAC.id];
138
+ if (!repoStates) continue;
139
+ const validStates = Object.entries(repoStates).filter(
140
+ ([repo]) => !failedRepos.has(repo)
141
+ );
142
+ if (validStates.length === 0) continue;
143
+ const allChecked = validStates.every(([, checked]) => checked);
144
+ const allUnchecked = validStates.every(([, checked]) => !checked);
145
+ const isShared = expectedRepos.length > 1;
146
+ if (isShared) {
147
+ if (allChecked && !specAC.completed) {
148
+ result.changes.push({
149
+ userStoryId: usId,
150
+ field: specAC.id,
151
+ specValue: String(specAC.completed),
152
+ githubValue: "true",
153
+ applied: true
154
+ });
155
+ } else if (!allChecked && !allUnchecked && !specAC.completed) {
156
+ const stateMap = {};
157
+ for (const [repo, checked] of validStates) {
158
+ stateMap[repo] = checked;
159
+ }
160
+ result.disagreements.push({
161
+ userStoryId: usId,
162
+ field: specAC.id,
163
+ repoStates: stateMap
164
+ });
165
+ }
166
+ } else {
167
+ const [, checked] = validStates[0];
168
+ if (checked && !specAC.completed) {
169
+ result.changes.push({
170
+ userStoryId: usId,
171
+ field: specAC.id,
172
+ specValue: String(specAC.completed),
173
+ githubValue: "true",
174
+ applied: true
175
+ });
176
+ }
177
+ }
178
+ }
179
+ }
180
+ return result;
181
+ }
182
+ export {
183
+ pullSyncFromGitHub,
184
+ pullSyncMultiRepo
185
+ };
@@ -0,0 +1,343 @@
1
+ /**
2
+ * GitHub Pull Sync — GitHub Issues → Spec
3
+ *
4
+ * Fetches GitHub issue state and compares with spec acceptance criteria
5
+ * to detect changes, conflicts, and apply updates.
6
+ *
7
+ * @module github-pull-sync
8
+ */
9
+
10
+ import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
11
+ import { parseIssueBody } from './github-issue-body-parser.js';
12
+ import type { GitHubUserStoryLink } from '../../../src/core/types/sync-profile.js';
13
+
14
+ export interface PullSyncOptions {
15
+ owner: string;
16
+ repo: string;
17
+ token?: string;
18
+ dryRun?: boolean;
19
+ }
20
+
21
+ export interface PullSyncChange {
22
+ userStoryId: string;
23
+ field: string;
24
+ specValue: string;
25
+ githubValue: string;
26
+ applied: boolean;
27
+ }
28
+
29
+ export interface PullSyncConflict {
30
+ userStoryId: string;
31
+ field: string;
32
+ specValue: string;
33
+ githubValue: string;
34
+ }
35
+
36
+ export interface PullSyncResult {
37
+ changes: PullSyncChange[];
38
+ conflicts: PullSyncConflict[];
39
+ errors: Array<{ userStoryId: string; error: string }>;
40
+ }
41
+
42
+ /**
43
+ * Pull sync from GitHub — fetch issue state and compare with spec ACs.
44
+ *
45
+ * For each linked user story:
46
+ * 1. Fetch issue via `gh issue view`
47
+ * 2. Parse body to extract AC checkbox states
48
+ * 3. Compare with spec AC states
49
+ * 4. Report changes and conflicts
50
+ */
51
+ export async function pullSyncFromGitHub(
52
+ userStoryLinks: Record<string, GitHubUserStoryLink>,
53
+ specAcceptanceCriteria: Record<string, Array<{ id: string; completed: boolean }>>,
54
+ options: PullSyncOptions,
55
+ ): Promise<PullSyncResult> {
56
+ const result: PullSyncResult = { changes: [], conflicts: [], errors: [] };
57
+
58
+ const entries = Object.entries(userStoryLinks);
59
+ if (entries.length === 0) {
60
+ return result;
61
+ }
62
+
63
+ const env = getEnv(options.token);
64
+ const repoSlug = `${options.owner}/${options.repo}`;
65
+
66
+ for (const [usId, link] of entries) {
67
+ try {
68
+ const issueData = await fetchIssue(link.issueNumber, repoSlug, env);
69
+ const parsed = parseIssueBody(issueData.body || '');
70
+ const specACs = specAcceptanceCriteria[usId] || [];
71
+
72
+ // Compare AC checkbox states
73
+ compareACStates(usId, specACs, parsed.acceptanceCriteria, options.dryRun ?? false, result);
74
+
75
+ // Compare issue state (open/closed)
76
+ compareIssueState(usId, issueData.state, specACs, result, options.dryRun ?? false);
77
+ } catch (err) {
78
+ result.errors.push({
79
+ userStoryId: usId,
80
+ error: err instanceof Error ? err.message : String(err),
81
+ });
82
+ }
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ function compareACStates(
89
+ usId: string,
90
+ specACs: Array<{ id: string; completed: boolean }>,
91
+ githubACs: Record<string, { checked: boolean; description: string }>,
92
+ dryRun: boolean,
93
+ result: PullSyncResult,
94
+ ): void {
95
+ for (const specAC of specACs) {
96
+ const ghAC = githubACs[specAC.id];
97
+ if (!ghAC) continue;
98
+
99
+ const specValue = String(specAC.completed);
100
+ const githubValue = String(ghAC.checked);
101
+
102
+ if (specValue !== githubValue) {
103
+ // Both changed from a baseline? That's a conflict.
104
+ // Simple heuristic: if spec says true and GitHub says false, it's a conflict
105
+ // (someone completed on spec side, but GitHub unchecked it)
106
+ if (specAC.completed && !ghAC.checked) {
107
+ result.conflicts.push({
108
+ userStoryId: usId,
109
+ field: specAC.id,
110
+ specValue,
111
+ githubValue,
112
+ });
113
+ } else {
114
+ // GitHub is ahead — record as a change
115
+ result.changes.push({
116
+ userStoryId: usId,
117
+ field: specAC.id,
118
+ specValue,
119
+ githubValue,
120
+ applied: !dryRun,
121
+ });
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ function compareIssueState(
128
+ usId: string,
129
+ githubState: string,
130
+ specACs: Array<{ id: string; completed: boolean }>,
131
+ result: PullSyncResult,
132
+ dryRun: boolean,
133
+ ): void {
134
+ // Normalize GitHub state
135
+ const normalizedState = githubState.toLowerCase() === 'closed' ? 'closed' : 'open';
136
+
137
+ // Check if spec is effectively "complete" (all ACs done) or "open"
138
+ const allDone = specACs.length > 0 && specACs.every(ac => ac.completed);
139
+ const specState = allDone ? 'closed' : 'open';
140
+
141
+ if (specState !== normalizedState) {
142
+ result.changes.push({
143
+ userStoryId: usId,
144
+ field: 'status',
145
+ specValue: specState,
146
+ githubValue: normalizedState,
147
+ applied: !dryRun,
148
+ });
149
+ }
150
+ }
151
+
152
+ async function fetchIssue(
153
+ issueNumber: number,
154
+ repoSlug: string,
155
+ env: NodeJS.ProcessEnv,
156
+ ): Promise<{ title: string; body: string; state: string; labels: Array<{ name: string }> }> {
157
+ const res = await execFileNoThrow('gh', [
158
+ 'issue', 'view', String(issueNumber),
159
+ '--repo', repoSlug,
160
+ '--json', 'title,body,state,labels',
161
+ ], { env });
162
+
163
+ if (!res.success) {
164
+ throw new Error(res.stderr || 'Failed to fetch issue');
165
+ }
166
+
167
+ return JSON.parse(res.stdout);
168
+ }
169
+
170
+ function getEnv(token?: string): NodeJS.ProcessEnv {
171
+ if (token) {
172
+ return { ...process.env, GH_TOKEN: token };
173
+ }
174
+ return process.env;
175
+ }
176
+
177
+ // ============================================================================
178
+ // Multi-Repo Pull Sync (v1.0.236+)
179
+ // ============================================================================
180
+
181
+ export interface MultiRepoPullSyncOptions extends PullSyncOptions {}
182
+
183
+ export interface MultiRepoDisagreement {
184
+ userStoryId: string;
185
+ field: string;
186
+ repoStates: Record<string, boolean>;
187
+ }
188
+
189
+ export interface MultiRepoPullSyncResult {
190
+ changes: PullSyncChange[];
191
+ conflicts: PullSyncConflict[];
192
+ disagreements: MultiRepoDisagreement[];
193
+ errors: Array<{ userStoryId?: string; repo?: string; error: string }>;
194
+ }
195
+
196
+ interface RepoConfig {
197
+ owner: string;
198
+ repo: string;
199
+ relevantStories: string[];
200
+ }
201
+
202
+ interface MultiRepoUserStoryLinks {
203
+ [usId: string]: Record<string, { issueNumber: number }>;
204
+ }
205
+
206
+ /**
207
+ * Pull sync from multiple repos with all-repos-must-agree consensus.
208
+ *
209
+ * For shared user stories (appearing in 2+ repos):
210
+ * - AC is marked done ONLY if ALL repos agree it is done
211
+ * - Disagreements are recorded separately
212
+ *
213
+ * For repo-specific user stories (1 repo):
214
+ * - Standard single-repo logic applies
215
+ *
216
+ * Repo errors are non-blocking: erroring repos are excluded from consensus.
217
+ */
218
+ export async function pullSyncMultiRepo(
219
+ repos: RepoConfig[],
220
+ userStoryLinks: MultiRepoUserStoryLinks,
221
+ specAcceptanceCriteria: Record<string, Array<{ id: string; completed: boolean }>>,
222
+ options: MultiRepoPullSyncOptions,
223
+ ): Promise<MultiRepoPullSyncResult> {
224
+ const result: MultiRepoPullSyncResult = {
225
+ changes: [],
226
+ conflicts: [],
227
+ disagreements: [],
228
+ errors: [],
229
+ };
230
+
231
+ if (repos.length === 0) {
232
+ return result;
233
+ }
234
+
235
+ const env = getEnv(options.token);
236
+
237
+ // Step 1: Fetch per-repo AC states
238
+ // Map: usId → acId → repoSlug → checked
239
+ const perRepoACStates: Record<string, Record<string, Record<string, boolean>>> = {};
240
+ const failedRepos = new Set<string>();
241
+
242
+ for (const repo of repos) {
243
+ const repoSlug = `${repo.owner}/${repo.repo}`;
244
+
245
+ for (const usId of repo.relevantStories) {
246
+ const links = userStoryLinks[usId];
247
+ if (!links || !links[repoSlug]) continue;
248
+
249
+ const issueNumber = links[repoSlug].issueNumber;
250
+
251
+ try {
252
+ const issueData = await fetchIssue(issueNumber, repoSlug, env);
253
+ const parsed = parseIssueBody(issueData.body || '');
254
+
255
+ // Record AC states for this repo
256
+ if (!perRepoACStates[usId]) {
257
+ perRepoACStates[usId] = {};
258
+ }
259
+
260
+ const specACs = specAcceptanceCriteria[usId] || [];
261
+ for (const specAC of specACs) {
262
+ const ghAC = parsed.acceptanceCriteria[specAC.id];
263
+ if (!ghAC) continue;
264
+
265
+ if (!perRepoACStates[usId][specAC.id]) {
266
+ perRepoACStates[usId][specAC.id] = {};
267
+ }
268
+ perRepoACStates[usId][specAC.id][repoSlug] = ghAC.checked;
269
+ }
270
+ } catch (err) {
271
+ failedRepos.add(repoSlug);
272
+ result.errors.push({
273
+ userStoryId: usId,
274
+ repo: repoSlug,
275
+ error: err instanceof Error ? err.message : String(err),
276
+ });
277
+ }
278
+ }
279
+ }
280
+
281
+ // Step 2: Build consensus per AC
282
+ for (const [usId, acMap] of Object.entries(perRepoACStates)) {
283
+ const specACs = specAcceptanceCriteria[usId] || [];
284
+
285
+ // Determine how many repos this US should appear in (excluding failed)
286
+ const usLinks = userStoryLinks[usId] || {};
287
+ const expectedRepos = Object.keys(usLinks).filter(r => !failedRepos.has(r));
288
+
289
+ for (const specAC of specACs) {
290
+ const repoStates = acMap[specAC.id];
291
+ if (!repoStates) continue;
292
+
293
+ // Filter out failed repos
294
+ const validStates = Object.entries(repoStates).filter(
295
+ ([repo]) => !failedRepos.has(repo),
296
+ );
297
+
298
+ if (validStates.length === 0) continue;
299
+
300
+ const allChecked = validStates.every(([, checked]) => checked);
301
+ const allUnchecked = validStates.every(([, checked]) => !checked);
302
+ const isShared = expectedRepos.length > 1;
303
+
304
+ if (isShared) {
305
+ // All-repos-must-agree consensus
306
+ if (allChecked && !specAC.completed) {
307
+ result.changes.push({
308
+ userStoryId: usId,
309
+ field: specAC.id,
310
+ specValue: String(specAC.completed),
311
+ githubValue: 'true',
312
+ applied: true,
313
+ });
314
+ } else if (!allChecked && !allUnchecked && !specAC.completed) {
315
+ // Disagreement
316
+ const stateMap: Record<string, boolean> = {};
317
+ for (const [repo, checked] of validStates) {
318
+ stateMap[repo] = checked;
319
+ }
320
+ result.disagreements.push({
321
+ userStoryId: usId,
322
+ field: specAC.id,
323
+ repoStates: stateMap,
324
+ });
325
+ }
326
+ } else {
327
+ // Single-repo: use directly
328
+ const [, checked] = validStates[0];
329
+ if (checked && !specAC.completed) {
330
+ result.changes.push({
331
+ userStoryId: usId,
332
+ field: specAC.id,
333
+ specValue: String(specAC.completed),
334
+ githubValue: 'true',
335
+ applied: true,
336
+ });
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ return result;
343
+ }