@gethmy/agent 1.0.0 → 1.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.
- package/README.md +5 -5
- package/dist/board-helpers.d.ts +23 -0
- package/dist/board-helpers.js +131 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2 -11761
- package/dist/completion.d.ts +7 -0
- package/dist/completion.js +132 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +91 -0
- package/dist/git-pr.d.ts +25 -0
- package/dist/git-pr.js +305 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +165 -11730
- package/dist/log.d.ts +10 -0
- package/dist/log.js +35 -0
- package/dist/merge-monitor.d.ts +23 -0
- package/dist/merge-monitor.js +155 -0
- package/dist/pm.d.ts +14 -0
- package/dist/pm.js +63 -0
- package/dist/pool.d.ts +36 -0
- package/dist/pool.js +134 -0
- package/dist/progress-tracker.d.ts +39 -0
- package/dist/progress-tracker.js +189 -0
- package/dist/prompt.d.ts +5 -0
- package/dist/prompt.js +40 -0
- package/dist/queue.d.ts +37 -0
- package/dist/queue.js +96 -0
- package/dist/reconcile.d.ts +21 -0
- package/dist/reconcile.js +107 -0
- package/dist/review-completion.d.ts +31 -0
- package/dist/review-completion.js +247 -0
- package/dist/review-knowledge.d.ts +14 -0
- package/dist/review-knowledge.js +89 -0
- package/dist/review-prompt.d.ts +12 -0
- package/dist/review-prompt.js +100 -0
- package/dist/review-worker.d.ts +35 -0
- package/dist/review-worker.js +302 -0
- package/dist/review-worktree.d.ts +12 -0
- package/dist/review-worktree.js +83 -0
- package/dist/stream-parser.d.ts +22 -0
- package/dist/stream-parser.js +81 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +53 -0
- package/dist/verification.d.ts +16 -0
- package/dist/verification.js +251 -0
- package/dist/watcher.d.ts +21 -0
- package/dist/watcher.js +62 -0
- package/dist/worker.d.ts +34 -0
- package/dist/worker.js +268 -0
- package/dist/worktree.d.ts +13 -0
- package/dist/worktree.js +115 -0
- package/package.json +6 -5
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
+
import type { Card } from "@harmony/shared";
|
|
3
|
+
import { type AgentConfig } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Post-work pipeline: push branch, create PR, move card, post summary.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runCompletion(client: HarmonyApiClient, card: Card, branchName: string, worktreePath: string, config: AgentConfig, workerId?: number): Promise<void>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { moveCardToColumn } from "./board-helpers.js";
|
|
3
|
+
import { createPullRequest, detectGitProvider, pushBranch } from "./git-pr.js";
|
|
4
|
+
import { log } from "./log.js";
|
|
5
|
+
import { AGENT_NAME, agentIdentifier } from "./types.js";
|
|
6
|
+
import { attemptAutoFix, reportFindings, runVerification, } from "./verification.js";
|
|
7
|
+
import { cleanupWorktree } from "./worktree.js";
|
|
8
|
+
const TAG = "completion";
|
|
9
|
+
// ============ COMPLETION PIPELINE ============
|
|
10
|
+
/**
|
|
11
|
+
* Post-work pipeline: push branch, create PR, move card, post summary.
|
|
12
|
+
*/
|
|
13
|
+
export async function runCompletion(client, card, branchName, worktreePath, config, workerId = 0) {
|
|
14
|
+
// Check if there are any commits on the branch
|
|
15
|
+
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
|
|
16
|
+
if (!hasCommits) {
|
|
17
|
+
log.warn(TAG, `No commits on branch ${branchName} — skipping completion`);
|
|
18
|
+
await client.endAgentSession(card.id, {
|
|
19
|
+
status: "completed",
|
|
20
|
+
progressPercent: 100,
|
|
21
|
+
});
|
|
22
|
+
cleanupWorktree(worktreePath, branchName);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// 1. Verification gate
|
|
26
|
+
if (config.verification.enabled) {
|
|
27
|
+
await client.updateAgentProgress(card.id, {
|
|
28
|
+
agentIdentifier: agentIdentifier(workerId),
|
|
29
|
+
agentName: AGENT_NAME,
|
|
30
|
+
status: "working",
|
|
31
|
+
currentTask: "Verifying build...",
|
|
32
|
+
progressPercent: 80,
|
|
33
|
+
});
|
|
34
|
+
let result = await runVerification(worktreePath, config, workerId);
|
|
35
|
+
if (!result.passed && config.verification.autoFix) {
|
|
36
|
+
for (let attempt = 0; attempt < config.verification.maxFixAttempts; attempt++) {
|
|
37
|
+
log.info(TAG, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
|
|
38
|
+
await client.updateAgentProgress(card.id, {
|
|
39
|
+
agentIdentifier: agentIdentifier(workerId),
|
|
40
|
+
agentName: AGENT_NAME,
|
|
41
|
+
status: "working",
|
|
42
|
+
currentTask: `Fixing issues (attempt ${attempt + 1})...`,
|
|
43
|
+
progressPercent: 85,
|
|
44
|
+
});
|
|
45
|
+
const allErrors = [...result.buildErrors, ...result.lintWarnings];
|
|
46
|
+
await attemptAutoFix(worktreePath, config, allErrors);
|
|
47
|
+
result = await runVerification(worktreePath, config, workerId);
|
|
48
|
+
if (result.passed) {
|
|
49
|
+
log.info(TAG, `Auto-fix succeeded on attempt ${attempt + 1}`);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!result.passed) {
|
|
55
|
+
log.warn(TAG, `Verification failed for #${card.short_id} — reporting findings`);
|
|
56
|
+
await reportFindings(client, card.id, result);
|
|
57
|
+
await moveCardToColumn(client, card, config.verification.failColumn);
|
|
58
|
+
await client.endAgentSession(card.id, { status: "paused" });
|
|
59
|
+
cleanupWorktree(worktreePath, branchName);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
log.info(TAG, `Verification passed for #${card.short_id}`);
|
|
63
|
+
}
|
|
64
|
+
// 2. Push branch (force-push on rework if remote branch already exists)
|
|
65
|
+
log.info(TAG, `Pushing branch ${branchName}...`);
|
|
66
|
+
pushBranch(branchName, worktreePath);
|
|
67
|
+
// 3. Create PR
|
|
68
|
+
let prUrl = null;
|
|
69
|
+
if (config.completion.createPR) {
|
|
70
|
+
const provider = detectGitProvider(worktreePath);
|
|
71
|
+
prUrl = createPullRequest(card, branchName, worktreePath, config, provider);
|
|
72
|
+
}
|
|
73
|
+
// 4. Move card to Review column
|
|
74
|
+
if (config.completion.moveToColumn) {
|
|
75
|
+
await moveCardToColumn(client, card, config.completion.moveToColumn);
|
|
76
|
+
}
|
|
77
|
+
// 5. Post summary — always includes branch, optionally PR link
|
|
78
|
+
if (config.completion.postSummary) {
|
|
79
|
+
await postSummary(client, card, branchName, worktreePath, prUrl, config.worktree.baseBranch);
|
|
80
|
+
}
|
|
81
|
+
// 6. End agent session
|
|
82
|
+
await client.endAgentSession(card.id, {
|
|
83
|
+
status: "completed",
|
|
84
|
+
progressPercent: 100,
|
|
85
|
+
});
|
|
86
|
+
// 7. Cleanup worktree
|
|
87
|
+
cleanupWorktree(worktreePath, branchName);
|
|
88
|
+
log.info(TAG, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
89
|
+
}
|
|
90
|
+
function checkHasCommits(worktreePath, baseBranch) {
|
|
91
|
+
try {
|
|
92
|
+
const count = execFileSync("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
93
|
+
return parseInt(count, 10) > 0;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function postSummary(client, card, branchName, worktreePath, prUrl, baseBranch) {
|
|
100
|
+
// Build commit summary
|
|
101
|
+
let commitLog = "";
|
|
102
|
+
try {
|
|
103
|
+
commitLog = execFileSync("git", ["log", "--oneline", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// best-effort
|
|
107
|
+
}
|
|
108
|
+
// Build description note
|
|
109
|
+
const SUMMARY_MARKER = "---\n**Agent completed**";
|
|
110
|
+
const parts = [`\n\n${SUMMARY_MARKER}`];
|
|
111
|
+
if (prUrl) {
|
|
112
|
+
parts.push(`PR: ${prUrl}`);
|
|
113
|
+
}
|
|
114
|
+
parts.push(`Branch: \`${branchName}\``);
|
|
115
|
+
if (commitLog) {
|
|
116
|
+
parts.push(`\n\`\`\`\n${commitLog}\n\`\`\``);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
// Strip any previous agent summary block before appending (rework)
|
|
120
|
+
const existingDesc = card.description || "";
|
|
121
|
+
const baseDesc = existingDesc.includes(SUMMARY_MARKER)
|
|
122
|
+
? existingDesc.slice(0, existingDesc.indexOf(SUMMARY_MARKER)).trimEnd()
|
|
123
|
+
: existingDesc;
|
|
124
|
+
await client.updateCard(card.id, {
|
|
125
|
+
description: baseDesc + parts.join("\n"),
|
|
126
|
+
});
|
|
127
|
+
log.info(TAG, `Posted completion summary to #${card.short_id}`);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
log.error(TAG, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
+
import type { AgentConfig, RealtimeCredentials } from "./types.js";
|
|
3
|
+
export interface DaemonConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
apiUrl: string;
|
|
6
|
+
workspaceId: string;
|
|
7
|
+
projectId: string;
|
|
8
|
+
userEmail: string;
|
|
9
|
+
agent: AgentConfig;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Load daemon configuration from ~/.harmony-mcp/config.json,
|
|
13
|
+
* merging default agent settings with any user overrides.
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadDaemonConfig(): DaemonConfig;
|
|
16
|
+
/**
|
|
17
|
+
* Fetch Supabase realtime credentials from the Harmony API.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fetchRealtimeCredentials(client: HarmonyApiClient): Promise<RealtimeCredentials>;
|
|
20
|
+
/**
|
|
21
|
+
* Create an API client from daemon config.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createApiClient(config: DaemonConfig): HarmonyApiClient;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
6
|
+
import { getActiveProjectId, getActiveWorkspaceId, getApiKey, getApiUrl, getUserEmail, } from "@gethmy/mcp/src/config.js";
|
|
7
|
+
import { DEFAULT_AGENT_CONFIG } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the git repo root so local config lookup always
|
|
10
|
+
* finds .harmony-mcp.json regardless of cwd.
|
|
11
|
+
*/
|
|
12
|
+
function getRepoRoot() {
|
|
13
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
}).trim();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Load daemon configuration from ~/.harmony-mcp/config.json,
|
|
19
|
+
* merging default agent settings with any user overrides.
|
|
20
|
+
*/
|
|
21
|
+
export function loadDaemonConfig() {
|
|
22
|
+
const repoRoot = getRepoRoot();
|
|
23
|
+
const apiKey = getApiKey();
|
|
24
|
+
const apiUrl = getApiUrl();
|
|
25
|
+
const workspaceId = getActiveWorkspaceId(repoRoot);
|
|
26
|
+
const projectId = getActiveProjectId(repoRoot);
|
|
27
|
+
const userEmail = getUserEmail();
|
|
28
|
+
if (!workspaceId) {
|
|
29
|
+
throw new Error("No active workspace configured. Run `npx @gethmy/mcp setup` first.");
|
|
30
|
+
}
|
|
31
|
+
if (!projectId) {
|
|
32
|
+
throw new Error("No active project configured. Run `npx @gethmy/mcp setup` first.");
|
|
33
|
+
}
|
|
34
|
+
if (!userEmail) {
|
|
35
|
+
throw new Error("No user email configured. Run `npx @gethmy/mcp setup` first.");
|
|
36
|
+
}
|
|
37
|
+
// Try to load agent-specific overrides from config file
|
|
38
|
+
let agentOverrides = {};
|
|
39
|
+
try {
|
|
40
|
+
const configPath = join(homedir(), ".harmony-mcp", "config.json");
|
|
41
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
if (parsed.agent) {
|
|
44
|
+
agentOverrides = parsed.agent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// No overrides, use defaults
|
|
49
|
+
}
|
|
50
|
+
const agent = {
|
|
51
|
+
...DEFAULT_AGENT_CONFIG,
|
|
52
|
+
...agentOverrides,
|
|
53
|
+
completion: {
|
|
54
|
+
...DEFAULT_AGENT_CONFIG.completion,
|
|
55
|
+
...(agentOverrides.completion ?? {}),
|
|
56
|
+
},
|
|
57
|
+
claude: {
|
|
58
|
+
...DEFAULT_AGENT_CONFIG.claude,
|
|
59
|
+
...(agentOverrides.claude ?? {}),
|
|
60
|
+
},
|
|
61
|
+
worktree: {
|
|
62
|
+
...DEFAULT_AGENT_CONFIG.worktree,
|
|
63
|
+
...(agentOverrides.worktree ?? {}),
|
|
64
|
+
},
|
|
65
|
+
verification: {
|
|
66
|
+
...DEFAULT_AGENT_CONFIG.verification,
|
|
67
|
+
...(agentOverrides.verification ?? {}),
|
|
68
|
+
},
|
|
69
|
+
review: {
|
|
70
|
+
...DEFAULT_AGENT_CONFIG.review,
|
|
71
|
+
...(agentOverrides.review ?? {}),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
return { apiKey, apiUrl, workspaceId, projectId, userEmail, agent };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fetch Supabase realtime credentials from the Harmony API.
|
|
78
|
+
*/
|
|
79
|
+
export async function fetchRealtimeCredentials(client) {
|
|
80
|
+
const result = (await client.request("GET", "/config/realtime"));
|
|
81
|
+
if (!result.supabaseUrl || !result.supabaseAnonKey) {
|
|
82
|
+
throw new Error("Invalid realtime credentials response from API");
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create an API client from daemon config.
|
|
88
|
+
*/
|
|
89
|
+
export function createApiClient(config) {
|
|
90
|
+
return new HarmonyApiClient({ apiKey: config.apiKey, apiUrl: config.apiUrl });
|
|
91
|
+
}
|
package/dist/git-pr.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Card } from "@harmony/shared";
|
|
2
|
+
import type { AgentConfig } from "./types.js";
|
|
3
|
+
export type GitProvider = "github" | "azure" | "gitlab" | "bitbucket" | "unknown";
|
|
4
|
+
export declare function detectGitProvider(cwd?: string): GitProvider;
|
|
5
|
+
/**
|
|
6
|
+
* Validate that the CLI for the detected git provider is installed and authenticated.
|
|
7
|
+
* Returns the provider name or throws with instructions.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateGitProviderCli(provider: GitProvider, cwd?: string): void;
|
|
10
|
+
export type PrState = "merged" | "open" | "closed" | "unknown";
|
|
11
|
+
/**
|
|
12
|
+
* Check whether a PR has been merged using the git provider CLI (async).
|
|
13
|
+
*/
|
|
14
|
+
export declare function checkPrMergeStatus(prUrl: string, cwd: string, provider: GitProvider): Promise<PrState>;
|
|
15
|
+
/**
|
|
16
|
+
* Extract a PR URL from card description. Matches the format written by completion.ts:
|
|
17
|
+
* `PR: https://...`
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractPrUrl(description: string | null): string | null;
|
|
20
|
+
export declare function remoteBranchExists(branchName: string, cwd: string): boolean;
|
|
21
|
+
export declare function pushBranch(branchName: string, cwd: string): void;
|
|
22
|
+
export declare function buildPrBody(card: Card, commitLog: string): string;
|
|
23
|
+
export declare function createPullRequest(card: Card, branchName: string, worktreePath: string, config: AgentConfig, provider: GitProvider): string | null;
|
|
24
|
+
export declare function findExistingPr(branchName: string, worktreePath: string, provider: GitProvider): string | null;
|
|
25
|
+
export declare function updateExistingPr(branchName: string, body: string, worktreePath: string, provider: GitProvider): void;
|
package/dist/git-pr.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { log } from "./log.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const TAG = "git-pr";
|
|
6
|
+
export function detectGitProvider(cwd) {
|
|
7
|
+
try {
|
|
8
|
+
const url = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
9
|
+
cwd,
|
|
10
|
+
encoding: "utf-8",
|
|
11
|
+
}).trim();
|
|
12
|
+
if (url.includes("github.com"))
|
|
13
|
+
return "github";
|
|
14
|
+
if (url.includes("dev.azure.com") || url.includes("visualstudio.com"))
|
|
15
|
+
return "azure";
|
|
16
|
+
if (url.includes("gitlab.com") || /\bgitlab\b/.test(url))
|
|
17
|
+
return "gitlab";
|
|
18
|
+
if (url.includes("bitbucket.org"))
|
|
19
|
+
return "bitbucket";
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate that the CLI for the detected git provider is installed and authenticated.
|
|
28
|
+
* Returns the provider name or throws with instructions.
|
|
29
|
+
*/
|
|
30
|
+
export function validateGitProviderCli(provider, cwd) {
|
|
31
|
+
switch (provider) {
|
|
32
|
+
case "github": {
|
|
33
|
+
try {
|
|
34
|
+
execFileSync("gh", ["auth", "status"], { cwd, stdio: "pipe" });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error("GitHub CLI (gh) is not authenticated. Run: gh auth login");
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case "azure": {
|
|
42
|
+
try {
|
|
43
|
+
execFileSync("az", ["--version"], { cwd, stdio: "pipe" });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error("Azure CLI (az) not found. Install it: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli");
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
execFileSync("az", ["account", "show"], { cwd, stdio: "pipe" });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error("Azure CLI is not authenticated. Run: az login");
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "gitlab": {
|
|
57
|
+
try {
|
|
58
|
+
execFileSync("glab", ["auth", "status"], { cwd, stdio: "pipe" });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error("GitLab CLI (glab) is not installed or not authenticated. Install: https://gitlab.com/gitlab-org/cli — then run: glab auth login");
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "bitbucket":
|
|
66
|
+
case "unknown":
|
|
67
|
+
log.warn(TAG, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const VALID_PR_URL_RE = /^https:\/\/(github\.com|gitlab\.com|dev\.azure\.com|bitbucket\.org)\//;
|
|
72
|
+
const PR_URL_RE = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
73
|
+
/** Validate that a PR URL looks like a real provider URL. */
|
|
74
|
+
function isValidPrUrl(url) {
|
|
75
|
+
return VALID_PR_URL_RE.test(url);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check whether a PR has been merged using the git provider CLI (async).
|
|
79
|
+
*/
|
|
80
|
+
export async function checkPrMergeStatus(prUrl, cwd, provider) {
|
|
81
|
+
if (!isValidPrUrl(prUrl))
|
|
82
|
+
return "unknown";
|
|
83
|
+
try {
|
|
84
|
+
switch (provider) {
|
|
85
|
+
case "github": {
|
|
86
|
+
const { stdout } = await execFileAsync("gh", ["pr", "view", prUrl, "--json", "state", "--jq", ".state"], { cwd, encoding: "utf-8", timeout: 10_000 });
|
|
87
|
+
switch (stdout.trim()) {
|
|
88
|
+
case "MERGED":
|
|
89
|
+
return "merged";
|
|
90
|
+
case "OPEN":
|
|
91
|
+
return "open";
|
|
92
|
+
case "CLOSED":
|
|
93
|
+
return "closed";
|
|
94
|
+
default:
|
|
95
|
+
return "unknown";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
case "gitlab": {
|
|
99
|
+
// Extract MR ID from URL (e.g. .../merge_requests/42)
|
|
100
|
+
const mrMatch = prUrl.match(/merge_requests\/(\d+)/);
|
|
101
|
+
if (!mrMatch)
|
|
102
|
+
return "unknown";
|
|
103
|
+
const { stdout } = await execFileAsync("glab", ["mr", "view", mrMatch[1], "--output", "json"], { cwd, encoding: "utf-8", timeout: 10_000 });
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(stdout.trim());
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
log.warn(TAG, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
|
|
110
|
+
return "unknown";
|
|
111
|
+
}
|
|
112
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
113
|
+
return "unknown";
|
|
114
|
+
const state = parsed.state;
|
|
115
|
+
if (state === "merged")
|
|
116
|
+
return "merged";
|
|
117
|
+
if (state === "opened")
|
|
118
|
+
return "open";
|
|
119
|
+
if (state === "closed")
|
|
120
|
+
return "closed";
|
|
121
|
+
return "unknown";
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
return "unknown";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return "unknown";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Extract a PR URL from card description. Matches the format written by completion.ts:
|
|
133
|
+
* `PR: https://...`
|
|
134
|
+
*/
|
|
135
|
+
export function extractPrUrl(description) {
|
|
136
|
+
if (!description)
|
|
137
|
+
return null;
|
|
138
|
+
const match = description.match(PR_URL_RE);
|
|
139
|
+
if (!match)
|
|
140
|
+
return null;
|
|
141
|
+
try {
|
|
142
|
+
return new URL(match[1]).href;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ============ PUSH (REWORK-AWARE) ============
|
|
149
|
+
export function remoteBranchExists(branchName, cwd) {
|
|
150
|
+
try {
|
|
151
|
+
execFileSync("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export function pushBranch(branchName, cwd) {
|
|
159
|
+
if (remoteBranchExists(branchName, cwd)) {
|
|
160
|
+
log.info(TAG, `Remote branch ${branchName} exists (rework), force-pushing`);
|
|
161
|
+
execFileSync("git", ["push", "--force-with-lease", "-u", "origin", branchName], { cwd, stdio: "pipe" });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
execFileSync("git", ["push", "-u", "origin", branchName], {
|
|
165
|
+
cwd,
|
|
166
|
+
stdio: "pipe",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ============ PR CREATION (PROVIDER-AWARE) ============
|
|
171
|
+
export function buildPrBody(card, commitLog) {
|
|
172
|
+
return [
|
|
173
|
+
"## Summary",
|
|
174
|
+
"",
|
|
175
|
+
`Automated PR for card **#${card.short_id} — ${card.title}**.`,
|
|
176
|
+
"",
|
|
177
|
+
"## Commits",
|
|
178
|
+
"",
|
|
179
|
+
"```",
|
|
180
|
+
commitLog,
|
|
181
|
+
"```",
|
|
182
|
+
"",
|
|
183
|
+
"## Card",
|
|
184
|
+
"",
|
|
185
|
+
card.description?.slice(0, 500) || "No description.",
|
|
186
|
+
"",
|
|
187
|
+
"---",
|
|
188
|
+
"*Created by Harmony Agent Daemon*",
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
export function createPullRequest(card, branchName, worktreePath, config, provider) {
|
|
192
|
+
let commitLog = "";
|
|
193
|
+
try {
|
|
194
|
+
commitLog = execFileSync("git", ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
commitLog = "(unable to retrieve commit log)";
|
|
198
|
+
}
|
|
199
|
+
const title = `#${card.short_id} ${card.title}`;
|
|
200
|
+
const body = buildPrBody(card, commitLog);
|
|
201
|
+
const base = config.worktree.baseBranch;
|
|
202
|
+
// Check for existing PR first (rework case)
|
|
203
|
+
const existingUrl = findExistingPr(branchName, worktreePath, provider);
|
|
204
|
+
if (existingUrl) {
|
|
205
|
+
log.info(TAG, `PR already exists for ${branchName}, updating body...`);
|
|
206
|
+
updateExistingPr(branchName, body, worktreePath, provider);
|
|
207
|
+
return existingUrl;
|
|
208
|
+
}
|
|
209
|
+
// No existing PR — create a new one
|
|
210
|
+
try {
|
|
211
|
+
let result;
|
|
212
|
+
switch (provider) {
|
|
213
|
+
case "github":
|
|
214
|
+
result = execFileSync("gh", ["pr", "create", "--title", title, "--body", body, "--base", base], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
215
|
+
break;
|
|
216
|
+
case "azure": {
|
|
217
|
+
const azOutput = execFileSync("az", [
|
|
218
|
+
"repos",
|
|
219
|
+
"pr",
|
|
220
|
+
"create",
|
|
221
|
+
"--title",
|
|
222
|
+
title,
|
|
223
|
+
"--description",
|
|
224
|
+
body,
|
|
225
|
+
"--source-branch",
|
|
226
|
+
branchName,
|
|
227
|
+
"--target-branch",
|
|
228
|
+
base,
|
|
229
|
+
"--auto-complete",
|
|
230
|
+
"false",
|
|
231
|
+
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
232
|
+
// az repos pr create returns JSON — extract the URL
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(azOutput);
|
|
235
|
+
result = parsed.remoteUrl ?? parsed.url ?? azOutput;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
result = azOutput;
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "gitlab":
|
|
243
|
+
result = execFileSync("glab", [
|
|
244
|
+
"mr",
|
|
245
|
+
"create",
|
|
246
|
+
"--title",
|
|
247
|
+
title,
|
|
248
|
+
"--description",
|
|
249
|
+
body,
|
|
250
|
+
"--source-branch",
|
|
251
|
+
branchName,
|
|
252
|
+
"--target-branch",
|
|
253
|
+
base,
|
|
254
|
+
"--no-editor",
|
|
255
|
+
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
256
|
+
break;
|
|
257
|
+
default:
|
|
258
|
+
log.warn(TAG, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
log.info(TAG, `PR created: ${result}`);
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
log.error(TAG, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export function findExistingPr(branchName, worktreePath, provider) {
|
|
270
|
+
try {
|
|
271
|
+
switch (provider) {
|
|
272
|
+
case "github":
|
|
273
|
+
return execFileSync("gh", ["pr", "view", branchName, "--json", "url", "--jq", ".url"], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
274
|
+
case "gitlab": {
|
|
275
|
+
const json = execFileSync("glab", ["mr", "view", branchName, "--output", "json"], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
276
|
+
const parsed = JSON.parse(json);
|
|
277
|
+
return parsed.web_url || null;
|
|
278
|
+
}
|
|
279
|
+
default:
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export function updateExistingPr(branchName, body, worktreePath, provider) {
|
|
288
|
+
try {
|
|
289
|
+
switch (provider) {
|
|
290
|
+
case "github":
|
|
291
|
+
execFileSync("gh", ["pr", "edit", branchName, "--body", body], {
|
|
292
|
+
cwd: worktreePath,
|
|
293
|
+
stdio: "pipe",
|
|
294
|
+
});
|
|
295
|
+
break;
|
|
296
|
+
case "gitlab":
|
|
297
|
+
execFileSync("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
log.info(TAG, `Updated existing PR body for ${branchName}`);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
log.warn(TAG, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|