sequant 1.20.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.claude-plugin/marketplace.json +2 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +36 -15
  4. package/dist/bin/cli.js +25 -2
  5. package/dist/src/commands/doctor.js +42 -9
  6. package/dist/src/commands/init.d.ts +1 -0
  7. package/dist/src/commands/init.js +52 -0
  8. package/dist/src/commands/logs.d.ts +1 -0
  9. package/dist/src/commands/logs.js +18 -2
  10. package/dist/src/commands/run.d.ts +7 -0
  11. package/dist/src/commands/run.js +235 -68
  12. package/dist/src/commands/serve.d.ts +13 -0
  13. package/dist/src/commands/serve.js +131 -0
  14. package/dist/src/commands/stats.d.ts +1 -0
  15. package/dist/src/commands/stats.js +185 -26
  16. package/dist/src/commands/status.d.ts +2 -0
  17. package/dist/src/commands/status.js +99 -50
  18. package/dist/src/index.d.ts +2 -2
  19. package/dist/src/index.js +4 -1
  20. package/dist/src/lib/ac-parser.d.ts +2 -0
  21. package/dist/src/lib/ac-parser.js +12 -2
  22. package/dist/src/lib/assess-comment-parser.d.ts +137 -0
  23. package/dist/src/lib/assess-comment-parser.js +344 -0
  24. package/dist/src/lib/ci/config.d.ts +22 -0
  25. package/dist/src/lib/ci/config.js +134 -0
  26. package/dist/src/lib/ci/index.d.ts +12 -0
  27. package/dist/src/lib/ci/index.js +10 -0
  28. package/dist/src/lib/ci/inputs.d.ts +29 -0
  29. package/dist/src/lib/ci/inputs.js +103 -0
  30. package/dist/src/lib/ci/labels.d.ts +34 -0
  31. package/dist/src/lib/ci/labels.js +101 -0
  32. package/dist/src/lib/ci/outputs.d.ts +25 -0
  33. package/dist/src/lib/ci/outputs.js +84 -0
  34. package/dist/src/lib/ci/triggers.d.ts +9 -0
  35. package/dist/src/lib/ci/triggers.js +86 -0
  36. package/dist/src/lib/ci/types.d.ts +131 -0
  37. package/dist/src/lib/ci/types.js +47 -0
  38. package/dist/src/lib/mcp-config.d.ts +54 -0
  39. package/dist/src/lib/mcp-config.js +172 -0
  40. package/dist/src/lib/merge-check/index.js +6 -12
  41. package/dist/src/lib/merge-check/types.d.ts +20 -7
  42. package/dist/src/lib/merge-check/types.js +11 -0
  43. package/dist/src/lib/phase-signal.d.ts +3 -3
  44. package/dist/src/lib/phase-signal.js +5 -3
  45. package/dist/src/lib/settings.d.ts +52 -0
  46. package/dist/src/lib/settings.js +41 -0
  47. package/dist/src/lib/shutdown.d.ts +16 -5
  48. package/dist/src/lib/shutdown.js +32 -12
  49. package/dist/src/lib/solve-comment-parser.d.ts +9 -102
  50. package/dist/src/lib/solve-comment-parser.js +13 -248
  51. package/dist/src/lib/stacks.d.ts +8 -0
  52. package/dist/src/lib/stacks.js +34 -0
  53. package/dist/src/lib/system.js +3 -7
  54. package/dist/src/lib/test-tautology-detector.d.ts +10 -0
  55. package/dist/src/lib/test-tautology-detector.js +43 -4
  56. package/dist/src/lib/upstream/assessment.js +9 -59
  57. package/dist/src/lib/upstream/issues.js +12 -75
  58. package/dist/src/lib/version-check.d.ts +2 -2
  59. package/dist/src/lib/version-check.js +6 -3
  60. package/dist/src/lib/version.d.ts +4 -0
  61. package/dist/src/lib/version.js +25 -0
  62. package/dist/src/lib/workflow/batch-executor.d.ts +26 -86
  63. package/dist/src/lib/workflow/batch-executor.js +269 -55
  64. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
  65. package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
  66. package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
  67. package/dist/src/lib/workflow/drivers/aider.js +160 -0
  68. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
  69. package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
  70. package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
  71. package/dist/src/lib/workflow/drivers/index.js +27 -0
  72. package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
  73. package/dist/src/lib/workflow/error-classifier.js +90 -0
  74. package/dist/src/lib/workflow/log-writer.d.ts +6 -3
  75. package/dist/src/lib/workflow/log-writer.js +57 -27
  76. package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
  77. package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
  78. package/dist/src/lib/workflow/phase-detection.js +45 -29
  79. package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
  80. package/dist/src/lib/workflow/phase-executor.js +375 -229
  81. package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
  82. package/dist/src/lib/workflow/phase-mapper.js +7 -7
  83. package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
  84. package/dist/src/lib/workflow/platforms/github.js +466 -0
  85. package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
  86. package/dist/src/lib/workflow/platforms/index.js +25 -0
  87. package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
  88. package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
  89. package/dist/src/lib/workflow/pr-status.d.ts +2 -4
  90. package/dist/src/lib/workflow/pr-status.js +3 -16
  91. package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
  92. package/dist/src/lib/workflow/qa-cache.js +88 -0
  93. package/dist/src/lib/workflow/reconcile.d.ts +69 -0
  94. package/dist/src/lib/workflow/reconcile.js +290 -0
  95. package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
  96. package/dist/src/lib/workflow/ring-buffer.js +37 -0
  97. package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
  98. package/dist/src/lib/workflow/run-log-schema.js +47 -12
  99. package/dist/src/lib/workflow/run-reflect.js +1 -1
  100. package/dist/src/lib/workflow/state-cleanup.js +21 -0
  101. package/dist/src/lib/workflow/state-manager.d.ts +34 -3
  102. package/dist/src/lib/workflow/state-manager.js +278 -126
  103. package/dist/src/lib/workflow/state-schema.d.ts +34 -30
  104. package/dist/src/lib/workflow/state-schema.js +35 -25
  105. package/dist/src/lib/workflow/state-utils.d.ts +3 -1
  106. package/dist/src/lib/workflow/state-utils.js +1 -0
  107. package/dist/src/lib/workflow/types.d.ts +224 -6
  108. package/dist/src/lib/workflow/types.js +20 -1
  109. package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
  110. package/dist/src/lib/workflow/worktree-discovery.js +6 -14
  111. package/dist/src/lib/workflow/worktree-manager.js +33 -51
  112. package/dist/src/mcp/index.d.ts +4 -0
  113. package/dist/src/mcp/index.js +4 -0
  114. package/dist/src/mcp/resources.d.ts +7 -0
  115. package/dist/src/mcp/resources.js +111 -0
  116. package/dist/src/mcp/run-registry.d.ts +34 -0
  117. package/dist/src/mcp/run-registry.js +42 -0
  118. package/dist/src/mcp/server.d.ts +12 -0
  119. package/dist/src/mcp/server.js +50 -0
  120. package/dist/src/mcp/tools/logs.d.ts +7 -0
  121. package/dist/src/mcp/tools/logs.js +149 -0
  122. package/dist/src/mcp/tools/run.d.ts +121 -0
  123. package/dist/src/mcp/tools/run.js +591 -0
  124. package/dist/src/mcp/tools/status.d.ts +7 -0
  125. package/dist/src/mcp/tools/status.js +127 -0
  126. package/package.json +26 -7
  127. package/templates/hooks/post-tool.sh +19 -8
  128. package/templates/hooks/pre-tool.sh +36 -49
  129. package/templates/mcp.json +6 -0
  130. package/templates/skills/assess/SKILL.md +354 -352
  131. package/templates/skills/exec/SKILL.md +64 -1
  132. package/templates/skills/fullsolve/SKILL.md +35 -4
  133. package/templates/skills/qa/SKILL.md +486 -9
  134. package/templates/skills/qa/scripts/quality-checks.sh +1 -1
  135. package/templates/skills/setup/SKILL.md +386 -0
  136. package/templates/skills/solve/SKILL.md +38 -664
  137. package/templates/skills/spec/SKILL.md +90 -31
@@ -36,7 +36,7 @@ export declare const COMPLEX_LABELS: string[];
36
36
  */
37
37
  export declare const SECURITY_LABELS: string[];
38
38
  /**
39
- * Detect phases based on issue labels (like /solve logic)
39
+ * Detect phases based on issue labels (like /assess logic)
40
40
  */
41
41
  export declare function detectPhasesFromLabels(labels: string[]): {
42
42
  phases: Phase[];
@@ -34,20 +34,20 @@ export const SECURITY_LABELS = [
34
34
  "admin",
35
35
  ];
36
36
  /**
37
- * Detect phases based on issue labels (like /solve logic)
37
+ * Detect phases based on issue labels (like /assess logic)
38
38
  */
39
39
  export function detectPhasesFromLabels(labels) {
40
40
  const lowerLabels = labels.map((l) => l.toLowerCase());
41
41
  // Check for bug/fix labels → exec → qa (skip spec)
42
- const isBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label.includes(bugLabel)));
42
+ const isBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label === bugLabel));
43
43
  // Check for docs labels → exec → qa (skip spec)
44
- const isDocs = lowerLabels.some((label) => DOCS_LABELS.some((docsLabel) => label.includes(docsLabel)));
44
+ const isDocs = lowerLabels.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
45
45
  // Check for UI labels → add test phase
46
- const isUI = lowerLabels.some((label) => UI_LABELS.some((uiLabel) => label.includes(uiLabel)));
46
+ const isUI = lowerLabels.some((label) => UI_LABELS.some((uiLabel) => label === uiLabel));
47
47
  // Check for complex labels → enable quality loop
48
- const isComplex = lowerLabels.some((label) => COMPLEX_LABELS.some((complexLabel) => label.includes(complexLabel)));
48
+ const isComplex = lowerLabels.some((label) => COMPLEX_LABELS.some((complexLabel) => label === complexLabel));
49
49
  // Check for security labels → add security-review phase
50
- const isSecurity = lowerLabels.some((label) => SECURITY_LABELS.some((secLabel) => label.includes(secLabel)));
50
+ const isSecurity = lowerLabels.some((label) => SECURITY_LABELS.some((secLabel) => label === secLabel));
51
51
  // Build phase list
52
52
  let phases;
53
53
  if (isBugFix || isDocs) {
@@ -118,7 +118,7 @@ export function parseRecommendedWorkflow(output) {
118
118
  * Check if an issue has UI-related labels
119
119
  */
120
120
  export function hasUILabels(labels) {
121
- return labels.some((label) => UI_LABELS.some((uiLabel) => label.toLowerCase().includes(uiLabel)));
121
+ return labels.some((label) => UI_LABELS.some((uiLabel) => label.toLowerCase() === uiLabel));
122
122
  }
123
123
  /**
124
124
  * Determine phases to run based on options and issue labels
@@ -0,0 +1,157 @@
1
+ /**
2
+ * GitHubProvider — PlatformProvider implementation wrapping the `gh` CLI.
3
+ *
4
+ * Owns all `gh` CLI calls for the orchestration layer. Skills continue
5
+ * to call `gh` directly for v1 (see Non-Goals in #368).
6
+ *
7
+ * Sync methods are provided for callers that are currently synchronous
8
+ * (phase-detection, pr-status, system, doctor, worktree-manager).
9
+ * Async interface methods delegate to the sync implementations.
10
+ */
11
+ import type { PlatformProvider, Issue, CreatePROptions, PRInfo, PRStatus, Comment } from "./platform-provider.js";
12
+ /**
13
+ * PR merge status values (matches the casing returned by `gh pr view`).
14
+ */
15
+ export type PRMergeStatus = "MERGED" | "CLOSED" | "OPEN" | null;
16
+ /**
17
+ * Closed issue shape returned by `gh issue list --state closed`.
18
+ */
19
+ export interface ClosedIssueRaw {
20
+ number: number;
21
+ title: string;
22
+ closedAt: string;
23
+ labels: {
24
+ name: string;
25
+ }[];
26
+ }
27
+ /**
28
+ * Result of a raw `gh pr create` CLI call.
29
+ */
30
+ export interface CreatePRCliResult {
31
+ stdout: string;
32
+ stderr: string;
33
+ exitCode: number | null;
34
+ }
35
+ /**
36
+ * Info returned for an issue in a batch query.
37
+ */
38
+ export interface BatchIssueInfo {
39
+ number: number;
40
+ title: string;
41
+ state: "OPEN" | "CLOSED";
42
+ }
43
+ /**
44
+ * Info returned for a PR in a batch query.
45
+ */
46
+ export interface BatchPRInfo {
47
+ number: number;
48
+ state: "OPEN" | "MERGED" | "CLOSED";
49
+ }
50
+ /**
51
+ * Result of a batch GitHub query.
52
+ */
53
+ export interface BatchGitHubResult {
54
+ issues: Record<number, BatchIssueInfo>;
55
+ pullRequests: Record<number, BatchPRInfo>;
56
+ error?: string;
57
+ }
58
+ export declare class GitHubProvider implements PlatformProvider {
59
+ name: string;
60
+ /**
61
+ * Fetch issue comment bodies as a string array.
62
+ * Used by phase-detection.ts for phase marker parsing.
63
+ */
64
+ fetchIssueCommentBodiesSync(issueId: string): string[];
65
+ /**
66
+ * Check if `gh` CLI is authenticated.
67
+ * Used by system.ts and doctor.ts.
68
+ */
69
+ checkAuthSync(): boolean;
70
+ /**
71
+ * Get PR merge status by PR number.
72
+ * Used by pr-status.ts and state-cleanup.ts.
73
+ */
74
+ getPRMergeStatusSync(prNumber: number): PRMergeStatus;
75
+ /**
76
+ * List recently closed issues.
77
+ * Used by doctor.ts for closed-issue verification.
78
+ */
79
+ listClosedIssuesSync(limit?: number): ClosedIssueRaw[];
80
+ /**
81
+ * View a PR by branch name, returning number and URL.
82
+ * Used by worktree-manager.ts to check for existing PRs.
83
+ */
84
+ viewPRByBranchSync(branch: string, cwd?: string): {
85
+ number: number;
86
+ url: string;
87
+ } | null;
88
+ /**
89
+ * Get the head branch name for a PR by number.
90
+ * Used by hooks/pre-tool.sh for pre-merge worktree cleanup.
91
+ */
92
+ getPRHeadBranchSync(prNumber: number): string | null;
93
+ /**
94
+ * Create a PR via `gh pr create` CLI, returning raw result.
95
+ * Used by worktree-manager.ts which needs access to stdout for URL extraction.
96
+ */
97
+ createPRCliSync(title: string, body: string, head: string, cwd?: string): CreatePRCliResult;
98
+ /**
99
+ * Batch fetch issue and PR status in a single GraphQL call.
100
+ * Returns a map keyed by issue/PR number.
101
+ */
102
+ batchFetchIssueAndPRStatus(issueNumbers: number[], prNumbers: number[]): BatchGitHubResult;
103
+ /**
104
+ * Fetch just the title of an issue by number.
105
+ * Used by merge-check and worktree-discovery.
106
+ */
107
+ fetchIssueTitleSync(issueId: string): string | null;
108
+ /**
109
+ * Check if the `gh` CLI binary is installed (not auth, just available).
110
+ * Used by upstream/assessment.ts for pre-flight checks.
111
+ */
112
+ checkGhInstalledSync(): boolean;
113
+ /**
114
+ * Fetch a single release from a specific repo.
115
+ * If `version` is omitted, fetches the latest release.
116
+ * Used by upstream/assessment.ts.
117
+ */
118
+ fetchReleaseSync(repo: string, version?: string): Record<string, unknown> | null;
119
+ /**
120
+ * List releases from a specific repo.
121
+ * Used by upstream/assessment.ts.
122
+ */
123
+ listReleasesSync(repo: string, limit?: number): Array<{
124
+ tagName: string;
125
+ publishedAt: string;
126
+ }>;
127
+ /**
128
+ * Search issues in a specific repo by labels and search query.
129
+ * Used by upstream/issues.ts for duplicate detection.
130
+ */
131
+ searchIssuesSync(repo: string, labels: string[], search: string, limit?: number): Array<{
132
+ number: number;
133
+ title: string;
134
+ }>;
135
+ /**
136
+ * Create an issue in a specific repo using a body file.
137
+ * Used by upstream/issues.ts.
138
+ */
139
+ createIssueWithBodyFileSync(repo: string, title: string, bodyFile: string, labels: string[]): {
140
+ number: number;
141
+ url: string;
142
+ } | null;
143
+ /**
144
+ * Add a comment to an issue in a specific repo using a body file.
145
+ * Used by upstream/issues.ts.
146
+ */
147
+ commentOnIssueWithBodyFileSync(repo: string, issueNumber: number, bodyFile: string): boolean;
148
+ fetchIssue(id: string): Promise<Issue>;
149
+ postComment(issueId: string, body: string): Promise<void>;
150
+ addLabel(issueId: string, label: string): Promise<void>;
151
+ removeLabel(issueId: string, label: string): Promise<void>;
152
+ createPR(opts: CreatePROptions): Promise<PRInfo>;
153
+ getPRStatus(prId: string): Promise<PRStatus>;
154
+ postPRComment(prId: string, body: string): Promise<void>;
155
+ checkAuth(): Promise<boolean>;
156
+ getIssueComments(issueId: string): Promise<Comment[]>;
157
+ }
@@ -0,0 +1,466 @@
1
+ /**
2
+ * GitHubProvider — PlatformProvider implementation wrapping the `gh` CLI.
3
+ *
4
+ * Owns all `gh` CLI calls for the orchestration layer. Skills continue
5
+ * to call `gh` directly for v1 (see Non-Goals in #368).
6
+ *
7
+ * Sync methods are provided for callers that are currently synchronous
8
+ * (phase-detection, pr-status, system, doctor, worktree-manager).
9
+ * Async interface methods delegate to the sync implementations.
10
+ */
11
+ import { execSync, spawnSync } from "child_process";
12
+ export class GitHubProvider {
13
+ name = "github";
14
+ // ─── Sync helpers (for synchronous callers) ────────────────────────
15
+ /**
16
+ * Fetch issue comment bodies as a string array.
17
+ * Used by phase-detection.ts for phase marker parsing.
18
+ */
19
+ fetchIssueCommentBodiesSync(issueId) {
20
+ try {
21
+ const result = spawnSync("gh", [
22
+ "issue",
23
+ "view",
24
+ issueId,
25
+ "--json",
26
+ "comments",
27
+ "--jq",
28
+ "[.comments[].body]",
29
+ ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
30
+ if (result.status !== 0 || !result.stdout)
31
+ return [];
32
+ return JSON.parse(result.stdout);
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ }
38
+ /**
39
+ * Check if `gh` CLI is authenticated.
40
+ * Used by system.ts and doctor.ts.
41
+ */
42
+ checkAuthSync() {
43
+ try {
44
+ execSync("gh auth status", { stdio: "ignore" });
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ /**
52
+ * Get PR merge status by PR number.
53
+ * Used by pr-status.ts and state-cleanup.ts.
54
+ */
55
+ getPRMergeStatusSync(prNumber) {
56
+ try {
57
+ const result = spawnSync("gh", ["pr", "view", String(prNumber), "--json", "state", "-q", ".state"], { stdio: "pipe", timeout: 10000 });
58
+ if (result.status === 0 && result.stdout) {
59
+ const state = result.stdout.toString().trim().toUpperCase();
60
+ if (state === "MERGED")
61
+ return "MERGED";
62
+ if (state === "CLOSED")
63
+ return "CLOSED";
64
+ if (state === "OPEN")
65
+ return "OPEN";
66
+ }
67
+ }
68
+ catch {
69
+ // gh not available or error
70
+ }
71
+ return null;
72
+ }
73
+ /**
74
+ * List recently closed issues.
75
+ * Used by doctor.ts for closed-issue verification.
76
+ */
77
+ listClosedIssuesSync(limit = 100) {
78
+ try {
79
+ const result = spawnSync("gh", [
80
+ "issue",
81
+ "list",
82
+ "--state",
83
+ "closed",
84
+ "--json",
85
+ "number,title,closedAt,labels",
86
+ "--limit",
87
+ String(limit),
88
+ ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
89
+ if (result.status !== 0 || !result.stdout)
90
+ return [];
91
+ return JSON.parse(result.stdout);
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ /**
98
+ * View a PR by branch name, returning number and URL.
99
+ * Used by worktree-manager.ts to check for existing PRs.
100
+ */
101
+ viewPRByBranchSync(branch, cwd) {
102
+ const result = spawnSync("gh", ["pr", "view", branch, "--json", "number,url"], { stdio: "pipe", cwd, timeout: 15000 });
103
+ if (result.status === 0 && result.stdout) {
104
+ try {
105
+ const info = JSON.parse(result.stdout.toString());
106
+ if (info.number && info.url) {
107
+ return { number: info.number, url: info.url };
108
+ }
109
+ }
110
+ catch {
111
+ // JSON parse failed
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ /**
117
+ * Get the head branch name for a PR by number.
118
+ * Used by hooks/pre-tool.sh for pre-merge worktree cleanup.
119
+ */
120
+ getPRHeadBranchSync(prNumber) {
121
+ const result = spawnSync("gh", [
122
+ "pr",
123
+ "view",
124
+ String(prNumber),
125
+ "--json",
126
+ "headRefName",
127
+ "--jq",
128
+ ".headRefName",
129
+ ], { stdio: "pipe", timeout: 10000 });
130
+ if (result.status === 0 && result.stdout) {
131
+ const branch = result.stdout.toString().trim();
132
+ return branch || null;
133
+ }
134
+ return null;
135
+ }
136
+ /**
137
+ * Create a PR via `gh pr create` CLI, returning raw result.
138
+ * Used by worktree-manager.ts which needs access to stdout for URL extraction.
139
+ */
140
+ createPRCliSync(title, body, head, cwd) {
141
+ const result = spawnSync("gh", ["pr", "create", "--title", title, "--body", body, "--head", head], { stdio: "pipe", cwd, timeout: 30000 });
142
+ return {
143
+ stdout: result.stdout?.toString() ?? "",
144
+ stderr: result.stderr?.toString() ?? "",
145
+ exitCode: result.status,
146
+ };
147
+ }
148
+ /**
149
+ * Batch fetch issue and PR status in a single GraphQL call.
150
+ * Returns a map keyed by issue/PR number.
151
+ */
152
+ batchFetchIssueAndPRStatus(issueNumbers, prNumbers) {
153
+ if (issueNumbers.length === 0 && prNumbers.length === 0) {
154
+ return { issues: {}, pullRequests: {}, error: undefined };
155
+ }
156
+ try {
157
+ // Build GraphQL query with aliases for each issue and PR
158
+ const issueFields = issueNumbers
159
+ .map((n) => `issue_${n}: issue(number: ${n}) { number title state }`)
160
+ .join("\n ");
161
+ const prFields = prNumbers
162
+ .map((n) => `pr_${n}: pullRequest(number: ${n}) { number state }`)
163
+ .join("\n ");
164
+ const query = `query {
165
+ repository(owner: "{owner}", name: "{repo}") {
166
+ ${issueFields}
167
+ ${prFields}
168
+ }
169
+ }`;
170
+ // Get repo owner/name
171
+ const repoResult = spawnSync("gh", ["repo", "view", "--json", "owner,name"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
172
+ if (repoResult.status !== 0 || !repoResult.stdout) {
173
+ return {
174
+ issues: {},
175
+ pullRequests: {},
176
+ error: "Failed to determine repository",
177
+ };
178
+ }
179
+ const repo = JSON.parse(repoResult.stdout);
180
+ const filledQuery = query
181
+ .replace("{owner}", repo.owner.login)
182
+ .replace("{repo}", repo.name);
183
+ const result = spawnSync("gh", ["api", "graphql", "-f", `query=${filledQuery}`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
184
+ if (result.status !== 0 || !result.stdout) {
185
+ const stderr = result.stderr?.trim() ?? "Unknown error";
186
+ return { issues: {}, pullRequests: {}, error: stderr };
187
+ }
188
+ const data = JSON.parse(result.stdout);
189
+ const issues = {};
190
+ const pullRequests = {};
191
+ const repoData = data.data?.repository ?? {};
192
+ for (const [key, value] of Object.entries(repoData)) {
193
+ if (!value)
194
+ continue;
195
+ if (key.startsWith("issue_")) {
196
+ issues[value.number] = {
197
+ number: value.number,
198
+ title: value.title ?? "",
199
+ state: value.state,
200
+ };
201
+ }
202
+ else if (key.startsWith("pr_")) {
203
+ pullRequests[value.number] = {
204
+ number: value.number,
205
+ state: value.state,
206
+ };
207
+ }
208
+ }
209
+ return { issues, pullRequests, error: undefined };
210
+ }
211
+ catch (err) {
212
+ return {
213
+ issues: {},
214
+ pullRequests: {},
215
+ error: err instanceof Error ? err.message : String(err),
216
+ };
217
+ }
218
+ }
219
+ // ─── Repo-aware sync helpers (for upstream / utility callers) ──────
220
+ /**
221
+ * Fetch just the title of an issue by number.
222
+ * Used by merge-check and worktree-discovery.
223
+ */
224
+ fetchIssueTitleSync(issueId) {
225
+ try {
226
+ const result = spawnSync("gh", ["issue", "view", issueId, "--json", "title", "--jq", ".title"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
227
+ if (result.status !== 0 || !result.stdout?.trim())
228
+ return null;
229
+ return result.stdout.trim();
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ }
235
+ /**
236
+ * Check if the `gh` CLI binary is installed (not auth, just available).
237
+ * Used by upstream/assessment.ts for pre-flight checks.
238
+ */
239
+ checkGhInstalledSync() {
240
+ try {
241
+ const result = spawnSync("gh", ["--version"], {
242
+ stdio: ["pipe", "pipe", "pipe"],
243
+ timeout: 5000,
244
+ });
245
+ return result.status === 0;
246
+ }
247
+ catch {
248
+ return false;
249
+ }
250
+ }
251
+ /**
252
+ * Fetch a single release from a specific repo.
253
+ * If `version` is omitted, fetches the latest release.
254
+ * Used by upstream/assessment.ts.
255
+ */
256
+ fetchReleaseSync(repo, version) {
257
+ try {
258
+ const args = ["release", "view"];
259
+ if (version)
260
+ args.push(version);
261
+ args.push("--repo", repo, "--json", "tagName,name,body,publishedAt");
262
+ const result = spawnSync("gh", args, {
263
+ encoding: "utf-8",
264
+ stdio: ["pipe", "pipe", "pipe"],
265
+ timeout: 15000,
266
+ });
267
+ if (result.status !== 0 || !result.stdout)
268
+ return null;
269
+ return JSON.parse(result.stdout);
270
+ }
271
+ catch {
272
+ return null;
273
+ }
274
+ }
275
+ /**
276
+ * List releases from a specific repo.
277
+ * Used by upstream/assessment.ts.
278
+ */
279
+ listReleasesSync(repo, limit = 50) {
280
+ try {
281
+ const result = spawnSync("gh", [
282
+ "release",
283
+ "list",
284
+ "--repo",
285
+ repo,
286
+ "--limit",
287
+ String(limit),
288
+ "--json",
289
+ "tagName,publishedAt",
290
+ ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
291
+ if (result.status !== 0 || !result.stdout)
292
+ return [];
293
+ return JSON.parse(result.stdout);
294
+ }
295
+ catch {
296
+ return [];
297
+ }
298
+ }
299
+ /**
300
+ * Search issues in a specific repo by labels and search query.
301
+ * Used by upstream/issues.ts for duplicate detection.
302
+ */
303
+ searchIssuesSync(repo, labels, search, limit = 10) {
304
+ try {
305
+ const args = ["issue", "list", "--repo", repo];
306
+ for (const label of labels) {
307
+ args.push("--label", label);
308
+ }
309
+ args.push("--search", search, "--json", "number,title", "--limit", String(limit));
310
+ const result = spawnSync("gh", args, {
311
+ encoding: "utf-8",
312
+ stdio: ["pipe", "pipe", "pipe"],
313
+ timeout: 15000,
314
+ });
315
+ if (result.status !== 0 || !result.stdout)
316
+ return [];
317
+ return JSON.parse(result.stdout);
318
+ }
319
+ catch {
320
+ return [];
321
+ }
322
+ }
323
+ /**
324
+ * Create an issue in a specific repo using a body file.
325
+ * Used by upstream/issues.ts.
326
+ */
327
+ createIssueWithBodyFileSync(repo, title, bodyFile, labels) {
328
+ try {
329
+ const args = [
330
+ "issue",
331
+ "create",
332
+ "--repo",
333
+ repo,
334
+ "--title",
335
+ title,
336
+ "--body-file",
337
+ bodyFile,
338
+ ];
339
+ for (const label of labels) {
340
+ args.push("--label", label);
341
+ }
342
+ const result = spawnSync("gh", args, {
343
+ encoding: "utf-8",
344
+ stdio: ["pipe", "pipe", "pipe"],
345
+ timeout: 30000,
346
+ });
347
+ if (result.status !== 0 || !result.stdout)
348
+ return null;
349
+ const url = result.stdout.trim();
350
+ const numberMatch = url.match(/\/issues\/(\d+)$/);
351
+ const number = numberMatch ? parseInt(numberMatch[1], 10) : 0;
352
+ return { number, url };
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ }
358
+ /**
359
+ * Add a comment to an issue in a specific repo using a body file.
360
+ * Used by upstream/issues.ts.
361
+ */
362
+ commentOnIssueWithBodyFileSync(repo, issueNumber, bodyFile) {
363
+ try {
364
+ const result = spawnSync("gh", [
365
+ "issue",
366
+ "comment",
367
+ String(issueNumber),
368
+ "--repo",
369
+ repo,
370
+ "--body-file",
371
+ bodyFile,
372
+ ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
373
+ return result.status === 0;
374
+ }
375
+ catch {
376
+ return false;
377
+ }
378
+ }
379
+ // ─── Async interface methods (PlatformProvider) ────────────────────
380
+ async fetchIssue(id) {
381
+ const result = spawnSync("gh", ["issue", "view", id, "--json", "number,title,body,labels,state"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
382
+ if (result.status !== 0 || !result.stdout) {
383
+ throw new Error(`Failed to fetch issue ${id}`);
384
+ }
385
+ const data = JSON.parse(result.stdout);
386
+ return {
387
+ id: String(data.number),
388
+ number: data.number,
389
+ title: data.title,
390
+ body: data.body,
391
+ labels: (data.labels ?? []).map((l) => l.name),
392
+ state: data.state.toLowerCase(),
393
+ };
394
+ }
395
+ async postComment(issueId, body) {
396
+ spawnSync("gh", ["issue", "comment", issueId, "--body", body], {
397
+ stdio: "pipe",
398
+ timeout: 15000,
399
+ });
400
+ }
401
+ async addLabel(issueId, label) {
402
+ spawnSync("gh", ["issue", "edit", issueId, "--add-label", label], {
403
+ stdio: "pipe",
404
+ timeout: 15000,
405
+ });
406
+ }
407
+ async removeLabel(issueId, label) {
408
+ spawnSync("gh", ["issue", "edit", issueId, "--remove-label", label], {
409
+ stdio: "pipe",
410
+ timeout: 15000,
411
+ });
412
+ }
413
+ async createPR(opts) {
414
+ const result = this.createPRCliSync(opts.title, opts.body, opts.head);
415
+ if (result.exitCode !== 0) {
416
+ const error = result.stderr.trim() || "Unknown error";
417
+ throw new Error(`gh pr create failed: ${error}`);
418
+ }
419
+ const urlMatch = result.stdout
420
+ .trim()
421
+ .match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
422
+ if (urlMatch) {
423
+ return {
424
+ number: parseInt(urlMatch[1], 10),
425
+ url: urlMatch[0],
426
+ };
427
+ }
428
+ throw new Error(`PR created but could not extract URL from output: ${result.stdout.trim()}`);
429
+ }
430
+ async getPRStatus(prId) {
431
+ const status = this.getPRMergeStatusSync(parseInt(prId, 10));
432
+ if (status) {
433
+ return { state: status.toLowerCase() };
434
+ }
435
+ throw new Error(`Could not determine PR status for ${prId}`);
436
+ }
437
+ async postPRComment(prId, body) {
438
+ spawnSync("gh", ["pr", "comment", prId, "--body", body], {
439
+ stdio: "pipe",
440
+ timeout: 15000,
441
+ });
442
+ }
443
+ async checkAuth() {
444
+ return this.checkAuthSync();
445
+ }
446
+ async getIssueComments(issueId) {
447
+ try {
448
+ const result = spawnSync("gh", [
449
+ "issue",
450
+ "view",
451
+ issueId,
452
+ "--json",
453
+ "comments",
454
+ "--jq",
455
+ "[.comments[] | {body: .body, createdAt: .createdAt}]",
456
+ ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
457
+ if (result.status !== 0 || !result.stdout)
458
+ return [];
459
+ const data = JSON.parse(result.stdout);
460
+ return data;
461
+ }
462
+ catch {
463
+ return [];
464
+ }
465
+ }
466
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Platform provider registry.
3
+ *
4
+ * Simple map-based registry — not a plugin system.
5
+ * New providers are registered by adding entries to PLATFORMS.
6
+ */
7
+ import type { PlatformProvider } from "./platform-provider.js";
8
+ export type { PlatformProvider, Issue, CreatePROptions, PRInfo, PRStatus, Comment, } from "./platform-provider.js";
9
+ export { GitHubProvider } from "./github.js";
10
+ export type { PRMergeStatus, ClosedIssueRaw, CreatePRCliResult, } from "./github.js";
11
+ /**
12
+ * Get a platform provider by name.
13
+ *
14
+ * @param name - Provider name (default: "github")
15
+ * @throws Error if provider name is unknown
16
+ */
17
+ export declare function getPlatform(name?: string): PlatformProvider;