pipeline-worker 0.1.0
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/.env.example +27 -0
- package/.pipeline-worker.yml.example +24 -0
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/agent/claude.d.ts +8 -0
- package/dist/agent/claude.js +40 -0
- package/dist/agent/claude.js.map +1 -0
- package/dist/agent/copilot.d.ts +13 -0
- package/dist/agent/copilot.js +42 -0
- package/dist/agent/copilot.js.map +1 -0
- package/dist/agent/index.d.ts +7 -0
- package/dist/agent/index.js +15 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/types.d.ts +22 -0
- package/dist/agent/types.js +3 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +77 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/detectChecks.d.ts +14 -0
- package/dist/config/detectChecks.js +53 -0
- package/dist/config/detectChecks.js.map +1 -0
- package/dist/config/loader.d.ts +11 -0
- package/dist/config/loader.js +98 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/forge/github.d.ts +30 -0
- package/dist/forge/github.js +134 -0
- package/dist/forge/github.js.map +1 -0
- package/dist/forge/gitlab.d.ts +15 -0
- package/dist/forge/gitlab.js +99 -0
- package/dist/forge/gitlab.js.map +1 -0
- package/dist/forge/index.d.ts +4 -0
- package/dist/forge/index.js +12 -0
- package/dist/forge/index.js.map +1 -0
- package/dist/forge/types.d.ts +25 -0
- package/dist/forge/types.js +7 -0
- package/dist/forge/types.js.map +1 -0
- package/dist/git/commit.d.ts +8 -0
- package/dist/git/commit.js +27 -0
- package/dist/git/commit.js.map +1 -0
- package/dist/git/diff.d.ts +11 -0
- package/dist/git/diff.js +22 -0
- package/dist/git/diff.js.map +1 -0
- package/dist/git/worktree.d.ts +21 -0
- package/dist/git/worktree.js +75 -0
- package/dist/git/worktree.js.map +1 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +109 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/state/runState.d.ts +9 -0
- package/dist/state/runState.js +37 -0
- package/dist/state/runState.js.map +1 -0
- package/dist/toon/envelope.d.ts +21 -0
- package/dist/toon/envelope.js +36 -0
- package/dist/toon/envelope.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow/captureIntent.d.ts +4 -0
- package/dist/workflow/captureIntent.js +35 -0
- package/dist/workflow/captureIntent.js.map +1 -0
- package/dist/workflow/openMergeRequest.d.ts +4 -0
- package/dist/workflow/openMergeRequest.js +20 -0
- package/dist/workflow/openMergeRequest.js.map +1 -0
- package/dist/workflow/orchestrate.d.ts +2 -0
- package/dist/workflow/orchestrate.js +82 -0
- package/dist/workflow/orchestrate.js.map +1 -0
- package/dist/workflow/runChecks.d.ts +8 -0
- package/dist/workflow/runChecks.js +47 -0
- package/dist/workflow/runChecks.js.map +1 -0
- package/dist/workflow/watchPipeline.d.ts +13 -0
- package/dist/workflow/watchPipeline.js +109 -0
- package/dist/workflow/watchPipeline.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom forge MCP server (GitLab or GitHub, per config) using stdio
|
|
3
|
+
* transport. Exposes the minimum tool set an agent needs to drive steps 7-8
|
|
4
|
+
* of the pipeline-worker workflow (open an MR/PR, watch its pipeline, inspect
|
|
5
|
+
* failures, leave a human-escalation note). Every tool response is
|
|
6
|
+
* TOON-encoded via toon/envelope.ts to minimize the tokens an agent spends
|
|
7
|
+
* reading forge state.
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { loadConfig } from '../config/loader.js';
|
|
13
|
+
import { createForge } from '../forge/index.js';
|
|
14
|
+
import { buildEnvelope, errorEnvelope } from '../toon/envelope.js';
|
|
15
|
+
function toolResult(text, isError = false) {
|
|
16
|
+
return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
|
|
17
|
+
}
|
|
18
|
+
/** Every tool follows the same try/create-forge/call/envelope-or-error shape. */
|
|
19
|
+
function wrap(handler) {
|
|
20
|
+
return async (args) => {
|
|
21
|
+
try {
|
|
22
|
+
const forge = createForge(loadConfig(process.cwd()));
|
|
23
|
+
const text = await handler(forge, args);
|
|
24
|
+
return toolResult(text);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
return toolResult(errorEnvelope('forge_error', message), true);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const fullFlag = z.boolean().describe('Skip truncation and return the full payload').optional();
|
|
33
|
+
export async function createServer() {
|
|
34
|
+
const server = new McpServer({ name: 'pipeline-worker-forge', version: '0.1.0' });
|
|
35
|
+
server.registerTool('create_merge_request', {
|
|
36
|
+
description: 'Create a merge request / pull request for a pushed branch',
|
|
37
|
+
inputSchema: z.object({
|
|
38
|
+
sourceBranch: z.string(),
|
|
39
|
+
targetBranch: z.string(),
|
|
40
|
+
title: z.string(),
|
|
41
|
+
description: z.string(),
|
|
42
|
+
}),
|
|
43
|
+
}, wrap(async (forge, args) => {
|
|
44
|
+
const mr = await forge.createMergeRequest(args);
|
|
45
|
+
return buildEnvelope({ status: 'success', data: mr, next: 'call get_pipeline_status with this mr iid' });
|
|
46
|
+
}));
|
|
47
|
+
server.registerTool('get_pipeline_status', {
|
|
48
|
+
description: "Get an MR/PR's latest pipeline status",
|
|
49
|
+
inputSchema: z.object({ mrIid: z.number() }),
|
|
50
|
+
}, wrap(async (forge, args) => {
|
|
51
|
+
const pipelines = await forge.getMrPipelines(args.mrIid);
|
|
52
|
+
const latest = pipelines[0];
|
|
53
|
+
if (!latest) {
|
|
54
|
+
return buildEnvelope({ status: 'success', counts: { pipelines: 0 }, next: 'no pipeline yet, poll again shortly' });
|
|
55
|
+
}
|
|
56
|
+
const next = latest.status === 'failed'
|
|
57
|
+
? 'call get_failed_jobs with this pipeline id'
|
|
58
|
+
: latest.status === 'running' || latest.status === 'pending'
|
|
59
|
+
? 'poll again shortly'
|
|
60
|
+
: undefined;
|
|
61
|
+
return buildEnvelope({ status: 'success', data: latest, next });
|
|
62
|
+
}));
|
|
63
|
+
server.registerTool('get_failed_jobs', {
|
|
64
|
+
description: 'List the failed jobs for a pipeline',
|
|
65
|
+
inputSchema: z.object({ pipelineId: z.number() }),
|
|
66
|
+
}, wrap(async (forge, args) => {
|
|
67
|
+
const jobs = await forge.getFailedJobs(args.pipelineId);
|
|
68
|
+
return buildEnvelope({
|
|
69
|
+
status: 'success',
|
|
70
|
+
data: jobs,
|
|
71
|
+
counts: { failedJobs: jobs.length },
|
|
72
|
+
next: jobs.length > 0 ? 'call get_job_log with a job id' : undefined,
|
|
73
|
+
});
|
|
74
|
+
}));
|
|
75
|
+
server.registerTool('get_job_log', {
|
|
76
|
+
description: "Get a job's log output (trace), truncated by default",
|
|
77
|
+
inputSchema: z.object({ jobId: z.number(), full: fullFlag }),
|
|
78
|
+
}, wrap(async (forge, args) => {
|
|
79
|
+
const log = await forge.getJobLog(args.jobId);
|
|
80
|
+
return buildEnvelope({ status: 'success', data: log }, { full: args.full });
|
|
81
|
+
}));
|
|
82
|
+
server.registerTool('retry_pipeline', {
|
|
83
|
+
description: 'Retry a failed pipeline',
|
|
84
|
+
inputSchema: z.object({ pipelineId: z.number() }),
|
|
85
|
+
}, wrap(async (forge, args) => {
|
|
86
|
+
const pipeline = await forge.retryPipeline(args.pipelineId);
|
|
87
|
+
return buildEnvelope({ status: 'success', data: pipeline });
|
|
88
|
+
}));
|
|
89
|
+
server.registerTool('create_mr_comment', {
|
|
90
|
+
description: 'Leave a comment/note on a merge request / pull request',
|
|
91
|
+
inputSchema: z.object({ mrIid: z.number(), body: z.string() }),
|
|
92
|
+
}, wrap(async (forge, args) => {
|
|
93
|
+
const note = await forge.createMrNote(args.mrIid, args.body);
|
|
94
|
+
return buildEnvelope({ status: 'success', data: note });
|
|
95
|
+
}));
|
|
96
|
+
return server;
|
|
97
|
+
}
|
|
98
|
+
export async function startServer() {
|
|
99
|
+
const server = await createServer();
|
|
100
|
+
const transport = new StdioServerTransport();
|
|
101
|
+
await server.connect(transport);
|
|
102
|
+
}
|
|
103
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
104
|
+
startServer().catch((error) => {
|
|
105
|
+
console.error('Failed to start MCP server:', error);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persists RunState at <repoRoot>/.pipeline-worker/state/<branch>.json so `pipeline-worker
|
|
3
|
+
* resume` can recover after a crash mid-poll. Read/write never throw — a
|
|
4
|
+
* lost state file only degrades `resume`/`status`, it never corrupts GitLab
|
|
5
|
+
* state (the forge client's findExistingMr is the real idempotency guard).
|
|
6
|
+
*/
|
|
7
|
+
import type { RunState } from '../types.js';
|
|
8
|
+
export declare function saveRunState(repoRoot: string, state: RunState): void;
|
|
9
|
+
export declare function loadRunState(repoRoot: string, branch: string): RunState | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persists RunState at <repoRoot>/.pipeline-worker/state/<branch>.json so `pipeline-worker
|
|
3
|
+
* resume` can recover after a crash mid-poll. Read/write never throw — a
|
|
4
|
+
* lost state file only degrades `resume`/`status`, it never corrupts GitLab
|
|
5
|
+
* state (the forge client's findExistingMr is the real idempotency guard).
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
function statePath(repoRoot, branch) {
|
|
10
|
+
const safeName = branch.replace(/\//g, '_');
|
|
11
|
+
return join(repoRoot, '.pipeline-worker', 'state', `${safeName}.json`);
|
|
12
|
+
}
|
|
13
|
+
export function saveRunState(repoRoot, state) {
|
|
14
|
+
try {
|
|
15
|
+
const path = statePath(repoRoot, state.branch);
|
|
16
|
+
mkdirSync(join(repoRoot, '.pipeline-worker', 'state'), { recursive: true });
|
|
17
|
+
writeFileSync(path, JSON.stringify(state, null, 2), 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
+
console.error(`Warning: failed to persist run state for branch ${state.branch}: ${message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function loadRunState(repoRoot, branch) {
|
|
25
|
+
const path = statePath(repoRoot, branch);
|
|
26
|
+
if (!existsSync(path))
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
console.error(`Warning: failed to read run state at ${path}: ${message}`);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=runState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runState.js","sourceRoot":"","sources":["../../src/state/runState.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,SAAS,SAAS,CAAC,QAAgB,EAAE,MAAc;IACjD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,IAAI,CAAC,QAAQ,EAAE,kBAAkB,EAAE,OAAO,EAAE,GAAG,QAAQ,OAAO,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,KAAe;IAC5D,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAC/C,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5E,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC/D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO,CAAC,KAAK,CAAC,mDAAmD,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC,CAAC;IAC/F,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,MAAc;IAC3D,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACzC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAa,CAAC;IAC7D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO,CAAC,KAAK,CAAC,wCAAwC,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;QAC1E,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOON response envelope for the GitLab MCP server's tools.
|
|
3
|
+
*
|
|
4
|
+
* Ports the response conventions already established in this repo family's
|
|
5
|
+
* files-mcp/server.py (status / aggregate counts / chars / truncate-with-
|
|
6
|
+
* `full=true` escape hatch / trailing `next:` hint) to TypeScript, but
|
|
7
|
+
* delegates the actual TOON row/table encoding to the real `@toon-format/toon`
|
|
8
|
+
* package instead of hand-rolling `name[N]{cols}:` header logic.
|
|
9
|
+
*/
|
|
10
|
+
export interface Envelope {
|
|
11
|
+
status: 'success' | 'error';
|
|
12
|
+
data?: unknown;
|
|
13
|
+
counts?: Record<string, number>;
|
|
14
|
+
next?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface EnvelopeOptions {
|
|
17
|
+
maxChars?: number;
|
|
18
|
+
full?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function buildEnvelope(env: Envelope, opts?: EnvelopeOptions): string;
|
|
21
|
+
export declare function errorEnvelope(kind: string, message: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOON response envelope for the GitLab MCP server's tools.
|
|
3
|
+
*
|
|
4
|
+
* Ports the response conventions already established in this repo family's
|
|
5
|
+
* files-mcp/server.py (status / aggregate counts / chars / truncate-with-
|
|
6
|
+
* `full=true` escape hatch / trailing `next:` hint) to TypeScript, but
|
|
7
|
+
* delegates the actual TOON row/table encoding to the real `@toon-format/toon`
|
|
8
|
+
* package instead of hand-rolling `name[N]{cols}:` header logic.
|
|
9
|
+
*/
|
|
10
|
+
import { encode } from '@toon-format/toon';
|
|
11
|
+
const DEFAULT_MAX_CHARS = 8000;
|
|
12
|
+
export function buildEnvelope(env, opts = {}) {
|
|
13
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
14
|
+
const body = env.data !== undefined ? encode(env.data) : '';
|
|
15
|
+
const truncated = !opts.full && body.length > maxChars;
|
|
16
|
+
const lines = [`status: ${env.status}`];
|
|
17
|
+
if (env.counts) {
|
|
18
|
+
for (const [key, value] of Object.entries(env.counts)) {
|
|
19
|
+
lines.push(`${key}: ${value}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (body) {
|
|
23
|
+
lines.push(`chars: ${body.length}`, '---', truncated ? body.slice(0, maxChars) : body, '---');
|
|
24
|
+
}
|
|
25
|
+
if (truncated) {
|
|
26
|
+
lines.push(`note: truncated to ${maxChars} of ${body.length} chars`, 'next: call again with full=true to see the rest');
|
|
27
|
+
}
|
|
28
|
+
else if (env.next) {
|
|
29
|
+
lines.push(`next: ${env.next}`);
|
|
30
|
+
}
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
}
|
|
33
|
+
export function errorEnvelope(kind, message) {
|
|
34
|
+
return `status: error\nkind: ${kind}\nmessage: ${message}`;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../src/toon/envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAc/B,MAAM,UAAU,aAAa,CAAC,GAAa,EAAE,OAAwB,EAAE;IACrE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IACpD,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,MAAM,SAAS,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;IAEvD,MAAM,KAAK,GAAG,CAAC,WAAW,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAExC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;QACf,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACtD,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,KAAK,EAAE,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAED,IAAI,IAAI,EAAE,CAAC;QACT,KAAK,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAChG,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,sBAAsB,QAAQ,OAAO,IAAI,CAAC,MAAM,QAAQ,EAAE,iDAAiD,CAAC,CAAC;IAC1H,CAAC;SAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,OAAe;IACzD,OAAO,wBAAwB,IAAI,cAAc,OAAO,EAAE,CAAC;AAC7D,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Single source of truth for pipeline-worker's cross-module data shapes. */
|
|
2
|
+
export type AgentName = 'claude' | 'copilot';
|
|
3
|
+
export type ForgeName = 'gitlab' | 'github';
|
|
4
|
+
export interface PipelineWorkerConfig {
|
|
5
|
+
agent: AgentName;
|
|
6
|
+
forge: ForgeName;
|
|
7
|
+
gitlab: {
|
|
8
|
+
host: string;
|
|
9
|
+
projectId: number;
|
|
10
|
+
};
|
|
11
|
+
github: {
|
|
12
|
+
/** "owner/name" slug of the repository. */
|
|
13
|
+
repo: string;
|
|
14
|
+
};
|
|
15
|
+
/** Check commands; defaults are auto-detected per toolchain, '' skips the stage. */
|
|
16
|
+
build: string;
|
|
17
|
+
lint: string;
|
|
18
|
+
test: string;
|
|
19
|
+
maxFixAttempts: number;
|
|
20
|
+
pollIntervalSeconds: number;
|
|
21
|
+
}
|
|
22
|
+
export type RunPhase = 'diff' | 'intent' | 'checks' | 'mr' | 'watch' | 'done' | 'escalated';
|
|
23
|
+
export interface RunState {
|
|
24
|
+
branch: string;
|
|
25
|
+
worktreePath: string;
|
|
26
|
+
mrIid?: number;
|
|
27
|
+
pipelineId?: number;
|
|
28
|
+
attempt: number;
|
|
29
|
+
phase: RunPhase;
|
|
30
|
+
}
|
|
31
|
+
export interface CapturedIntent {
|
|
32
|
+
summary: string;
|
|
33
|
+
branchName: string;
|
|
34
|
+
commitMessage: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CheckResult {
|
|
37
|
+
name: 'build' | 'lint' | 'test';
|
|
38
|
+
ok: boolean;
|
|
39
|
+
stdout: string;
|
|
40
|
+
stderr: string;
|
|
41
|
+
durationMs: number;
|
|
42
|
+
}
|
|
43
|
+
export interface MergeRequest {
|
|
44
|
+
iid: number;
|
|
45
|
+
webUrl: string;
|
|
46
|
+
sourceBranch: string;
|
|
47
|
+
targetBranch: string;
|
|
48
|
+
state: string;
|
|
49
|
+
}
|
|
50
|
+
export type PipelineStatus = 'created' | 'waiting_for_resource' | 'preparing' | 'pending' | 'running' | 'success' | 'failed' | 'canceled' | 'skipped' | 'manual' | 'scheduled';
|
|
51
|
+
export interface Pipeline {
|
|
52
|
+
id: number;
|
|
53
|
+
status: PipelineStatus;
|
|
54
|
+
webUrl: string;
|
|
55
|
+
}
|
|
56
|
+
export interface PipelineJob {
|
|
57
|
+
id: number;
|
|
58
|
+
name: string;
|
|
59
|
+
stage: string;
|
|
60
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,6EAA6E"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Step 3: ask the configured agent what a diff is *for*, in a structured, reusable shape. */
|
|
2
|
+
import type { AgentAdapter } from '../agent/types.js';
|
|
3
|
+
import type { CapturedIntent } from '../types.js';
|
|
4
|
+
export declare function captureIntent(agent: AgentAdapter, diffText: string, worktreePath: string): Promise<CapturedIntent>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Step 3: ask the configured agent what a diff is *for*, in a structured, reusable shape. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const INTENT_SCHEMA = {
|
|
4
|
+
type: 'object',
|
|
5
|
+
properties: {
|
|
6
|
+
summary: { type: 'string', description: 'One or two sentence description of what this change does and why' },
|
|
7
|
+
branchName: { type: 'string', description: 'A short kebab-case branch name, prefixed with pipeline-worker/' },
|
|
8
|
+
commitMessage: { type: 'string', description: 'A conventional-commit-style commit message for this change' },
|
|
9
|
+
},
|
|
10
|
+
required: ['summary', 'branchName', 'commitMessage'],
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Agent output is untrusted input: validate the shape and constrain the
|
|
14
|
+
* branch name to characters that are safe as a git ref and a URL segment.
|
|
15
|
+
*/
|
|
16
|
+
const IntentShape = z.object({
|
|
17
|
+
summary: z.string().min(1),
|
|
18
|
+
branchName: z.string().regex(/^pipeline-worker\/[A-Za-z0-9][A-Za-z0-9._-]*$/, 'branchName must be pipeline-worker/<kebab-case-name>'),
|
|
19
|
+
commitMessage: z.string().min(1),
|
|
20
|
+
});
|
|
21
|
+
export async function captureIntent(agent, diffText, worktreePath) {
|
|
22
|
+
const prompt = 'Read the following git diff and determine the intent behind it. ' +
|
|
23
|
+
'Respond with a JSON object matching the given schema: a short summary of what changed and why, ' +
|
|
24
|
+
'a kebab-case branch name prefixed with "pipeline-worker/", and a conventional-commit-style commit message.\n\n' +
|
|
25
|
+
`\`\`\`diff\n${diffText}\n\`\`\``;
|
|
26
|
+
const result = await agent.invoke({ prompt, cwd: worktreePath, jsonSchema: INTENT_SCHEMA });
|
|
27
|
+
try {
|
|
28
|
+
return IntentShape.parse(JSON.parse(result.text));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
throw new Error(`agent returned an unusable intent payload: ${message}\n--- agent output ---\n${result.text.slice(0, 2000)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=captureIntent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"captureIntent.js","sourceRoot":"","sources":["../../src/workflow/captureIntent.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAE9F,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,MAAM,aAAa,GAAG;IACpB,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,kEAAkE,EAAE;QAC5G,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gEAAgE,EAAE;QAC7G,aAAa,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4DAA4D,EAAE;KAC7G;IACD,QAAQ,EAAE,CAAC,SAAS,EAAE,YAAY,EAAE,eAAe,CAAC;CAC5C,CAAC;AAEX;;;GAGG;AACH,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,+CAA+C,EAAE,sDAAsD,CAAC;IACrI,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAmB,EAAE,QAAgB,EAAE,YAAoB;IAC7F,MAAM,MAAM,GACV,kEAAkE;QAClE,iGAAiG;QACjG,gHAAgH;QAChH,eAAe,QAAQ,UAAU,CAAC;IAEpC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,CAAC;IAC5F,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,8CAA8C,OAAO,2BAA2B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAChI,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Steps 6-7: push the branch and open (or reuse) an MR/PR describing the workflow that produced it. */
|
|
2
|
+
import type { ForgeClient } from '../forge/types.js';
|
|
3
|
+
import type { CapturedIntent, MergeRequest } from '../types.js';
|
|
4
|
+
export declare function openMergeRequest(forge: ForgeClient, worktreePath: string, branch: string, targetBranch: string, intent: CapturedIntent, agentName: string): Promise<MergeRequest>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Steps 6-7: push the branch and open (or reuse) an MR/PR describing the workflow that produced it. */
|
|
2
|
+
import { push } from '../git/commit.js';
|
|
3
|
+
export async function openMergeRequest(forge, worktreePath, branch, targetBranch, intent, agentName) {
|
|
4
|
+
await push(worktreePath, 'origin', branch);
|
|
5
|
+
const existing = await forge.findExistingMr(branch);
|
|
6
|
+
if (existing)
|
|
7
|
+
return existing;
|
|
8
|
+
const description = `${intent.summary}\n\n` +
|
|
9
|
+
'---\n' +
|
|
10
|
+
`Generated by \`pipeline-worker\`: intent captured via **${agentName}**, ` +
|
|
11
|
+
'build/lint/test passed locally before this MR was opened. ' +
|
|
12
|
+
'If the pipeline fails, pipeline-worker will attempt automated fixes (up to the configured retry cap) before escalating here.';
|
|
13
|
+
return forge.createMergeRequest({
|
|
14
|
+
sourceBranch: branch,
|
|
15
|
+
targetBranch,
|
|
16
|
+
title: intent.commitMessage,
|
|
17
|
+
description,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=openMergeRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openMergeRequest.js","sourceRoot":"","sources":["../../src/workflow/openMergeRequest.ts"],"names":[],"mappings":"AAAA,wGAAwG;AAExG,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAIxC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAkB,EAClB,YAAoB,EACpB,MAAc,EACd,YAAoB,EACpB,MAAsB,EACtB,SAAiB;IAEjB,MAAM,IAAI,CAAC,YAAY,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAE3C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACpD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,WAAW,GACf,GAAG,MAAM,CAAC,OAAO,MAAM;QACvB,OAAO;QACP,2DAA2D,SAAS,MAAM;QAC1E,4DAA4D;QAC5D,8HAA8H,CAAC;IAEjI,OAAO,KAAK,CAAC,kBAAkB,CAAC;QAC9B,YAAY,EAAE,MAAM;QACpB,YAAY;QACZ,KAAK,EAAE,MAAM,CAAC,aAAa;QAC3B,WAAW;KACZ,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Top-level control flow wiring the user's 8-step workflow together. */
|
|
2
|
+
import { loadConfig } from '../config/loader.js';
|
|
3
|
+
import { createForge } from '../forge/index.js';
|
|
4
|
+
import { selectAgent } from '../agent/index.js';
|
|
5
|
+
import { captureDiff } from '../git/diff.js';
|
|
6
|
+
import { createWorktree, applyDiffToWorktree, removeWorktree, renameBranch, generateTempBranchName } from '../git/worktree.js';
|
|
7
|
+
import { currentBranch, commit } from '../git/commit.js';
|
|
8
|
+
import { captureIntent } from './captureIntent.js';
|
|
9
|
+
import { runChecks } from './runChecks.js';
|
|
10
|
+
import { openMergeRequest } from './openMergeRequest.js';
|
|
11
|
+
import { watchPipeline } from './watchPipeline.js';
|
|
12
|
+
import { saveRunState } from '../state/runState.js';
|
|
13
|
+
/** Function-boundary read so TS reports the declared RunPhase union, not a narrowed literal. */
|
|
14
|
+
function readPhase(state) {
|
|
15
|
+
return state.phase;
|
|
16
|
+
}
|
|
17
|
+
export async function runWorkflow(repoRoot) {
|
|
18
|
+
const config = loadConfig(repoRoot);
|
|
19
|
+
const forge = createForge(config);
|
|
20
|
+
const agent = selectAgent(config);
|
|
21
|
+
const { diffText, untrackedFiles } = await captureDiff(repoRoot);
|
|
22
|
+
if (diffText.trim().length === 0 && untrackedFiles.length === 0) {
|
|
23
|
+
console.log('pipeline-worker: no changes to process.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const targetBranch = await currentBranch(repoRoot);
|
|
27
|
+
const tempBranch = generateTempBranchName();
|
|
28
|
+
const worktreePath = await createWorktree(repoRoot, tempBranch);
|
|
29
|
+
let state = { branch: tempBranch, worktreePath, attempt: 0, phase: 'diff' };
|
|
30
|
+
saveRunState(repoRoot, state);
|
|
31
|
+
let cleanedUp = false;
|
|
32
|
+
const cleanup = async () => {
|
|
33
|
+
if (cleanedUp)
|
|
34
|
+
return;
|
|
35
|
+
cleanedUp = true;
|
|
36
|
+
await removeWorktree(repoRoot, worktreePath);
|
|
37
|
+
};
|
|
38
|
+
const onSignal = (exitCode) => {
|
|
39
|
+
void cleanup().then(() => process.exit(exitCode));
|
|
40
|
+
};
|
|
41
|
+
process.once('SIGINT', () => onSignal(130));
|
|
42
|
+
process.once('SIGTERM', () => onSignal(143));
|
|
43
|
+
try {
|
|
44
|
+
await applyDiffToWorktree(worktreePath, diffText, untrackedFiles, repoRoot);
|
|
45
|
+
const intent = await captureIntent(agent, diffText, worktreePath);
|
|
46
|
+
await renameBranch(worktreePath, intent.branchName);
|
|
47
|
+
state = { ...state, branch: intent.branchName, phase: 'intent' };
|
|
48
|
+
saveRunState(repoRoot, state);
|
|
49
|
+
const checks = await runChecks(config, worktreePath);
|
|
50
|
+
const failedCheck = checks.find((c) => !c.ok);
|
|
51
|
+
if (failedCheck) {
|
|
52
|
+
console.error(`pipeline-worker: ${failedCheck.name} failed, aborting before opening a merge request.\n${failedCheck.stderr}`);
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
state.phase = 'checks';
|
|
57
|
+
saveRunState(repoRoot, state);
|
|
58
|
+
// applyDiffToWorktree left everything staged; without this commit the
|
|
59
|
+
// push would carry no changes and the MR would be empty.
|
|
60
|
+
await commit(worktreePath, intent.commitMessage);
|
|
61
|
+
const mr = await openMergeRequest(forge, worktreePath, state.branch, targetBranch, intent, config.agent);
|
|
62
|
+
state.mrIid = mr.iid;
|
|
63
|
+
state.phase = 'mr';
|
|
64
|
+
saveRunState(repoRoot, state);
|
|
65
|
+
await watchPipeline(forge, config, agent, worktreePath, state.branch, mr.iid, state, repoRoot);
|
|
66
|
+
// watchPipeline mutates state.phase in place; go through a function
|
|
67
|
+
// boundary so TS uses the declared RunPhase return type instead of the
|
|
68
|
+
// 'mr' literal it narrowed state.phase to just before the call.
|
|
69
|
+
const finalPhase = readPhase(state);
|
|
70
|
+
if (finalPhase === 'done') {
|
|
71
|
+
console.log(`pipeline-worker: MR ${mr.webUrl} passed CI.`);
|
|
72
|
+
}
|
|
73
|
+
else if (finalPhase === 'escalated') {
|
|
74
|
+
console.log(`pipeline-worker: automated fixes exhausted — escalated to a human on ${mr.webUrl}.`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await cleanup();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=orchestrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrate.js","sourceRoot":"","sources":["../../src/workflow/orchestrate.ts"],"names":[],"mappings":"AAAA,yEAAyE;AAEzE,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,cAAc,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC/H,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,gGAAgG;AAChG,SAAS,SAAS,CAAC,KAAe;IAChC,OAAO,KAAK,CAAC,KAAK,CAAC;AACrB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAgB;IAChD,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAElC,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;IACjE,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChE,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;QACvD,OAAO;IACT,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,sBAAsB,EAAE,CAAC;IAC5C,MAAM,YAAY,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAEhE,IAAI,KAAK,GAAa,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACtF,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAE9B,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,MAAM,OAAO,GAAG,KAAK,IAAmB,EAAE;QACxC,IAAI,SAAS;YAAE,OAAO;QACtB,SAAS,GAAG,IAAI,CAAC;QACjB,MAAM,cAAc,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IAC/C,CAAC,CAAC;IACF,MAAM,QAAQ,GAAG,CAAC,QAAgB,EAAE,EAAE;QACpC,KAAK,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IAE7C,IAAI,CAAC;QACH,MAAM,mBAAmB,CAAC,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;QAE5E,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;QAClE,MAAM,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;QACpD,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;QACjE,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAE9B,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QACrD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC9C,IAAI,WAAW,EAAE,CAAC;YAChB,OAAO,CAAC,KAAK,CACX,oBAAoB,WAAW,CAAC,IAAI,sDAAsD,WAAW,CAAC,MAAM,EAAE,CAC/G,CAAC;YACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QACD,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAC;QACvB,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAE9B,sEAAsE;QACtE,yDAAyD;QACzD,MAAM,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;QAEjD,MAAM,EAAE,GAAG,MAAM,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QACzG,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,GAAG,CAAC;QACrB,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;QACnB,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAE9B,MAAM,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;QAE/F,oEAAoE;QACpE,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,CAAC,MAAM,aAAa,CAAC,CAAC;QAC7D,CAAC;aAAM,IAAI,UAAU,KAAK,WAAW,EAAE,CAAC;YACtC,OAAO,CAAC,GAAG,CAAC,wEAAwE,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;YAClG,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,EAAE,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steps 4-5: real build/lint/test commands, script-based. Pass/fail comes
|
|
3
|
+
* from process exit codes only — the agent is never asked to judge these,
|
|
4
|
+
* only to fix a failure the scripts already reported (see watchPipeline.ts).
|
|
5
|
+
*/
|
|
6
|
+
import type { CheckResult, PipelineWorkerConfig } from '../types.js';
|
|
7
|
+
/** Runs build, then lint, then test, stopping at the first failure (fail-fast). */
|
|
8
|
+
export declare function runChecks(config: PipelineWorkerConfig, worktreePath: string): Promise<CheckResult[]>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steps 4-5: real build/lint/test commands, script-based. Pass/fail comes
|
|
3
|
+
* from process exit codes only — the agent is never asked to judge these,
|
|
4
|
+
* only to fix a failure the scripts already reported (see watchPipeline.ts).
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
async function runStage(name, command, cwd) {
|
|
10
|
+
// execFile (not exec) runs argv directly with no shell, so command must be
|
|
11
|
+
// plain space-separated argv — no quoting, pipes, or `&&` chains.
|
|
12
|
+
const [cmd, ...args] = command.split(' ');
|
|
13
|
+
const start = Date.now();
|
|
14
|
+
try {
|
|
15
|
+
const { stdout, stderr } = await execFileAsync(cmd, args, { cwd, maxBuffer: 64 * 1024 * 1024 });
|
|
16
|
+
return { name, ok: true, stdout, stderr, durationMs: Date.now() - start };
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const err = error;
|
|
20
|
+
return {
|
|
21
|
+
name,
|
|
22
|
+
ok: false,
|
|
23
|
+
stdout: err.stdout ?? '',
|
|
24
|
+
stderr: err.stderr ?? err.message ?? String(error),
|
|
25
|
+
durationMs: Date.now() - start,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Runs build, then lint, then test, stopping at the first failure (fail-fast). */
|
|
30
|
+
export async function runChecks(config, worktreePath) {
|
|
31
|
+
const stages = [
|
|
32
|
+
{ name: 'build', command: config.build },
|
|
33
|
+
{ name: 'lint', command: config.lint },
|
|
34
|
+
{ name: 'test', command: config.test },
|
|
35
|
+
];
|
|
36
|
+
const results = [];
|
|
37
|
+
for (const stage of stages) {
|
|
38
|
+
if (!stage.command.trim())
|
|
39
|
+
continue; // empty command = no default for this toolchain, stage skipped
|
|
40
|
+
const result = await runStage(stage.name, stage.command, worktreePath);
|
|
41
|
+
results.push(result);
|
|
42
|
+
if (!result.ok)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=runChecks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runChecks.js","sourceRoot":"","sources":["../../src/workflow/runChecks.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,KAAK,UAAU,QAAQ,CAAC,IAAyB,EAAE,OAAe,EAAE,GAAW;IAC7E,2EAA2E;IAC3E,kEAAkE;IAClE,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC;QAChG,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;IAC5E,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,KAA+D,CAAC;QAC5E,OAAO;YACL,IAAI;YACJ,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE;YACxB,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC;YAClD,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;SAC/B,CAAC;IACJ,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAA4B,EAAE,YAAoB;IAChF,MAAM,MAAM,GAA0D;QACpE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,EAAE;QACxC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE;QACtC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE;KACvC,CAAC;IAEF,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE;YAAE,SAAS,CAAC,+DAA+D;QACpG,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QACvE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,MAAM;IACxB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step 8: poll the MR/PR's pipeline (at config.pollIntervalSeconds) until it
|
|
3
|
+
* succeeds; on failure, hand the failing jobs' logs to the configured agent
|
|
4
|
+
* (with pipeline-worker's own forge MCP server available so it can pull further
|
|
5
|
+
* detail itself), commit the fix, push, and retry — capped at
|
|
6
|
+
* config.maxFixAttempts before escalating via an MR comment. Never retries
|
|
7
|
+
* indefinitely, and never spends agent tokens on pipelines that are not
|
|
8
|
+
* actually failed (canceled/skipped go straight to a human).
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentAdapter } from '../agent/types.js';
|
|
11
|
+
import type { ForgeClient } from '../forge/types.js';
|
|
12
|
+
import type { PipelineWorkerConfig, RunState } from '../types.js';
|
|
13
|
+
export declare function watchPipeline(forge: ForgeClient, config: PipelineWorkerConfig, agent: AgentAdapter, worktreePath: string, branch: string, mrIid: number, state: RunState, repoRoot: string): Promise<void>;
|