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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glab-agent",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
5
  "description": "Multi-agent GitLab To-Do watcher with YAML-defined agents, skills, and GitLab registry.",
6
6
  "license": "MIT",
@@ -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
- return Number.isNaN(id) ? undefined : { id };
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 dependencies.worktreeManager.ensureWorktree(targetIid, `mr-${targetIid}`);
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 dependencies.worktreeManager.ensureWorktree(issue.iid, issue.title);
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 dependencies.worktreeManager.ensureWorktree(targetIid, worktreeTitle);
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 bio with agent profile (P1: Agent = 员工)
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) {