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,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
|
+
}
|