glab-agent 0.2.11 → 0.2.13
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/package.json +1 -1
- package/src/local-agent/agent-runner.ts +6 -1
- package/src/local-agent/claude-runner.ts +2 -2
- package/src/local-agent/codex-runner.ts +2 -2
- package/src/local-agent/gitlab-glab-client.ts +8 -3
- package/src/local-agent/repo-cache.ts +15 -0
- package/src/local-agent/watcher.ts +79 -8
package/package.json
CHANGED
|
@@ -24,6 +24,10 @@ export interface AgentRunContext {
|
|
|
24
24
|
signal?: AbortSignal;
|
|
25
25
|
/** Hint when issue labels don't match trigger config. Passed to agent prompt. */
|
|
26
26
|
labelMismatchHint?: string;
|
|
27
|
+
/** Path to the target project's repo (cached bare clone). Used by runners for git operations. */
|
|
28
|
+
targetRepoPath?: string;
|
|
29
|
+
/** Override gitlabProjectId for the prompt (target project, not agents repo) */
|
|
30
|
+
gitlabProjectId?: number;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export interface SpawnedRunResult {
|
|
@@ -230,7 +234,8 @@ export function buildBasePrompt(ctx: AgentPromptContext): string {
|
|
|
230
234
|
"",
|
|
231
235
|
"## 当前环境",
|
|
232
236
|
`- 分支: ${ctx.branch}`,
|
|
233
|
-
"- 工作目录:
|
|
237
|
+
"- 工作目录: issue 对应项目的独立工作副本",
|
|
238
|
+
"- 如果任务涉及其他仓库,可以用 git clone 拉取(GITLAB_TOKEN 已配置,支持认证)",
|
|
234
239
|
ctx.labelMismatchHint ? `\n## 注意\n${ctx.labelMismatchHint}` : "",
|
|
235
240
|
"",
|
|
236
241
|
"## 最终输出",
|
|
@@ -67,7 +67,7 @@ export class ClaudeRunner implements AgentRunner {
|
|
|
67
67
|
issueIid: issue.iid,
|
|
68
68
|
issueTitle: issue.title,
|
|
69
69
|
gitlabHost: this.gitlabHost,
|
|
70
|
-
gitlabProjectId: this.gitlabProjectId,
|
|
70
|
+
gitlabProjectId: context?.gitlabProjectId ?? this.gitlabProjectId,
|
|
71
71
|
worktreePath: worktree.worktreePath,
|
|
72
72
|
branch: worktree.branch,
|
|
73
73
|
provider: "claude",
|
|
@@ -151,7 +151,7 @@ export class ClaudeRunner implements AgentRunner {
|
|
|
151
151
|
issueIid: issue.iid,
|
|
152
152
|
issueTitle: issue.title,
|
|
153
153
|
gitlabHost: this.gitlabHost,
|
|
154
|
-
gitlabProjectId: this.gitlabProjectId,
|
|
154
|
+
gitlabProjectId: context?.gitlabProjectId ?? this.gitlabProjectId,
|
|
155
155
|
worktreePath: worktree.worktreePath,
|
|
156
156
|
branch: worktree.branch,
|
|
157
157
|
provider: "claude",
|
|
@@ -67,7 +67,7 @@ export class CodexRunner implements AgentRunner {
|
|
|
67
67
|
issueIid: issue.iid,
|
|
68
68
|
issueTitle: issue.title,
|
|
69
69
|
gitlabHost: this.gitlabHost,
|
|
70
|
-
gitlabProjectId: this.gitlabProjectId,
|
|
70
|
+
gitlabProjectId: context?.gitlabProjectId ?? this.gitlabProjectId,
|
|
71
71
|
worktreePath: worktree.worktreePath,
|
|
72
72
|
branch: worktree.branch,
|
|
73
73
|
provider: "codex",
|
|
@@ -154,7 +154,7 @@ export class CodexRunner implements AgentRunner {
|
|
|
154
154
|
issueIid: issue.iid,
|
|
155
155
|
issueTitle: issue.title,
|
|
156
156
|
gitlabHost: this.gitlabHost,
|
|
157
|
-
gitlabProjectId: this.gitlabProjectId,
|
|
157
|
+
gitlabProjectId: context?.gitlabProjectId ?? this.gitlabProjectId,
|
|
158
158
|
worktreePath: worktree.worktreePath,
|
|
159
159
|
branch: worktree.branch,
|
|
160
160
|
provider: "codex",
|
|
@@ -101,7 +101,7 @@ export interface GitlabClient {
|
|
|
101
101
|
getWikiPage(projectId: number | string, slug: string): Promise<WikiPage>;
|
|
102
102
|
createWikiPage(projectId: number | string, title: string, content: string): Promise<WikiPage>;
|
|
103
103
|
updateWikiPage(projectId: number | string, slug: string, title: string, content: string): Promise<WikiPage>;
|
|
104
|
-
getProject(projectIdOrPath: number | string): Promise<{ id: number } | undefined>;
|
|
104
|
+
getProject(projectIdOrPath: number | string): Promise<{ id: number; httpUrlToRepo?: string; pathWithNamespace?: string } | undefined>;
|
|
105
105
|
createProject(name: string, options?: { visibility?: string; initializeWithReadme?: boolean }): Promise<{ id: number }>;
|
|
106
106
|
getRepositoryFile(projectId: number | string, filePath: string, ref?: string): Promise<{ content: string } | undefined>;
|
|
107
107
|
createOrUpdateRepositoryFile(projectId: number | string, filePath: string, content: string, commitMessage: string, options?: { create?: boolean }): Promise<void>;
|
|
@@ -723,13 +723,18 @@ export class GitlabGlabClient implements GitlabClient {
|
|
|
723
723
|
};
|
|
724
724
|
}
|
|
725
725
|
|
|
726
|
-
async getProject(projectIdOrPath: number | string): Promise<{ id: number } | undefined> {
|
|
726
|
+
async getProject(projectIdOrPath: number | string): Promise<{ id: number; httpUrlToRepo?: string; pathWithNamespace?: string } | undefined> {
|
|
727
727
|
try {
|
|
728
728
|
const encoded = typeof projectIdOrPath === "string" ? encodeURIComponent(projectIdOrPath) : projectIdOrPath;
|
|
729
729
|
const payload = await this.readJson(`projects/${encoded}`);
|
|
730
730
|
const p = (payload ?? {}) as Payload;
|
|
731
731
|
const id = Number(p.id);
|
|
732
|
-
|
|
732
|
+
if (Number.isNaN(id)) return undefined;
|
|
733
|
+
return {
|
|
734
|
+
id,
|
|
735
|
+
httpUrlToRepo: typeof p.http_url_to_repo === "string" ? p.http_url_to_repo as string : undefined,
|
|
736
|
+
pathWithNamespace: typeof p.path_with_namespace === "string" ? p.path_with_namespace as string : undefined,
|
|
737
|
+
};
|
|
733
738
|
} catch {
|
|
734
739
|
return undefined;
|
|
735
740
|
}
|
|
@@ -122,6 +122,21 @@ export class RepoCache {
|
|
|
122
122
|
return true;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Build a git clone URL with embedded token for authentication.
|
|
127
|
+
* e.g., https://oauth2:TOKEN@gitlab.example.com/group/project.git
|
|
128
|
+
*/
|
|
129
|
+
static authenticatedUrl(httpUrl: string, token: string): string {
|
|
130
|
+
try {
|
|
131
|
+
const url = new URL(httpUrl);
|
|
132
|
+
url.username = "oauth2";
|
|
133
|
+
url.password = token;
|
|
134
|
+
return url.toString();
|
|
135
|
+
} catch {
|
|
136
|
+
return httpUrl;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
/**
|
|
126
141
|
* Get the cache directory path for a given repo URL.
|
|
127
142
|
* Uses SHA-256 hash of the URL for the directory name.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
5
6
|
import { AgentRunnerError, type AgentRunner, type SpawnedRun } from "./agent-runner.js";
|
|
@@ -33,6 +34,7 @@ import { rotateIfNeeded } from "./log-rotate.js";
|
|
|
33
34
|
import { validateToken } from "./token-check.js";
|
|
34
35
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
35
36
|
import type { WorktreeInfo } from "./worktree-manager.js";
|
|
37
|
+
import { RepoCache } from "./repo-cache.js";
|
|
36
38
|
import { injectSkillFiles } from "./skill-inject.js";
|
|
37
39
|
import { startHealthServer } from "./health-server.js";
|
|
38
40
|
import type { HealthStatus } from "./health-server.js";
|
|
@@ -103,6 +105,8 @@ export interface WatcherDependencies {
|
|
|
103
105
|
issueCloseCheckIntervalMs?: number;
|
|
104
106
|
/** Feishu app client for targeted personal notifications */
|
|
105
107
|
feishuClient?: import("./feishu-client.js").FeishuClient;
|
|
108
|
+
/** Cache for bare clones of target project repos */
|
|
109
|
+
repoCache?: RepoCache;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
export interface WatcherCycleResult {
|
|
@@ -338,6 +342,52 @@ async function replaceReactionOnTodo(
|
|
|
338
342
|
}
|
|
339
343
|
}
|
|
340
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Resolve the target project's repo and create a worktree for the given issue/MR.
|
|
347
|
+
* Uses RepoCache to clone/fetch the target project, then creates a worktree from it.
|
|
348
|
+
* Falls back to the agents repo worktree if the target project can't be resolved.
|
|
349
|
+
*/
|
|
350
|
+
async function resolveTargetWorktree(
|
|
351
|
+
projectId: number,
|
|
352
|
+
iid: number,
|
|
353
|
+
title: string,
|
|
354
|
+
config: LocalAgentConfig,
|
|
355
|
+
dependencies: WatcherDependencies,
|
|
356
|
+
logger: Logger | typeof console
|
|
357
|
+
): Promise<{ worktree: WorktreeInfo }> {
|
|
358
|
+
if (dependencies.repoCache) {
|
|
359
|
+
try {
|
|
360
|
+
const project = await dependencies.gitlabClient.getProject(projectId);
|
|
361
|
+
if (project?.httpUrlToRepo) {
|
|
362
|
+
const authedUrl = RepoCache.authenticatedUrl(project.httpUrlToRepo, config.gitlabToken);
|
|
363
|
+
const cachedRepoPath = await dependencies.repoCache.ensureRepo(authedUrl);
|
|
364
|
+
|
|
365
|
+
const agentName = config.agentDefinition?.name;
|
|
366
|
+
const worktreeRoot = path.join(config.agentRepoPath, ".glab-agent", "worktrees", agentName ?? "agent");
|
|
367
|
+
|
|
368
|
+
await mkdir(worktreeRoot, { recursive: true });
|
|
369
|
+
|
|
370
|
+
// Use a WorktreeManager pointing at the cached bare repo
|
|
371
|
+
const targetWtm = new WorktreeManager({
|
|
372
|
+
repoPath: cachedRepoPath,
|
|
373
|
+
worktreeRoot,
|
|
374
|
+
agentName,
|
|
375
|
+
});
|
|
376
|
+
const worktree = await targetWtm.ensureWorktree(iid, title);
|
|
377
|
+
|
|
378
|
+
(logger as Logger | undefined)?.info?.(`Target repo resolved: ${project.pathWithNamespace ?? projectId} → ${cachedRepoPath}`);
|
|
379
|
+
return { worktree };
|
|
380
|
+
}
|
|
381
|
+
} catch (err) {
|
|
382
|
+
(logger as Logger | undefined)?.warn?.(`Failed to resolve target repo for project ${projectId}, falling back to agents repo: ${String(err).slice(0, 150)}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Fallback: use agents repo worktree (original behavior)
|
|
387
|
+
const worktree = await dependencies.worktreeManager.ensureWorktree(iid, title);
|
|
388
|
+
return { worktree };
|
|
389
|
+
}
|
|
390
|
+
|
|
341
391
|
export async function runWatcherCycle(
|
|
342
392
|
config: LocalAgentConfig,
|
|
343
393
|
dependencies: WatcherDependencies
|
|
@@ -346,6 +396,13 @@ export async function runWatcherCycle(
|
|
|
346
396
|
const logger = dependencies.logger ?? console;
|
|
347
397
|
const state = await dependencies.stateStore.load();
|
|
348
398
|
|
|
399
|
+
// When Feishu app client is configured, suppress group webhook notifications —
|
|
400
|
+
// personal Feishu cards already cover the full lifecycle (accepted/running/completed/failed).
|
|
401
|
+
// Mutate a local copy so the original config is untouched.
|
|
402
|
+
if (dependencies.feishuClient && config.webhookUrl) {
|
|
403
|
+
config = { ...config, webhookUrl: undefined };
|
|
404
|
+
}
|
|
405
|
+
|
|
349
406
|
if (state.activeRun && isPidAlive(state.activeRun.pid) && state.activeRun.pid !== pid) {
|
|
350
407
|
logger.info(`Watcher is busy with issue #${state.activeRun.issueIid}.`);
|
|
351
408
|
await updateAgentUserStatus(dependencies, "busy", `#${state.activeRun.issueIid}`);
|
|
@@ -429,7 +486,9 @@ export async function runWatcherCycle(
|
|
|
429
486
|
webUrl: ""
|
|
430
487
|
};
|
|
431
488
|
|
|
432
|
-
const worktree = await
|
|
489
|
+
const { worktree } = await resolveTargetWorktree(
|
|
490
|
+
candidate.projectId, targetIid, `mr-${targetIid}`, config, dependencies, logger
|
|
491
|
+
);
|
|
433
492
|
state.activeRun = {
|
|
434
493
|
pid,
|
|
435
494
|
todoId: candidate.id,
|
|
@@ -456,7 +515,7 @@ export async function runWatcherCycle(
|
|
|
456
515
|
logger.info(`Starting agent for MR !${targetIid} branch=${worktree.branch}`);
|
|
457
516
|
|
|
458
517
|
try {
|
|
459
|
-
const result = await dependencies.agentRunner.run(issue, worktree, { todoId: candidate.id });
|
|
518
|
+
const result = await dependencies.agentRunner.run(issue, worktree, { todoId: candidate.id, gitlabProjectId: candidate.projectId });
|
|
460
519
|
logger.info(`Agent completed MR !${targetIid}. Summary: ${result.summary.slice(0, 200)}`);
|
|
461
520
|
} catch (error) {
|
|
462
521
|
const summary = error instanceof AgentRunnerError ? error.summary : String(error);
|
|
@@ -486,7 +545,9 @@ export async function runWatcherCycle(
|
|
|
486
545
|
logger.info(`Issue #${issue.iid}: label mismatch, passing hint to agent`);
|
|
487
546
|
}
|
|
488
547
|
|
|
489
|
-
const worktree = await
|
|
548
|
+
const { worktree } = await resolveTargetWorktree(
|
|
549
|
+
issue.projectId, issue.iid, issue.title, config, dependencies, logger
|
|
550
|
+
);
|
|
490
551
|
|
|
491
552
|
state.activeRun = {
|
|
492
553
|
pid,
|
|
@@ -566,7 +627,7 @@ export async function runWatcherCycle(
|
|
|
566
627
|
|
|
567
628
|
try {
|
|
568
629
|
const raceResult = await Promise.race([
|
|
569
|
-
dependencies.agentRunner.run(issue, worktree, { todoId: candidate.id, signal: abortController.signal, labelMismatchHint }).then(r => ({ type: "completed" as const, result: r })),
|
|
630
|
+
dependencies.agentRunner.run(issue, worktree, { todoId: candidate.id, signal: abortController.signal, labelMismatchHint, gitlabProjectId: issue.projectId }).then(r => ({ type: "completed" as const, result: r })),
|
|
570
631
|
closeDetection.then(() => ({ type: "closed" as const }))
|
|
571
632
|
]);
|
|
572
633
|
|
|
@@ -712,6 +773,11 @@ export async function runConcurrentWatcherCycle(
|
|
|
712
773
|
const state = await dependencies.stateStore.load();
|
|
713
774
|
const concurrency = config.agentDefinition?.concurrency ?? 1;
|
|
714
775
|
|
|
776
|
+
// Suppress group webhook when Feishu personal cards are active
|
|
777
|
+
if (dependencies.feishuClient && config.webhookUrl) {
|
|
778
|
+
config = { ...config, webhookUrl: undefined };
|
|
779
|
+
}
|
|
780
|
+
|
|
715
781
|
// 1. Collect finished tasks
|
|
716
782
|
for (const [issueIid, spawned] of spawnedRuns) {
|
|
717
783
|
const result = await spawned.run.poll();
|
|
@@ -860,7 +926,10 @@ export async function runConcurrentWatcherCycle(
|
|
|
860
926
|
|
|
861
927
|
// Prepare and spawn
|
|
862
928
|
const worktreeTitle = isIssueTodo ? issue.title : `mr-${targetIid}`;
|
|
863
|
-
const worktree = await
|
|
929
|
+
const { worktree } = await resolveTargetWorktree(
|
|
930
|
+
isIssueTodo ? issue.projectId : candidate.projectId,
|
|
931
|
+
targetIid, worktreeTitle, config, dependencies, logger
|
|
932
|
+
);
|
|
864
933
|
|
|
865
934
|
const skills = config.agentDefinition?.skills ?? [];
|
|
866
935
|
if (skills.length > 0) {
|
|
@@ -870,7 +939,7 @@ export async function runConcurrentWatcherCycle(
|
|
|
870
939
|
} catch (err) { logger.warn(`Failed to inject skill files: ${String(err)}`); }
|
|
871
940
|
}
|
|
872
941
|
|
|
873
|
-
const spawnedRun = await dependencies.agentRunner.spawn(issue, worktree, { todoId: candidate.id, labelMismatchHint });
|
|
942
|
+
const spawnedRun = await dependencies.agentRunner.spawn(issue, worktree, { todoId: candidate.id, labelMismatchHint, gitlabProjectId: isIssueTodo ? issue.projectId : candidate.projectId });
|
|
874
943
|
const now = new Date().toISOString();
|
|
875
944
|
|
|
876
945
|
spawnedRuns.set(issue.iid, {
|
|
@@ -1602,6 +1671,9 @@ function createDependencies(config: LocalAgentConfig, logger?: Logger): WatcherD
|
|
|
1602
1671
|
agentName: config.agentDefinition?.name
|
|
1603
1672
|
}),
|
|
1604
1673
|
agentRunner,
|
|
1674
|
+
repoCache: new RepoCache({
|
|
1675
|
+
cacheDir: path.join(config.agentRepoPath, ".glab-agent", "repo-cache")
|
|
1676
|
+
}),
|
|
1605
1677
|
logger
|
|
1606
1678
|
};
|
|
1607
1679
|
}
|
|
@@ -1805,9 +1877,8 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
|
|
1805
1877
|
config.botName = botUser.name;
|
|
1806
1878
|
logger.info(`Token validated. Bot identity: @${botUser.username} (${botUser.name})`);
|
|
1807
1879
|
|
|
1808
|
-
// Update GitLab user
|
|
1880
|
+
// Update GitLab user profile (P1: Agent = 员工)
|
|
1809
1881
|
await updateAgentUserBio(config, dependencies);
|
|
1810
|
-
await publishAgentProfileWiki(config, dependencies);
|
|
1811
1882
|
await publishProfileReadme(config, dependencies);
|
|
1812
1883
|
await updateAgentUserStatus(dependencies, "idle", undefined, config);
|
|
1813
1884
|
} catch (error) {
|