glab-agent 0.2.11 → 0.2.12
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 +67 -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
|
|
@@ -429,7 +479,9 @@ export async function runWatcherCycle(
|
|
|
429
479
|
webUrl: ""
|
|
430
480
|
};
|
|
431
481
|
|
|
432
|
-
const worktree = await
|
|
482
|
+
const { worktree } = await resolveTargetWorktree(
|
|
483
|
+
candidate.projectId, targetIid, `mr-${targetIid}`, config, dependencies, logger
|
|
484
|
+
);
|
|
433
485
|
state.activeRun = {
|
|
434
486
|
pid,
|
|
435
487
|
todoId: candidate.id,
|
|
@@ -456,7 +508,7 @@ export async function runWatcherCycle(
|
|
|
456
508
|
logger.info(`Starting agent for MR !${targetIid} branch=${worktree.branch}`);
|
|
457
509
|
|
|
458
510
|
try {
|
|
459
|
-
const result = await dependencies.agentRunner.run(issue, worktree, { todoId: candidate.id });
|
|
511
|
+
const result = await dependencies.agentRunner.run(issue, worktree, { todoId: candidate.id, gitlabProjectId: candidate.projectId });
|
|
460
512
|
logger.info(`Agent completed MR !${targetIid}. Summary: ${result.summary.slice(0, 200)}`);
|
|
461
513
|
} catch (error) {
|
|
462
514
|
const summary = error instanceof AgentRunnerError ? error.summary : String(error);
|
|
@@ -486,7 +538,9 @@ export async function runWatcherCycle(
|
|
|
486
538
|
logger.info(`Issue #${issue.iid}: label mismatch, passing hint to agent`);
|
|
487
539
|
}
|
|
488
540
|
|
|
489
|
-
const worktree = await
|
|
541
|
+
const { worktree } = await resolveTargetWorktree(
|
|
542
|
+
issue.projectId, issue.iid, issue.title, config, dependencies, logger
|
|
543
|
+
);
|
|
490
544
|
|
|
491
545
|
state.activeRun = {
|
|
492
546
|
pid,
|
|
@@ -566,7 +620,7 @@ export async function runWatcherCycle(
|
|
|
566
620
|
|
|
567
621
|
try {
|
|
568
622
|
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 })),
|
|
623
|
+
dependencies.agentRunner.run(issue, worktree, { todoId: candidate.id, signal: abortController.signal, labelMismatchHint, gitlabProjectId: issue.projectId }).then(r => ({ type: "completed" as const, result: r })),
|
|
570
624
|
closeDetection.then(() => ({ type: "closed" as const }))
|
|
571
625
|
]);
|
|
572
626
|
|
|
@@ -860,7 +914,10 @@ export async function runConcurrentWatcherCycle(
|
|
|
860
914
|
|
|
861
915
|
// Prepare and spawn
|
|
862
916
|
const worktreeTitle = isIssueTodo ? issue.title : `mr-${targetIid}`;
|
|
863
|
-
const worktree = await
|
|
917
|
+
const { worktree } = await resolveTargetWorktree(
|
|
918
|
+
isIssueTodo ? issue.projectId : candidate.projectId,
|
|
919
|
+
targetIid, worktreeTitle, config, dependencies, logger
|
|
920
|
+
);
|
|
864
921
|
|
|
865
922
|
const skills = config.agentDefinition?.skills ?? [];
|
|
866
923
|
if (skills.length > 0) {
|
|
@@ -870,7 +927,7 @@ export async function runConcurrentWatcherCycle(
|
|
|
870
927
|
} catch (err) { logger.warn(`Failed to inject skill files: ${String(err)}`); }
|
|
871
928
|
}
|
|
872
929
|
|
|
873
|
-
const spawnedRun = await dependencies.agentRunner.spawn(issue, worktree, { todoId: candidate.id, labelMismatchHint });
|
|
930
|
+
const spawnedRun = await dependencies.agentRunner.spawn(issue, worktree, { todoId: candidate.id, labelMismatchHint, gitlabProjectId: isIssueTodo ? issue.projectId : candidate.projectId });
|
|
874
931
|
const now = new Date().toISOString();
|
|
875
932
|
|
|
876
933
|
spawnedRuns.set(issue.iid, {
|
|
@@ -1602,6 +1659,9 @@ function createDependencies(config: LocalAgentConfig, logger?: Logger): WatcherD
|
|
|
1602
1659
|
agentName: config.agentDefinition?.name
|
|
1603
1660
|
}),
|
|
1604
1661
|
agentRunner,
|
|
1662
|
+
repoCache: new RepoCache({
|
|
1663
|
+
cacheDir: path.join(config.agentRepoPath, ".glab-agent", "repo-cache")
|
|
1664
|
+
}),
|
|
1605
1665
|
logger
|
|
1606
1666
|
};
|
|
1607
1667
|
}
|
|
@@ -1805,9 +1865,8 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
|
|
1805
1865
|
config.botName = botUser.name;
|
|
1806
1866
|
logger.info(`Token validated. Bot identity: @${botUser.username} (${botUser.name})`);
|
|
1807
1867
|
|
|
1808
|
-
// Update GitLab user
|
|
1868
|
+
// Update GitLab user profile (P1: Agent = 员工)
|
|
1809
1869
|
await updateAgentUserBio(config, dependencies);
|
|
1810
|
-
await publishAgentProfileWiki(config, dependencies);
|
|
1811
1870
|
await publishProfileReadme(config, dependencies);
|
|
1812
1871
|
await updateAgentUserStatus(dependencies, "idle", undefined, config);
|
|
1813
1872
|
} catch (error) {
|