glab-agent 0.2.10 → 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/README.md +10 -0
- package/package.json +3 -1
- package/src/local-agent/agent-runner.ts +11 -1
- package/src/local-agent/claude-runner.ts +4 -2
- package/src/local-agent/cli.ts +111 -0
- package/src/local-agent/codex-runner.ts +4 -2
- package/src/local-agent/gitlab-glab-client.ts +8 -3
- package/src/local-agent/repo-cache.ts +15 -0
- package/src/local-agent/report.ts +66 -1
- package/src/local-agent/watcher.ts +149 -90
package/README.md
CHANGED
|
@@ -153,6 +153,16 @@ No extra dashboards needed. glab-agent maps to GitLab's native features:
|
|
|
153
153
|
- **Metrics** — Append-only JSONL per agent (`.glab-agent/metrics/`)
|
|
154
154
|
- **Heartbeat** — Cycle count and last error in `.glab-agent/heartbeat/`
|
|
155
155
|
|
|
156
|
+
## Documentation
|
|
157
|
+
|
|
158
|
+
Development documentation lives in [`docs/`](docs/index.md):
|
|
159
|
+
|
|
160
|
+
- **[Design Principles](docs/design-principles.md)** — P1-P17, architecture decision criteria
|
|
161
|
+
- **[Test Strategy](docs/test-strategy.md)** — coverage policy, key modules, known gaps
|
|
162
|
+
- **[Harness Engineering](docs/harness-engineering.md)** — human + AI collaboration methodology
|
|
163
|
+
|
|
164
|
+
Run `glab-agent doctor` for environment diagnostics.
|
|
165
|
+
|
|
156
166
|
## Requirements
|
|
157
167
|
|
|
158
168
|
- Node.js >= 20
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glab-agent",
|
|
3
|
-
"version": "0.2.
|
|
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",
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
"prepare": "git config core.hooksPath scripts/hooks",
|
|
35
35
|
"build": "tsc -p tsconfig.build.json",
|
|
36
36
|
"test": "vitest run",
|
|
37
|
+
"test:coverage": "vitest run --coverage",
|
|
37
38
|
"agent": "tsx src/local-agent/cli.ts",
|
|
39
|
+
"check:deliver": "zsh scripts/delivery-check.sh",
|
|
38
40
|
"smoke-test": "zsh scripts/smoke-test.sh",
|
|
39
41
|
"smoke-test:full": "zsh scripts/smoke-test.sh --wait-finish",
|
|
40
42
|
"docs-lint": "zsh scripts/docs-lint.sh",
|
|
@@ -22,6 +22,12 @@ export interface AgentRunResult {
|
|
|
22
22
|
export interface AgentRunContext {
|
|
23
23
|
todoId?: number;
|
|
24
24
|
signal?: AbortSignal;
|
|
25
|
+
/** Hint when issue labels don't match trigger config. Passed to agent prompt. */
|
|
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;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export interface SpawnedRunResult {
|
|
@@ -116,6 +122,8 @@ export interface AgentPromptContext {
|
|
|
116
122
|
append?: string;
|
|
117
123
|
templatePath?: string;
|
|
118
124
|
labels?: AgentLabelConfig;
|
|
125
|
+
/** Hint when issue labels don't match trigger config. Agent decides how to respond. */
|
|
126
|
+
labelMismatchHint?: string;
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
export interface ContextualPromptContext {
|
|
@@ -226,7 +234,9 @@ export function buildBasePrompt(ctx: AgentPromptContext): string {
|
|
|
226
234
|
"",
|
|
227
235
|
"## 当前环境",
|
|
228
236
|
`- 分支: ${ctx.branch}`,
|
|
229
|
-
"- 工作目录:
|
|
237
|
+
"- 工作目录: issue 对应项目的独立工作副本",
|
|
238
|
+
"- 如果任务涉及其他仓库,可以用 git clone 拉取(GITLAB_TOKEN 已配置,支持认证)",
|
|
239
|
+
ctx.labelMismatchHint ? `\n## 注意\n${ctx.labelMismatchHint}` : "",
|
|
230
240
|
"",
|
|
231
241
|
"## 最终输出",
|
|
232
242
|
"标准输出不超过 5 行的简短总结(不是 GitLab 评论)。"
|
|
@@ -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",
|
|
@@ -75,6 +75,7 @@ export class ClaudeRunner implements AgentRunner {
|
|
|
75
75
|
preamble: this.agentDefinition?.prompt.preamble,
|
|
76
76
|
append: this.agentDefinition?.prompt.append,
|
|
77
77
|
labels: this.agentDefinition?.labels,
|
|
78
|
+
labelMismatchHint: context?.labelMismatchHint,
|
|
78
79
|
// skills injected via .claude/skills/*.md files, not in prompt
|
|
79
80
|
});
|
|
80
81
|
const claudeCommand = resolveClaudeCommand(this.env);
|
|
@@ -150,7 +151,7 @@ export class ClaudeRunner implements AgentRunner {
|
|
|
150
151
|
issueIid: issue.iid,
|
|
151
152
|
issueTitle: issue.title,
|
|
152
153
|
gitlabHost: this.gitlabHost,
|
|
153
|
-
gitlabProjectId: this.gitlabProjectId,
|
|
154
|
+
gitlabProjectId: context?.gitlabProjectId ?? this.gitlabProjectId,
|
|
154
155
|
worktreePath: worktree.worktreePath,
|
|
155
156
|
branch: worktree.branch,
|
|
156
157
|
provider: "claude",
|
|
@@ -158,6 +159,7 @@ export class ClaudeRunner implements AgentRunner {
|
|
|
158
159
|
preamble: this.agentDefinition?.prompt.preamble,
|
|
159
160
|
append: this.agentDefinition?.prompt.append,
|
|
160
161
|
labels: this.agentDefinition?.labels,
|
|
162
|
+
labelMismatchHint: context?.labelMismatchHint,
|
|
161
163
|
});
|
|
162
164
|
const claudeCommand = resolveClaudeCommand(this.env);
|
|
163
165
|
|
package/src/local-agent/cli.ts
CHANGED
|
@@ -97,6 +97,7 @@ Commands:
|
|
|
97
97
|
watch <name> Watch agent status in real-time (refreshes every 5s)
|
|
98
98
|
history <name> Show agent execution history (--last N, default 20)
|
|
99
99
|
report <name> Show execution report (--since <N>d, default 7d) [--publish]
|
|
100
|
+
dashboard Multi-agent overview (status + recent runs + alerts)
|
|
100
101
|
sweep <name|--all> Move merged In Review issues to Done
|
|
101
102
|
gc <name|--all> Remove worktrees for closed/merged issues
|
|
102
103
|
install <name|--all> Generate launchd plist for agent(s)
|
|
@@ -2214,6 +2215,113 @@ async function cmdWikiSetup(): Promise<void> {
|
|
|
2214
2215
|
console.log(` ${wikiUrl(remote.host, remote.projectPath, "home")}`);
|
|
2215
2216
|
}
|
|
2216
2217
|
|
|
2218
|
+
// ── Dashboard command ─────────────────────────────────────────────────────
|
|
2219
|
+
|
|
2220
|
+
async function cmdDashboard(): Promise<void> {
|
|
2221
|
+
const agents = await discoverAgents(AGENTS_DIR).catch(() => []);
|
|
2222
|
+
if (agents.length === 0) {
|
|
2223
|
+
console.log("No agents found. Run 'glab-agent init <name>' first.");
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
// ── Agent Status Table ──
|
|
2228
|
+
console.log("AGENTS");
|
|
2229
|
+
console.log("-".repeat(80));
|
|
2230
|
+
const nameW = 20;
|
|
2231
|
+
const statusW = 10;
|
|
2232
|
+
const taskW = 35;
|
|
2233
|
+
const lastW = 15;
|
|
2234
|
+
console.log(
|
|
2235
|
+
`${"Name".padEnd(nameW)}${"Status".padEnd(statusW)}${"Current Task".padEnd(taskW)}${"Last Run".padEnd(lastW)}`
|
|
2236
|
+
);
|
|
2237
|
+
console.log("-".repeat(80));
|
|
2238
|
+
|
|
2239
|
+
const alerts: string[] = [];
|
|
2240
|
+
|
|
2241
|
+
for (const agent of agents) {
|
|
2242
|
+
const status = await getAgentStatus(PROJECT_DIR, agent.name);
|
|
2243
|
+
const statePath = agentStatePath(PROJECT_DIR, agent.name);
|
|
2244
|
+
const store = new FileStateStore(statePath);
|
|
2245
|
+
const state = await store.load();
|
|
2246
|
+
|
|
2247
|
+
// Status
|
|
2248
|
+
const statusStr = status.alive ? "busy" : "idle";
|
|
2249
|
+
|
|
2250
|
+
// Current task
|
|
2251
|
+
let taskStr = "\u2014";
|
|
2252
|
+
if (state.activeRun) {
|
|
2253
|
+
taskStr = `#${state.activeRun.issueIid}`;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Last run info
|
|
2257
|
+
const history = state.runHistory ?? [];
|
|
2258
|
+
let lastRunStr = "\u2014";
|
|
2259
|
+
if (history.length > 0) {
|
|
2260
|
+
const last = history[history.length - 1];
|
|
2261
|
+
const ago = Math.round((Date.now() - new Date(last.finishedAt).getTime()) / 60000);
|
|
2262
|
+
if (ago < 60) lastRunStr = `${ago}min ago`;
|
|
2263
|
+
else if (ago < 1440) lastRunStr = `${Math.round(ago / 60)}hr ago`;
|
|
2264
|
+
else lastRunStr = `${Math.round(ago / 1440)}d ago`;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
console.log(
|
|
2268
|
+
`${agent.name.padEnd(nameW)}${statusStr.padEnd(statusW)}${taskStr.slice(0, taskW - 1).padEnd(taskW)}${lastRunStr.padEnd(lastW)}`
|
|
2269
|
+
);
|
|
2270
|
+
|
|
2271
|
+
// Collect alerts
|
|
2272
|
+
if (history.length > 0) {
|
|
2273
|
+
const last = history[history.length - 1];
|
|
2274
|
+
if (last.status === "failed") {
|
|
2275
|
+
alerts.push(`${agent.name} #${last.issueIid} failed — check: pnpm agent logs ${agent.name}`);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// Check heartbeat staleness
|
|
2280
|
+
const hbPath = agentHeartbeatPath(PROJECT_DIR, agent.name);
|
|
2281
|
+
const hb = await readHeartbeat(hbPath);
|
|
2282
|
+
if (hb && isHeartbeatStale(hb, 300)) {
|
|
2283
|
+
alerts.push(`${agent.name} heartbeat stale (>5min) — may need restart`);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// ── Recent Runs ──
|
|
2288
|
+
console.log("\nRECENT (last 10 across all agents)");
|
|
2289
|
+
console.log("-".repeat(80));
|
|
2290
|
+
|
|
2291
|
+
const allRuns: Array<{ agentName: string; entry: RunHistoryEntry }> = [];
|
|
2292
|
+
for (const agent of agents) {
|
|
2293
|
+
const statePath = agentStatePath(PROJECT_DIR, agent.name);
|
|
2294
|
+
const store = new FileStateStore(statePath);
|
|
2295
|
+
const state = await store.load();
|
|
2296
|
+
for (const entry of state.runHistory ?? []) {
|
|
2297
|
+
allRuns.push({ agentName: agent.name, entry });
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
allRuns.sort((a, b) => new Date(b.entry.finishedAt).getTime() - new Date(a.entry.finishedAt).getTime());
|
|
2302
|
+
|
|
2303
|
+
const recentRuns = allRuns.slice(0, 10);
|
|
2304
|
+
if (recentRuns.length === 0) {
|
|
2305
|
+
console.log(" No execution history yet.");
|
|
2306
|
+
} else {
|
|
2307
|
+
for (const { agentName, entry } of recentRuns) {
|
|
2308
|
+
const status = entry.status === "completed" ? "done" : entry.status;
|
|
2309
|
+
const duration = formatDuration(entry.startedAt, entry.finishedAt);
|
|
2310
|
+
const title = (entry.issueTitle ?? "").slice(0, 30);
|
|
2311
|
+
console.log(` ${agentName.padEnd(16)} #${String(entry.issueIid).padEnd(6)} ${status.padEnd(10)} ${duration.padEnd(8)} ${title}`);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// ── Alerts ──
|
|
2316
|
+
if (alerts.length > 0) {
|
|
2317
|
+
console.log("\nALERTS");
|
|
2318
|
+
console.log("-".repeat(80));
|
|
2319
|
+
for (const alert of alerts) {
|
|
2320
|
+
console.log(` ${alert}`);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2217
2325
|
async function main(): Promise<void> {
|
|
2218
2326
|
const args = stripProjectFlag(process.argv.slice(2));
|
|
2219
2327
|
const command = args[0];
|
|
@@ -2350,6 +2458,9 @@ Examples:
|
|
|
2350
2458
|
case "doctor":
|
|
2351
2459
|
await cmdDoctor();
|
|
2352
2460
|
break;
|
|
2461
|
+
case "dashboard":
|
|
2462
|
+
await cmdDashboard();
|
|
2463
|
+
break;
|
|
2353
2464
|
case "wiki": {
|
|
2354
2465
|
const wikiSub = args[1];
|
|
2355
2466
|
if (wikiSub === "setup") {
|
|
@@ -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",
|
|
@@ -75,6 +75,7 @@ export class CodexRunner implements AgentRunner {
|
|
|
75
75
|
preamble: this.agentDefinition?.prompt.preamble,
|
|
76
76
|
append: this.agentDefinition?.prompt.append,
|
|
77
77
|
labels: this.agentDefinition?.labels,
|
|
78
|
+
labelMismatchHint: context?.labelMismatchHint,
|
|
78
79
|
// skills injected via .agent_context/skills/*.md files, not in prompt
|
|
79
80
|
});
|
|
80
81
|
const codexCommand = resolveCodexCommand(this.env);
|
|
@@ -153,7 +154,7 @@ export class CodexRunner implements AgentRunner {
|
|
|
153
154
|
issueIid: issue.iid,
|
|
154
155
|
issueTitle: issue.title,
|
|
155
156
|
gitlabHost: this.gitlabHost,
|
|
156
|
-
gitlabProjectId: this.gitlabProjectId,
|
|
157
|
+
gitlabProjectId: context?.gitlabProjectId ?? this.gitlabProjectId,
|
|
157
158
|
worktreePath: worktree.worktreePath,
|
|
158
159
|
branch: worktree.branch,
|
|
159
160
|
provider: "codex",
|
|
@@ -161,6 +162,7 @@ export class CodexRunner implements AgentRunner {
|
|
|
161
162
|
preamble: this.agentDefinition?.prompt.preamble,
|
|
162
163
|
append: this.agentDefinition?.prompt.append,
|
|
163
164
|
labels: this.agentDefinition?.labels,
|
|
165
|
+
labelMismatchHint: context?.labelMismatchHint,
|
|
164
166
|
});
|
|
165
167
|
const codexCommand = resolveCodexCommand(this.env);
|
|
166
168
|
|
|
@@ -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.
|
|
@@ -25,6 +25,10 @@ export interface ReportResult {
|
|
|
25
25
|
timestamp: string;
|
|
26
26
|
error?: string;
|
|
27
27
|
}[];
|
|
28
|
+
insights?: {
|
|
29
|
+
frequentFiles: { file: string; count: number }[];
|
|
30
|
+
retryHotspots: { issueIid: number; retryCount: number }[];
|
|
31
|
+
};
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export async function generateReport(
|
|
@@ -95,6 +99,45 @@ export async function generateReport(
|
|
|
95
99
|
error: e.error
|
|
96
100
|
}));
|
|
97
101
|
|
|
102
|
+
// Pattern insights (HX-3)
|
|
103
|
+
// Frequent files: from git log
|
|
104
|
+
const frequentFiles: { file: string; count: number }[] = [];
|
|
105
|
+
try {
|
|
106
|
+
const { execFile: execFileCb } = await import("node:child_process");
|
|
107
|
+
const { promisify } = await import("node:util");
|
|
108
|
+
const execFileAsync = promisify(execFileCb);
|
|
109
|
+
const sinceStr = `${sinceDays} days ago`;
|
|
110
|
+
const { stdout } = await execFileAsync("git", [
|
|
111
|
+
"log", "--since", sinceStr, "--name-only", "--pretty=format:", "--diff-filter=M"
|
|
112
|
+
], { maxBuffer: 1024 * 1024 });
|
|
113
|
+
const fileCounts = new Map<string, number>();
|
|
114
|
+
for (const line of stdout.toString().split("\n")) {
|
|
115
|
+
const f = line.trim();
|
|
116
|
+
if (f && f.startsWith("src/")) {
|
|
117
|
+
fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const sorted = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
121
|
+
for (const [file, count] of sorted) {
|
|
122
|
+
frequentFiles.push({ file, count });
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// git not available or not a git repo — skip
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Retry hotspots: issues that appear multiple times in events
|
|
129
|
+
const issueOccurrences = new Map<number, number>();
|
|
130
|
+
for (const e of events) {
|
|
131
|
+
if (e.issueIid) {
|
|
132
|
+
issueOccurrences.set(e.issueIid, (issueOccurrences.get(e.issueIid) ?? 0) + 1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const retryHotspots = [...issueOccurrences.entries()]
|
|
136
|
+
.filter(([_, count]) => count > 1)
|
|
137
|
+
.sort((a, b) => b[1] - a[1])
|
|
138
|
+
.slice(0, 5)
|
|
139
|
+
.map(([issueIid, retryCount]) => ({ issueIid, retryCount }));
|
|
140
|
+
|
|
98
141
|
return {
|
|
99
142
|
agentName,
|
|
100
143
|
period: { from: sinceDate, to: now },
|
|
@@ -106,7 +149,8 @@ export async function generateReport(
|
|
|
106
149
|
successRate,
|
|
107
150
|
avgDurationMs,
|
|
108
151
|
topFailures,
|
|
109
|
-
recentRuns
|
|
152
|
+
recentRuns,
|
|
153
|
+
insights: { frequentFiles, retryHotspots }
|
|
110
154
|
};
|
|
111
155
|
}
|
|
112
156
|
|
|
@@ -180,6 +224,27 @@ export function formatReport(result: ReportResult): string {
|
|
|
180
224
|
}
|
|
181
225
|
}
|
|
182
226
|
|
|
227
|
+
// Pattern insights (HX-3)
|
|
228
|
+
const insights = result.insights;
|
|
229
|
+
if (insights && (insights.frequentFiles.length > 0 || insights.retryHotspots.length > 0)) {
|
|
230
|
+
lines.push("", "PATTERN INSIGHTS");
|
|
231
|
+
lines.push("-".repeat(40));
|
|
232
|
+
|
|
233
|
+
if (insights.frequentFiles.length > 0) {
|
|
234
|
+
lines.push("Frequently modified:");
|
|
235
|
+
for (const f of insights.frequentFiles) {
|
|
236
|
+
lines.push(` ${f.file} (${f.count}x)`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (insights.retryHotspots.length > 0) {
|
|
241
|
+
lines.push("Retry hotspots:");
|
|
242
|
+
for (const h of insights.retryHotspots) {
|
|
243
|
+
lines.push(` #${h.issueIid} (${h.retryCount} runs)`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
183
248
|
return lines.join("\n");
|
|
184
249
|
}
|
|
185
250
|
|
|
@@ -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);
|
|
@@ -474,35 +526,21 @@ export async function runWatcherCycle(
|
|
|
474
526
|
|
|
475
527
|
const issue = await dependencies.gitlabClient.getIssue(candidate.projectId, candidate.issueIid);
|
|
476
528
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (mentionType === "contextual" && dependencies.agentRunner.runContextual) {
|
|
481
|
-
logger.info(`NT-4 contextual reply for issue #${issue.iid}: "${issue.title}"`);
|
|
482
|
-
await updateAgentUserStatus(dependencies, "busy", `#${issue.iid}`);
|
|
483
|
-
|
|
484
|
-
try {
|
|
485
|
-
const result = await dependencies.agentRunner.runContextual(issue, candidate.body ?? "", { todoId: candidate.id });
|
|
486
|
-
logger.info(`Contextual reply completed for #${issue.iid}: ${result.summary.slice(0, 100)}`);
|
|
487
|
-
} catch (error) {
|
|
488
|
-
logger.warn(`Contextual reply failed for #${issue.iid}: ${String(error).slice(0, 200)}`);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
await updateAgentUserStatus(dependencies, "idle", undefined, config);
|
|
492
|
-
state.processedTodoIds = rememberIds(state.processedTodoIds, candidate.id);
|
|
493
|
-
state.activeRun = undefined;
|
|
494
|
-
await dependencies.stateStore.save(state);
|
|
495
|
-
try { await dependencies.gitlabClient.markTodoDone(candidate.id); } catch { /* silent */ }
|
|
496
|
-
|
|
497
|
-
return { status: "completed", issueIid: issue.iid, summary: `Contextual reply for #${issue.iid}` };
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Label-based trigger filtering (full_work path only)
|
|
529
|
+
// Label mismatch: don't skip (P1: @mention must always get a response),
|
|
530
|
+
// but pass hint to agent so it can decide how to respond (P3: agent autonomy)
|
|
531
|
+
let labelMismatchHint: string | undefined;
|
|
501
532
|
if (triggers && !matchesTriggerLabels(issue.labels, triggers.labels, triggers.exclude_labels)) {
|
|
502
|
-
|
|
503
|
-
|
|
533
|
+
const agentName = config.agentDefinition?.name ?? "agent";
|
|
534
|
+
const required = triggers.labels?.join(", ") || "(无)";
|
|
535
|
+
const excluded = triggers.exclude_labels?.join(", ") || "(无)";
|
|
536
|
+
const actual = issue.labels.join(", ") || "(无)";
|
|
537
|
+
labelMismatchHint = `此 issue 的 label [${actual}] 不在 ${agentName} 的触发范围内(要求: [${required}],排除: [${excluded}])。你仍然需要回复这个 @mention(P1),但可以选择只回复说明而不执行代码修改。`;
|
|
538
|
+
logger.info(`Issue #${issue.iid}: label mismatch, passing hint to agent`);
|
|
504
539
|
}
|
|
505
|
-
|
|
540
|
+
|
|
541
|
+
const { worktree } = await resolveTargetWorktree(
|
|
542
|
+
issue.projectId, issue.iid, issue.title, config, dependencies, logger
|
|
543
|
+
);
|
|
506
544
|
|
|
507
545
|
state.activeRun = {
|
|
508
546
|
pid,
|
|
@@ -582,7 +620,7 @@ export async function runWatcherCycle(
|
|
|
582
620
|
|
|
583
621
|
try {
|
|
584
622
|
const raceResult = await Promise.race([
|
|
585
|
-
dependencies.agentRunner.run(issue, worktree, { todoId: candidate.id, signal: abortController.signal }).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 })),
|
|
586
624
|
closeDetection.then(() => ({ type: "closed" as const }))
|
|
587
625
|
]);
|
|
588
626
|
|
|
@@ -740,17 +778,21 @@ export async function runConcurrentWatcherCycle(
|
|
|
740
778
|
|
|
741
779
|
if (result.error) {
|
|
742
780
|
// Failed
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
781
|
+
const isMr = spawned.todo.targetType === "MergeRequest";
|
|
782
|
+
const targetLabel = isMr ? `MR !${issueIid}` : `issue #${issueIid}`;
|
|
783
|
+
logger.error(`Agent failed ${targetLabel} after ${durationSec}s. Error: ${result.error.slice(0, 200)}`);
|
|
784
|
+
if (!isMr) {
|
|
785
|
+
try {
|
|
786
|
+
await dependencies.gitlabClient.updateIssueLabels(
|
|
787
|
+
spawned.issue.projectId, issueIid,
|
|
788
|
+
transitionStatusLabels(spawned.issue.labels, getLabelConfig(config).error, getStatusLabelsList(config))
|
|
789
|
+
);
|
|
790
|
+
await dependencies.gitlabClient.addIssueNote(
|
|
791
|
+
spawned.issue.projectId, issueIid,
|
|
792
|
+
`⚠️ Agent 执行失败。\n\n${result.summary}`
|
|
793
|
+
);
|
|
794
|
+
} catch (e) { logger.warn(`Post-run cleanup failed: ${String(e).slice(0, 100)}`); }
|
|
795
|
+
}
|
|
754
796
|
await notifyFailed(config.agentDefinition?.name ?? "agent", issueIid, spawned.issue.title, result.summary, config.webhookUrl);
|
|
755
797
|
// Feishu: update card to failed state
|
|
756
798
|
if (dependencies.feishuClient && spawned.feishuMessageId) {
|
|
@@ -833,34 +875,35 @@ export async function runConcurrentWatcherCycle(
|
|
|
833
875
|
if (!candidate) break;
|
|
834
876
|
processedInThisCycle.add(candidate.id);
|
|
835
877
|
|
|
836
|
-
// Skip MR todos and contextual replies in concurrent mode — handle them synchronously
|
|
837
878
|
const isIssueTodo = candidate.targetType === "Issue";
|
|
838
|
-
|
|
839
|
-
// Mark as processed, skip for now (MR handling stays synchronous)
|
|
840
|
-
state.processedTodoIds = rememberIds(state.processedTodoIds, candidate.id);
|
|
841
|
-
try { await dependencies.gitlabClient.markTodoDone(candidate.id); } catch { /* silent */ }
|
|
842
|
-
continue;
|
|
843
|
-
}
|
|
879
|
+
const targetIid = isIssueTodo ? candidate.issueIid : (candidate.targetIid ?? 0);
|
|
844
880
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
881
|
+
// Build issue object — real for Issues, synthetic for MRs
|
|
882
|
+
let issue: GitlabIssue;
|
|
883
|
+
if (isIssueTodo) {
|
|
884
|
+
issue = await dependencies.gitlabClient.getIssue(candidate.projectId, candidate.issueIid);
|
|
885
|
+
} else {
|
|
886
|
+
issue = {
|
|
887
|
+
id: 0,
|
|
888
|
+
iid: targetIid,
|
|
889
|
+
projectId: candidate.projectId,
|
|
890
|
+
title: `MR !${targetIid} @mention`,
|
|
891
|
+
description: candidate.body ?? "",
|
|
892
|
+
labels: [],
|
|
893
|
+
webUrl: candidate.targetUrl ?? ""
|
|
894
|
+
};
|
|
895
|
+
logger.info(`MR todo !${targetIid}: spawning into slot`);
|
|
858
896
|
}
|
|
859
897
|
|
|
860
|
-
// Label
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
898
|
+
// Label mismatch: pass hint to agent (P1: always respond, P3: agent decides)
|
|
899
|
+
let labelMismatchHint: string | undefined;
|
|
900
|
+
if (isIssueTodo && triggers && !matchesTriggerLabels(issue.labels, triggers.labels, triggers.exclude_labels)) {
|
|
901
|
+
const agentName = config.agentDefinition?.name ?? "agent";
|
|
902
|
+
const required = triggers.labels?.join(", ") || "(无)";
|
|
903
|
+
const excluded = triggers.exclude_labels?.join(", ") || "(无)";
|
|
904
|
+
const actual = issue.labels.join(", ") || "(无)";
|
|
905
|
+
labelMismatchHint = `此 issue 的 label [${actual}] 不在 ${agentName} 的触发范围内(要求: [${required}],排除: [${excluded}])。你仍然需要回复这个 @mention(P1),但可以选择只回复说明而不执行代码修改。`;
|
|
906
|
+
logger.info(`Issue #${issue.iid}: label mismatch, passing hint to agent`);
|
|
864
907
|
}
|
|
865
908
|
|
|
866
909
|
// Ensure spawn() is available
|
|
@@ -870,7 +913,11 @@ export async function runConcurrentWatcherCycle(
|
|
|
870
913
|
}
|
|
871
914
|
|
|
872
915
|
// Prepare and spawn
|
|
873
|
-
const
|
|
916
|
+
const worktreeTitle = isIssueTodo ? issue.title : `mr-${targetIid}`;
|
|
917
|
+
const { worktree } = await resolveTargetWorktree(
|
|
918
|
+
isIssueTodo ? issue.projectId : candidate.projectId,
|
|
919
|
+
targetIid, worktreeTitle, config, dependencies, logger
|
|
920
|
+
);
|
|
874
921
|
|
|
875
922
|
const skills = config.agentDefinition?.skills ?? [];
|
|
876
923
|
if (skills.length > 0) {
|
|
@@ -880,7 +927,7 @@ export async function runConcurrentWatcherCycle(
|
|
|
880
927
|
} catch (err) { logger.warn(`Failed to inject skill files: ${String(err)}`); }
|
|
881
928
|
}
|
|
882
929
|
|
|
883
|
-
const spawnedRun = await dependencies.agentRunner.spawn(issue, worktree, { todoId: candidate.id });
|
|
930
|
+
const spawnedRun = await dependencies.agentRunner.spawn(issue, worktree, { todoId: candidate.id, labelMismatchHint, gitlabProjectId: isIssueTodo ? issue.projectId : candidate.projectId });
|
|
884
931
|
const now = new Date().toISOString();
|
|
885
932
|
|
|
886
933
|
spawnedRuns.set(issue.iid, {
|
|
@@ -1057,7 +1104,9 @@ async function finalizeTodo(
|
|
|
1057
1104
|
const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
|
|
1058
1105
|
|
|
1059
1106
|
// P11 post-run compensation: agent is probabilistic, verify critical state transitions
|
|
1060
|
-
|
|
1107
|
+
// Skip for MR todos — MRs don't have issue labels/notes to compensate
|
|
1108
|
+
const isMrTodo = todo.targetType === "MergeRequest";
|
|
1109
|
+
if (result.status === "completed" && !isMrTodo) {
|
|
1061
1110
|
try {
|
|
1062
1111
|
const freshIssue = await dependencies.gitlabClient.getIssue(issue.projectId, issue.iid);
|
|
1063
1112
|
const logger = dependencies.logger ?? console;
|
|
@@ -1087,23 +1136,25 @@ async function finalizeTodo(
|
|
|
1087
1136
|
}
|
|
1088
1137
|
}
|
|
1089
1138
|
|
|
1090
|
-
// NT-1: Post structured run summary note to GitLab
|
|
1139
|
+
// NT-1: Post structured run summary note to GitLab (history visible in GitLab)
|
|
1091
1140
|
// Posted AFTER agent reply and P11 compensation so it appears last in the timeline
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1141
|
+
if (!isMrTodo) {
|
|
1142
|
+
try {
|
|
1143
|
+
const durationStr = durationMs < 60000
|
|
1144
|
+
? `${Math.round(durationMs / 1000)}s`
|
|
1145
|
+
: `${Math.round(durationMs / 60000)}m ${Math.round((durationMs % 60000) / 1000)}s`;
|
|
1146
|
+
const statusEmoji = result.status === "completed" ? "✅" : "❌";
|
|
1147
|
+
const noteBody = [
|
|
1148
|
+
`${statusEmoji} **Agent run ${result.status}** (${durationStr})`,
|
|
1149
|
+
"",
|
|
1150
|
+
result.summary.slice(0, 500),
|
|
1151
|
+
"",
|
|
1152
|
+
`<!-- agent-run-log ${JSON.stringify({ agent: _config.agentDefinition?.name, status: result.status, startedAt, finishedAt, durationMs, branch: result.branch })} -->`
|
|
1153
|
+
].join("\n");
|
|
1154
|
+
await dependencies.gitlabClient.addIssueNote(issue.projectId, issue.iid, noteBody);
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
(dependencies.logger ?? console).warn(`Failed to post run summary note: ${String(err)}`);
|
|
1157
|
+
}
|
|
1107
1158
|
}
|
|
1108
1159
|
|
|
1109
1160
|
appendRunHistory(state, {
|
|
@@ -1511,13 +1562,17 @@ function pickNextTodo(
|
|
|
1511
1562
|
const acceptedActions = triggerActions ?? ACCEPTED_TODO_ACTIONS;
|
|
1512
1563
|
|
|
1513
1564
|
const candidates = todos.filter((todo) => {
|
|
1565
|
+
const todoLabel = todo.targetType === "MergeRequest"
|
|
1566
|
+
? `MR !${todo.targetIid ?? 0}`
|
|
1567
|
+
: `issue #${todo.issueIid}`;
|
|
1568
|
+
|
|
1514
1569
|
if (processedTodos.has(todo.id)) {
|
|
1515
|
-
logger?.info?.(`Todo ${todo.id} (
|
|
1570
|
+
logger?.info?.(`Todo ${todo.id} (${todoLabel}): skipped — already processed`);
|
|
1516
1571
|
return false;
|
|
1517
1572
|
}
|
|
1518
1573
|
|
|
1519
1574
|
if (!acceptedActions.has(todo.actionName)) {
|
|
1520
|
-
logger?.info?.(`Todo ${todo.id} (
|
|
1575
|
+
logger?.info?.(`Todo ${todo.id} (${todoLabel}): skipped — action "${todo.actionName}" not in triggers`);
|
|
1521
1576
|
return false;
|
|
1522
1577
|
}
|
|
1523
1578
|
|
|
@@ -1525,8 +1580,10 @@ function pickNextTodo(
|
|
|
1525
1580
|
return false;
|
|
1526
1581
|
}
|
|
1527
1582
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1583
|
+
// For MR todos, check by targetIid; for Issues, check by issueIid
|
|
1584
|
+
const effectiveIid = todo.targetType === "MergeRequest" ? (todo.targetIid ?? 0) : todo.issueIid;
|
|
1585
|
+
if (activeIssueIids?.has(effectiveIid)) {
|
|
1586
|
+
logger?.info?.(`Todo ${todo.id} (${todoLabel}): skipped — already being processed`);
|
|
1530
1587
|
return false;
|
|
1531
1588
|
}
|
|
1532
1589
|
|
|
@@ -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) {
|