pipeline-worker 0.1.0 → 0.1.2
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 +42 -36
- package/dist/agent/claude.d.ts +17 -0
- package/dist/agent/claude.js +79 -5
- package/dist/agent/claude.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/config/detectChecks.js +51 -2
- package/dist/config/detectChecks.js.map +1 -1
- package/dist/config/loader.js +26 -3
- package/dist/config/loader.js.map +1 -1
- package/dist/forge/github.js +12 -1
- package/dist/forge/github.js.map +1 -1
- package/dist/forge/gitlab.d.ts +1 -1
- package/dist/forge/gitlab.js +18 -5
- package/dist/forge/gitlab.js.map +1 -1
- package/dist/forge/types.d.ts +9 -0
- package/dist/git/commit.d.ts +20 -0
- package/dist/git/commit.js +37 -0
- package/dist/git/commit.js.map +1 -1
- package/dist/git/diff.d.ts +6 -0
- package/dist/git/diff.js +7 -1
- package/dist/git/diff.js.map +1 -1
- package/dist/git/resolveProjectPath.d.ts +22 -0
- package/dist/git/resolveProjectPath.js +37 -0
- package/dist/git/resolveProjectPath.js.map +1 -0
- package/dist/git/worktree.d.ts +24 -1
- package/dist/git/worktree.js +63 -3
- package/dist/git/worktree.js.map +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/ui/steps.d.ts +34 -0
- package/dist/ui/steps.js +83 -0
- package/dist/ui/steps.js.map +1 -0
- package/dist/ui/welcome.d.ts +5 -0
- package/dist/ui/welcome.js +34 -0
- package/dist/ui/welcome.js.map +1 -0
- package/dist/workflow/captureIntent.js +71 -8
- package/dist/workflow/captureIntent.js.map +1 -1
- package/dist/workflow/openMergeRequest.d.ts +2 -0
- package/dist/workflow/openMergeRequest.js +24 -13
- package/dist/workflow/openMergeRequest.js.map +1 -1
- package/dist/workflow/orchestrate.js +48 -13
- package/dist/workflow/orchestrate.js.map +1 -1
- package/dist/workflow/runChecks.d.ts +6 -0
- package/dist/workflow/runChecks.js +33 -5
- package/dist/workflow/runChecks.js.map +1 -1
- package/dist/workflow/watchPipeline.d.ts +8 -1
- package/dist/workflow/watchPipeline.js +112 -22
- package/dist/workflow/watchPipeline.js.map +1 -1
- package/package.json +2 -4
- package/.env.example +0 -27
- package/.pipeline-worker.yml.example +0 -24
|
@@ -1,29 +1,92 @@
|
|
|
1
1
|
/** Step 3: ask the configured agent what a diff is *for*, in a structured, reusable shape. */
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
const COMMIT_MESSAGE_MAX_LENGTH = 72;
|
|
4
|
+
/**
|
|
5
|
+
* Naming a branch/commit/summary from a diff needs no deep reasoning, so a
|
|
6
|
+
* lighter model keeps this step's token cost down. Adapters that don't
|
|
7
|
+
* support per-invocation model selection (e.g. copilot) just ignore this.
|
|
8
|
+
* The CI-fix path (watchPipeline.ts) deliberately does NOT set this — fixing
|
|
9
|
+
* a real failing build needs the stronger default model.
|
|
10
|
+
*/
|
|
11
|
+
const INTENT_MODEL = 'haiku';
|
|
12
|
+
const RISK_CRITERIA = 'low: the change is isolated to independent components with a small blast radius. ' +
|
|
13
|
+
'medium: the change touches a shared/dependent component, but that component is well covered by existing unit tests. ' +
|
|
14
|
+
"high: the change touches existing/critical code paths and needs a human reviewer's attention before merging.";
|
|
3
15
|
const INTENT_SCHEMA = {
|
|
4
16
|
type: 'object',
|
|
5
17
|
properties: {
|
|
6
|
-
|
|
18
|
+
intent: { type: 'string', description: 'One short sentence, no line breaks: why this change exists / what problem it solves.' },
|
|
19
|
+
summary: { type: 'string', description: 'One or two sentences (single line, no line breaks) on what this change does and why' },
|
|
7
20
|
branchName: { type: 'string', description: 'A short kebab-case branch name, prefixed with pipeline-worker/' },
|
|
8
|
-
commitMessage: {
|
|
21
|
+
commitMessage: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
maxLength: COMMIT_MESSAGE_MAX_LENGTH,
|
|
24
|
+
description: `A single-line conventional-commit subject (e.g. "fix: handle empty diff"), max ${COMMIT_MESSAGE_MAX_LENGTH} characters. ` +
|
|
25
|
+
'Used verbatim as both the git commit message and the MR/PR title — no body, bullet list, or line breaks.',
|
|
26
|
+
},
|
|
27
|
+
fileChanges: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
file: { type: 'string', description: 'The file path (single line).' },
|
|
33
|
+
summary: { type: 'string', description: 'A single-line summary of what changed in that file.' },
|
|
34
|
+
},
|
|
35
|
+
required: ['file', 'summary'],
|
|
36
|
+
},
|
|
37
|
+
description: 'One entry per file touched in the diff, each a single-line summary of what changed in that file.',
|
|
38
|
+
},
|
|
39
|
+
risk: { type: 'string', enum: ['low', 'medium', 'high'], description: RISK_CRITERIA },
|
|
40
|
+
riskReason: { type: 'string', description: 'One short sentence (no line breaks) justifying the risk level.' },
|
|
41
|
+
testScenarios: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: { type: 'string', description: 'A single-line test scenario a reviewer should verify before merging.' },
|
|
44
|
+
description: 'Concrete test scenarios (each a single line) a reviewer should verify before merging.',
|
|
45
|
+
},
|
|
9
46
|
},
|
|
10
|
-
required: ['summary', 'branchName', 'commitMessage'],
|
|
47
|
+
required: ['intent', 'summary', 'branchName', 'commitMessage', 'fileChanges', 'risk', 'riskReason', 'testScenarios'],
|
|
11
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* Every one of these fields is rendered by openMergeRequest.ts's
|
|
51
|
+
* buildDescription() as a single inline line or bullet, so — like
|
|
52
|
+
* commitMessage — none of them may contain a newline, or they'd break the
|
|
53
|
+
* MR/PR description's formatting.
|
|
54
|
+
*/
|
|
55
|
+
function singleLine(fieldName) {
|
|
56
|
+
return z
|
|
57
|
+
.string()
|
|
58
|
+
.min(1)
|
|
59
|
+
.refine((s) => !s.includes('\n'), `${fieldName} must be a single line`);
|
|
60
|
+
}
|
|
12
61
|
/**
|
|
13
62
|
* Agent output is untrusted input: validate the shape and constrain the
|
|
14
63
|
* branch name to characters that are safe as a git ref and a URL segment.
|
|
64
|
+
* commitMessage doubles as the MR/PR title (see openMergeRequest.ts), so it
|
|
65
|
+
* must stay a single line short enough to render as one.
|
|
15
66
|
*/
|
|
16
67
|
const IntentShape = z.object({
|
|
17
|
-
|
|
68
|
+
intent: singleLine('intent'),
|
|
69
|
+
summary: singleLine('summary'),
|
|
18
70
|
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
|
|
71
|
+
commitMessage: z
|
|
72
|
+
.string()
|
|
73
|
+
.min(1)
|
|
74
|
+
.max(COMMIT_MESSAGE_MAX_LENGTH)
|
|
75
|
+
.refine((s) => !s.includes('\n'), 'commitMessage must be a single line (it doubles as the MR/PR title)'),
|
|
76
|
+
fileChanges: z.array(z.object({ file: singleLine('fileChanges[].file'), summary: singleLine('fileChanges[].summary') })).min(1),
|
|
77
|
+
risk: z.enum(['low', 'medium', 'high']),
|
|
78
|
+
riskReason: singleLine('riskReason'),
|
|
79
|
+
testScenarios: z.array(singleLine('testScenarios[]')).min(1),
|
|
20
80
|
});
|
|
21
81
|
export async function captureIntent(agent, diffText, worktreePath) {
|
|
22
82
|
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
|
|
24
|
-
'a kebab-case branch name prefixed with "pipeline-worker/",
|
|
83
|
+
'Respond with a JSON object matching the given schema: why this change exists, a short summary of what changed, ' +
|
|
84
|
+
'a kebab-case branch name prefixed with "pipeline-worker/", a short single-line conventional-commit ' +
|
|
85
|
+
`subject (max ${COMMIT_MESSAGE_MAX_LENGTH} characters, no body or bullet list, used verbatim as the MR/PR title), ` +
|
|
86
|
+
'a one-line summary per changed file, a risk level with a one-line justification ' +
|
|
87
|
+
`(${RISK_CRITERIA}), and the concrete test scenarios a reviewer should check before merging.\n\n` +
|
|
25
88
|
`\`\`\`diff\n${diffText}\n\`\`\``;
|
|
26
|
-
const result = await agent.invoke({ prompt, cwd: worktreePath, jsonSchema: INTENT_SCHEMA });
|
|
89
|
+
const result = await agent.invoke({ prompt, cwd: worktreePath, jsonSchema: INTENT_SCHEMA, model: INTENT_MODEL });
|
|
27
90
|
try {
|
|
28
91
|
return IntentShape.parse(JSON.parse(result.text));
|
|
29
92
|
}
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"captureIntent.js","sourceRoot":"","sources":["../../src/workflow/captureIntent.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** Steps 6-7: push the branch and open (or reuse) an MR/PR describing the workflow that produced it. */
|
|
2
2
|
import type { ForgeClient } from '../forge/types.js';
|
|
3
3
|
import type { CapturedIntent, MergeRequest } from '../types.js';
|
|
4
|
+
/** Renders the MR/PR description as: Intent, Summary, per-file changes, Risk, Test Scenarios. */
|
|
5
|
+
export declare function buildDescription(intent: CapturedIntent, agentName: string): string;
|
|
4
6
|
export declare function openMergeRequest(forge: ForgeClient, worktreePath: string, branch: string, targetBranch: string, intent: CapturedIntent, agentName: string): Promise<MergeRequest>;
|
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
/** Steps 6-7: push the branch and open (or reuse) an MR/PR describing the workflow that produced it. */
|
|
2
2
|
import { push } from '../git/commit.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
3
|
+
import { runStep, note } from '../ui/steps.js';
|
|
4
|
+
/** Renders the MR/PR description as: Intent, Summary, per-file changes, Risk, Test Scenarios. */
|
|
5
|
+
export function buildDescription(intent, agentName) {
|
|
6
|
+
const fileChanges = intent.fileChanges.map((f) => `- \`${f.file}\`: ${f.summary}`).join('\n');
|
|
7
|
+
const testScenarios = intent.testScenarios.map((s) => `- ${s}`).join('\n');
|
|
8
|
+
const risk = intent.risk.charAt(0).toUpperCase() + intent.risk.slice(1);
|
|
9
|
+
return (`**Intent:** ${intent.intent}\n\n` +
|
|
10
|
+
`**Summary:** ${intent.summary}\n\n` +
|
|
11
|
+
`**File changes:**\n${fileChanges}\n\n` +
|
|
12
|
+
`**Risk:** ${risk} — ${intent.riskReason}\n\n` +
|
|
13
|
+
`**Test Scenarios:**\n${testScenarios}\n\n` +
|
|
9
14
|
'---\n' +
|
|
10
15
|
`Generated by \`pipeline-worker\`: intent captured via **${agentName}**, ` +
|
|
11
16
|
'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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
'If the pipeline fails, pipeline-worker will attempt automated fixes (up to the configured retry cap) before escalating here.');
|
|
18
|
+
}
|
|
19
|
+
export async function openMergeRequest(forge, worktreePath, branch, targetBranch, intent, agentName) {
|
|
20
|
+
await runStep(9, '⬆', 'Pushing your branch', `push ${branch} to origin`, () => push(worktreePath, 'origin', branch));
|
|
21
|
+
const existing = await forge.findExistingMr(branch);
|
|
22
|
+
if (existing) {
|
|
23
|
+
note(`reusing existing merge request ${existing.webUrl}`);
|
|
24
|
+
return existing;
|
|
25
|
+
}
|
|
26
|
+
const description = buildDescription(intent, agentName);
|
|
27
|
+
const mr = await runStep(10, '🔀', 'Opening merge request', `target branch: ${targetBranch}`, () => forge.createMergeRequest({ sourceBranch: branch, targetBranch, title: intent.commitMessage, description }));
|
|
28
|
+
note(mr.webUrl);
|
|
29
|
+
return mr;
|
|
19
30
|
}
|
|
20
31
|
//# sourceMappingURL=openMergeRequest.js.map
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"openMergeRequest.js","sourceRoot":"","sources":["../../src/workflow/openMergeRequest.ts"],"names":[],"mappings":"AAAA,wGAAwG;AAExG,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAI/C,iGAAiG;AACjG,MAAM,UAAU,gBAAgB,CAAC,MAAsB,EAAE,SAAiB;IACxE,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9F,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3E,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAExE,OAAO,CACL,eAAe,MAAM,CAAC,MAAM,MAAM;QAClC,gBAAgB,MAAM,CAAC,OAAO,MAAM;QACpC,sBAAsB,WAAW,MAAM;QACvC,aAAa,IAAI,MAAM,MAAM,CAAC,UAAU,MAAM;QAC9C,wBAAwB,aAAa,MAAM;QAC3C,OAAO;QACP,2DAA2D,SAAS,MAAM;QAC1E,4DAA4D;QAC5D,8HAA8H,CAC/H,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAkB,EAClB,YAAoB,EACpB,MAAc,EACd,YAAoB,EACpB,MAAsB,EACtB,SAAiB;IAEjB,MAAM,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,qBAAqB,EAAE,QAAQ,MAAM,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAErH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACpD,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,CAAC,kCAAkC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAC1D,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAExD,MAAM,EAAE,GAAG,MAAM,OAAO,CACtB,EAAE,EACF,IAAI,EACJ,uBAAuB,EACvB,kBAAkB,YAAY,EAAE,EAChC,GAAG,EAAE,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,CAAC,aAAa,EAAE,WAAW,EAAE,CAAC,CACjH,CAAC;IACF,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;IAChB,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -3,30 +3,55 @@ import { loadConfig } from '../config/loader.js';
|
|
|
3
3
|
import { createForge } from '../forge/index.js';
|
|
4
4
|
import { selectAgent } from '../agent/index.js';
|
|
5
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';
|
|
6
|
+
import { createWorktree, syncWithOrigin, applyDiffToWorktree, removeWorktree, renameBranch, generateTempBranchName } from '../git/worktree.js';
|
|
7
|
+
import { currentBranch, commit, stageAll, findUnresolvedConflictMarkers } from '../git/commit.js';
|
|
8
8
|
import { captureIntent } from './captureIntent.js';
|
|
9
9
|
import { runChecks } from './runChecks.js';
|
|
10
10
|
import { openMergeRequest } from './openMergeRequest.js';
|
|
11
11
|
import { watchPipeline } from './watchPipeline.js';
|
|
12
12
|
import { saveRunState } from '../state/runState.js';
|
|
13
|
+
import { step, runStep, note, noteRisk } from '../ui/steps.js';
|
|
14
|
+
import { printWelcome } from '../ui/welcome.js';
|
|
13
15
|
/** Function-boundary read so TS reports the declared RunPhase union, not a narrowed literal. */
|
|
14
16
|
function readPhase(state) {
|
|
15
17
|
return state.phase;
|
|
16
18
|
}
|
|
19
|
+
function buildApplyConflictPrompt(conflictedFiles) {
|
|
20
|
+
return (`Applying your diff produced merge conflicts (the target branch moved since the diff was captured) in: ${conflictedFiles.join(', ')}. ` +
|
|
21
|
+
'Resolve the conflict markers (<<<<<<<, =======, >>>>>>>) in each file by choosing the correct combined content ' +
|
|
22
|
+
'that preserves the intent of both sides, then remove the markers.');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A best-effort, single attempt: there's no MR yet at this point in the
|
|
26
|
+
* workflow, so unlike watchPipeline.ts's CI-fix/merge-conflict loops there's
|
|
27
|
+
* nothing to leave an escalation comment on if the agent can't resolve it —
|
|
28
|
+
* fail the run clearly instead so the user can intervene manually.
|
|
29
|
+
*/
|
|
30
|
+
async function resolveApplyConflicts(agent, worktreePath, conflictedFiles) {
|
|
31
|
+
const agentResponse = await runStep(4, '🔧', 'Resolving conflicts', `asking the agent to resolve ${conflictedFiles.length} conflicted file(s)`, async () => (await agent.invoke({ prompt: buildApplyConflictPrompt(conflictedFiles), cwd: worktreePath, permissionMode: 'acceptEdits' })).text);
|
|
32
|
+
note(`agent: ${agentResponse.slice(0, 300).trim()}${agentResponse.length > 300 ? '…' : ''}`);
|
|
33
|
+
const stillConflicted = findUnresolvedConflictMarkers(worktreePath, conflictedFiles);
|
|
34
|
+
if (stillConflicted.length > 0) {
|
|
35
|
+
throw new Error(`pipeline-worker: could not automatically resolve conflicts applying your diff — ${stillConflicted.join(', ')} ` +
|
|
36
|
+
'still have conflict markers. Resolve them manually and retry.');
|
|
37
|
+
}
|
|
38
|
+
await stageAll(worktreePath);
|
|
39
|
+
}
|
|
17
40
|
export async function runWorkflow(repoRoot) {
|
|
18
41
|
const config = loadConfig(repoRoot);
|
|
19
42
|
const forge = createForge(config);
|
|
20
43
|
const agent = selectAgent(config);
|
|
21
|
-
|
|
44
|
+
await printWelcome(config, repoRoot);
|
|
45
|
+
const { diffText, untrackedFiles } = await runStep(1, '📸', 'Capturing your changes', 'reading uncommitted edits and untracked files from your repo', () => captureDiff(repoRoot));
|
|
22
46
|
if (diffText.trim().length === 0 && untrackedFiles.length === 0) {
|
|
23
47
|
console.log('pipeline-worker: no changes to process.');
|
|
24
48
|
return;
|
|
25
49
|
}
|
|
50
|
+
note(`${untrackedFiles.length} new file(s), ${diffText.split('\n').length} line(s) of diff`);
|
|
26
51
|
const targetBranch = await currentBranch(repoRoot);
|
|
27
52
|
const tempBranch = generateTempBranchName();
|
|
28
|
-
const worktreePath = await createWorktree(repoRoot, tempBranch);
|
|
29
|
-
let state = { branch: tempBranch, worktreePath, attempt: 0, phase: 'diff' };
|
|
53
|
+
const worktreePath = await runStep(2, '🌳', 'Creating worktree', `create worktree with name ${tempBranch}`, () => createWorktree(repoRoot, tempBranch));
|
|
54
|
+
let state = { branch: tempBranch, targetBranch, worktreePath, attempt: 0, phase: 'diff' };
|
|
30
55
|
saveRunState(repoRoot, state);
|
|
31
56
|
let cleanedUp = false;
|
|
32
57
|
const cleanup = async () => {
|
|
@@ -41,12 +66,21 @@ export async function runWorkflow(repoRoot) {
|
|
|
41
66
|
process.once('SIGINT', () => onSignal(130));
|
|
42
67
|
process.once('SIGTERM', () => onSignal(143));
|
|
43
68
|
try {
|
|
44
|
-
await
|
|
45
|
-
const
|
|
46
|
-
|
|
69
|
+
await runStep(3, '🔄', 'Syncing worktree with origin', `pull --rebase origin ${targetBranch}, so the diff lands on the latest base`, () => syncWithOrigin(worktreePath, targetBranch));
|
|
70
|
+
const applyResult = await runStep(4, '📦', 'Applying your changes', 'replay your diff and untracked files into the new worktree', () => applyDiffToWorktree(worktreePath, diffText, untrackedFiles, repoRoot));
|
|
71
|
+
if (applyResult.conflicted) {
|
|
72
|
+
note(`conflict in: ${applyResult.conflictedFiles.join(', ')}`);
|
|
73
|
+
await resolveApplyConflicts(agent, worktreePath, applyResult.conflictedFiles);
|
|
74
|
+
}
|
|
75
|
+
const intent = await runStep(5, '🧠', 'Understanding your changes', `ask ${config.agent} to infer a branch name, commit message, and summary`, () => captureIntent(agent, diffText, worktreePath));
|
|
76
|
+
note(`${config.agent} says: ${intent.summary}`);
|
|
77
|
+
noteRisk(intent.risk, intent.riskReason);
|
|
78
|
+
await runStep(6, '🌿', 'Checkout feature branch', `switch to feature branch ${intent.branchName}`, () => renameBranch(worktreePath, intent.branchName));
|
|
47
79
|
state = { ...state, branch: intent.branchName, phase: 'intent' };
|
|
48
80
|
saveRunState(repoRoot, state);
|
|
49
|
-
const checks = await runChecks(config, worktreePath);
|
|
81
|
+
const checks = await runStep(7, '✅', 'Running checks', 'build, lint, and test — whichever your repo has configured', () => runChecks(config, worktreePath));
|
|
82
|
+
for (const check of checks)
|
|
83
|
+
note(`${check.name}: ${check.ok ? 'passed' : 'failed'} (${(check.durationMs / 1000).toFixed(1)}s)`);
|
|
50
84
|
const failedCheck = checks.find((c) => !c.ok);
|
|
51
85
|
if (failedCheck) {
|
|
52
86
|
console.error(`pipeline-worker: ${failedCheck.name} failed, aborting before opening a merge request.\n${failedCheck.stderr}`);
|
|
@@ -55,23 +89,24 @@ export async function runWorkflow(repoRoot) {
|
|
|
55
89
|
}
|
|
56
90
|
state.phase = 'checks';
|
|
57
91
|
saveRunState(repoRoot, state);
|
|
92
|
+
await runStep(8, '💾', 'Committing changes', `commit message: "${intent.commitMessage}"`,
|
|
58
93
|
// applyDiffToWorktree left everything staged; without this commit the
|
|
59
94
|
// push would carry no changes and the MR would be empty.
|
|
60
|
-
|
|
95
|
+
() => commit(worktreePath, intent.commitMessage));
|
|
61
96
|
const mr = await openMergeRequest(forge, worktreePath, state.branch, targetBranch, intent, config.agent);
|
|
62
97
|
state.mrIid = mr.iid;
|
|
63
98
|
state.phase = 'mr';
|
|
64
99
|
saveRunState(repoRoot, state);
|
|
65
|
-
await watchPipeline(forge, config, agent, worktreePath, state.branch, mr.iid, state, repoRoot);
|
|
100
|
+
await watchPipeline(forge, config, agent, worktreePath, state.branch, targetBranch, mr.iid, state, repoRoot);
|
|
66
101
|
// watchPipeline mutates state.phase in place; go through a function
|
|
67
102
|
// boundary so TS uses the declared RunPhase return type instead of the
|
|
68
103
|
// 'mr' literal it narrowed state.phase to just before the call.
|
|
69
104
|
const finalPhase = readPhase(state);
|
|
70
105
|
if (finalPhase === 'done') {
|
|
71
|
-
|
|
106
|
+
step('🎉', 'Done', `MR ${mr.webUrl} passed CI`);
|
|
72
107
|
}
|
|
73
108
|
else if (finalPhase === 'escalated') {
|
|
74
|
-
|
|
109
|
+
step('🚨', 'Stopped for human review', `see ${mr.webUrl} for what was tried and why`);
|
|
75
110
|
process.exitCode = 1;
|
|
76
111
|
}
|
|
77
112
|
}
|
|
@@ -1 +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/
|
|
1
|
+
{"version":3,"file":"orchestrate.js","sourceRoot":"","sources":["../../src/workflow/orchestrate.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,5 +4,11 @@
|
|
|
4
4
|
* only to fix a failure the scripts already reported (see watchPipeline.ts).
|
|
5
5
|
*/
|
|
6
6
|
import type { CheckResult, PipelineWorkerConfig } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Splits a command into argv, keeping "..."/'...'-quoted substrings (e.g. a
|
|
9
|
+
* dotnet test filter) together as one argument instead of splitting on the
|
|
10
|
+
* spaces inside them.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseCommand(command: string): string[];
|
|
7
13
|
/** Runs build, then lint, then test, stopping at the first failure (fail-fast). */
|
|
8
14
|
export declare function runChecks(config: PipelineWorkerConfig, worktreePath: string): Promise<CheckResult[]>;
|
|
@@ -3,16 +3,44 @@
|
|
|
3
3
|
* from process exit codes only — the agent is never asked to judge these,
|
|
4
4
|
* only to fix a failure the scripts already reported (see watchPipeline.ts).
|
|
5
5
|
*/
|
|
6
|
-
import { execFile } from 'node:child_process';
|
|
6
|
+
import { execFile, exec } from 'node:child_process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
/**
|
|
11
|
+
* Shell operators (&&, ||, ;, |, backticks, redirects) require a shell
|
|
12
|
+
* interpreter. Commands that contain any of these are routed to exec();
|
|
13
|
+
* plain commands (no shell metacharacters) use execFile() for tighter control.
|
|
14
|
+
*/
|
|
15
|
+
const NEEDS_SHELL = /&&|\|\|?|[;`<>()]|\$\(/;
|
|
16
|
+
/**
|
|
17
|
+
* Splits a command into argv, keeping "..."/'...'-quoted substrings (e.g. a
|
|
18
|
+
* dotnet test filter) together as one argument instead of splitting on the
|
|
19
|
+
* spaces inside them.
|
|
20
|
+
*/
|
|
21
|
+
export function parseCommand(command) {
|
|
22
|
+
const tokens = [];
|
|
23
|
+
const tokenPattern = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = tokenPattern.exec(command)) !== null) {
|
|
26
|
+
tokens.push(match[1] ?? match[2] ?? match[3]);
|
|
27
|
+
}
|
|
28
|
+
return tokens;
|
|
29
|
+
}
|
|
9
30
|
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
31
|
const start = Date.now();
|
|
32
|
+
const opts = { cwd, maxBuffer: 64 * 1024 * 1024 };
|
|
14
33
|
try {
|
|
15
|
-
|
|
34
|
+
let stdout;
|
|
35
|
+
let stderr;
|
|
36
|
+
if (NEEDS_SHELL.test(command)) {
|
|
37
|
+
// Shell-compound commands (e.g. 'dotnet tool restore && dotnet csharpier check .')
|
|
38
|
+
({ stdout, stderr } = await execAsync(command, opts));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const [cmd, ...args] = parseCommand(command);
|
|
42
|
+
({ stdout, stderr } = await execFileAsync(cmd, args, opts));
|
|
43
|
+
}
|
|
16
44
|
return { name, ok: true, stdout, stderr, durationMs: Date.now() - start };
|
|
17
45
|
}
|
|
18
46
|
catch (error) {
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"runChecks.js","sourceRoot":"","sources":["../../src/workflow/runChecks.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
* config.maxFixAttempts before escalating via an MR comment. Never retries
|
|
7
7
|
* indefinitely, and never spends agent tokens on pipelines that are not
|
|
8
8
|
* actually failed (canceled/skipped go straight to a human).
|
|
9
|
+
*
|
|
10
|
+
* Also watches for the forge confirming a real merge conflict against the
|
|
11
|
+
* target branch (GitHub's "dirty" / GitLab's "cannot_be_merged") — some
|
|
12
|
+
* repos never even run CI on an unmergeable PR, so this is checked on every
|
|
13
|
+
* poll interval rather than only after a pipeline goes terminal. When found,
|
|
14
|
+
* merges the target branch in and asks the agent to resolve any actual
|
|
15
|
+
* conflict markers, sharing the same maxFixAttempts budget as CI fixes.
|
|
9
16
|
*/
|
|
10
17
|
import type { AgentAdapter } from '../agent/types.js';
|
|
11
18
|
import type { ForgeClient } from '../forge/types.js';
|
|
12
19
|
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>;
|
|
20
|
+
export declare function watchPipeline(forge: ForgeClient, config: PipelineWorkerConfig, agent: AgentAdapter, worktreePath: string, branch: string, targetBranch: string, mrIid: number, state: RunState, repoRoot: string): Promise<void>;
|
|
@@ -6,35 +6,61 @@
|
|
|
6
6
|
* config.maxFixAttempts before escalating via an MR comment. Never retries
|
|
7
7
|
* indefinitely, and never spends agent tokens on pipelines that are not
|
|
8
8
|
* actually failed (canceled/skipped go straight to a human).
|
|
9
|
+
*
|
|
10
|
+
* Also watches for the forge confirming a real merge conflict against the
|
|
11
|
+
* target branch (GitHub's "dirty" / GitLab's "cannot_be_merged") — some
|
|
12
|
+
* repos never even run CI on an unmergeable PR, so this is checked on every
|
|
13
|
+
* poll interval rather than only after a pipeline goes terminal. When found,
|
|
14
|
+
* merges the target branch in and asks the agent to resolve any actual
|
|
15
|
+
* conflict markers, sharing the same maxFixAttempts budget as CI fixes.
|
|
9
16
|
*/
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
10
18
|
import { writeFileSync, unlinkSync } from 'node:fs';
|
|
11
19
|
import { join } from 'node:path';
|
|
12
20
|
import { randomUUID } from 'node:crypto';
|
|
13
21
|
import { tmpdir } from 'node:os';
|
|
14
|
-
import {
|
|
22
|
+
import { promisify } from 'node:util';
|
|
23
|
+
import { stageAll, commit, push, hasChanges, listConflictedFiles, findUnresolvedConflictMarkers } from '../git/commit.js';
|
|
15
24
|
import { saveRunState } from '../state/runState.js';
|
|
25
|
+
import { step, runStep, note } from '../ui/steps.js';
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
16
27
|
const MAX_POLL_WINDOW_MS = 2 * 60 * 60 * 1000; // per pipeline attempt, as a safety net
|
|
17
28
|
const TERMINAL_STATUSES = ['success', 'failed', 'canceled', 'skipped'];
|
|
18
29
|
function sleep(ms) {
|
|
19
30
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
31
|
}
|
|
21
32
|
/**
|
|
22
|
-
* Waits for a *new* terminal pipeline
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
33
|
+
* Waits for either a *new* terminal pipeline or a confirmed merge conflict,
|
|
34
|
+
* whichever comes first. `previousPipelineId` is the one we already
|
|
35
|
+
* handled, and it stays "latest" on the forge until the pipeline for our fix
|
|
36
|
+
* push is created — without this, the loop would re-fix stale logs and burn
|
|
37
|
+
* attempts on a single real failure.
|
|
38
|
+
*
|
|
39
|
+
* The conflict check runs on every interval (not just once per outer loop
|
|
40
|
+
* iteration): some repos never run CI on an unmergeable PR, so waiting for a
|
|
41
|
+
* terminal pipeline that will never arrive would otherwise spin until the
|
|
42
|
+
* 2-hour safety window times out instead of surfacing the conflict promptly.
|
|
26
43
|
*/
|
|
27
|
-
async function
|
|
44
|
+
async function pollForNextAction(forge, mrIid, intervalMs, previousPipelineId) {
|
|
28
45
|
const maxPolls = Math.max(1, Math.ceil(MAX_POLL_WINDOW_MS / intervalMs));
|
|
29
46
|
for (let i = 0; i < maxPolls; i++) {
|
|
47
|
+
if (await forge.hasMergeConflicts(mrIid)) {
|
|
48
|
+
return { kind: 'conflict' };
|
|
49
|
+
}
|
|
30
50
|
const pipelines = await forge.getMrPipelines(mrIid);
|
|
31
51
|
const latest = pipelines[0];
|
|
32
52
|
if (latest && latest.id !== previousPipelineId && TERMINAL_STATUSES.includes(latest.status)) {
|
|
33
|
-
return latest;
|
|
53
|
+
return { kind: 'pipeline', pipeline: latest };
|
|
34
54
|
}
|
|
35
55
|
await sleep(intervalMs);
|
|
36
56
|
}
|
|
37
|
-
throw new Error(`
|
|
57
|
+
throw new Error(`MR ${mrIid} did not reach a terminal pipeline state or a resolvable conflict within the polling window`);
|
|
58
|
+
}
|
|
59
|
+
function buildConflictPrompt(conflictedFiles) {
|
|
60
|
+
return (`This branch has merge conflicts with the target branch in the following file(s): ${conflictedFiles.join(', ')}. ` +
|
|
61
|
+
'Resolve the conflict markers (<<<<<<<, =======, >>>>>>>) in each file by choosing the correct combined content ' +
|
|
62
|
+
'that preserves the intent of both sides, then remove the markers. Do not run git commands — pipeline-worker ' +
|
|
63
|
+
'stages and commits the resolution itself.');
|
|
38
64
|
}
|
|
39
65
|
function writeAgentMcpConfig() {
|
|
40
66
|
const path = join(tmpdir(), `pipeline-worker-mcp-${randomUUID()}.json`);
|
|
@@ -51,17 +77,77 @@ function buildFixPrompt(pipeline, jobs) {
|
|
|
51
77
|
jobSummaries);
|
|
52
78
|
}
|
|
53
79
|
async function escalate(forge, mrIid, message, state, repoRoot) {
|
|
54
|
-
await forge.createMrNote(mrIid, message);
|
|
80
|
+
await runStep(11, '🚨', 'Escalating to a human', message, () => forge.createMrNote(mrIid, message));
|
|
55
81
|
state.phase = 'escalated';
|
|
56
82
|
saveRunState(repoRoot, state);
|
|
57
83
|
}
|
|
58
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Merges origin/targetBranch into the worktree's branch to clear a confirmed
|
|
86
|
+
* merge conflict, asking the agent to resolve any real conflict markers.
|
|
87
|
+
* Shares state.attempt/config.maxFixAttempts with the CI-fix loop — both are
|
|
88
|
+
* "automated intervention attempts before giving up and escalating to a
|
|
89
|
+
* human" from the same budget. Returns false when it escalated instead of
|
|
90
|
+
* resolving (caller should stop the run in that case).
|
|
91
|
+
*/
|
|
92
|
+
async function tryResolveConflicts(forge, agent, config, worktreePath, branch, targetBranch, mrIid, state, repoRoot) {
|
|
93
|
+
state.attempt += 1;
|
|
94
|
+
saveRunState(repoRoot, state);
|
|
95
|
+
step('⚠️', 'Merge conflicts detected', `attempt ${state.attempt}/${config.maxFixAttempts} — merging origin/${targetBranch} into ${branch}`);
|
|
96
|
+
if (state.attempt > config.maxFixAttempts) {
|
|
97
|
+
await escalate(forge, mrIid, `pipeline-worker: automated fix attempts exhausted (${state.attempt - 1} attempts). This MR/PR still has merge conflicts and needs a human to resolve them.`, state, repoRoot);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const cleanMerge = await runStep(11, '🔀', 'Merging target branch', `git merge origin/${targetBranch} --no-edit`, async () => {
|
|
101
|
+
await execFileAsync('git', ['fetch', 'origin', targetBranch], { cwd: worktreePath });
|
|
102
|
+
try {
|
|
103
|
+
await execFileAsync('git', ['merge', `origin/${targetBranch}`, '--no-edit'], { cwd: worktreePath });
|
|
104
|
+
return true; // merge succeeded and auto-committed; nothing left to resolve
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false; // conflicts left in the working tree, merge in progress
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (!cleanMerge) {
|
|
111
|
+
const conflictedFiles = await listConflictedFiles(worktreePath);
|
|
112
|
+
if (conflictedFiles.length === 0) {
|
|
113
|
+
throw new Error(`git merge origin/${targetBranch} failed for a reason other than conflicts — check the worktree for details`);
|
|
114
|
+
}
|
|
115
|
+
note(conflictedFiles.join(', '));
|
|
116
|
+
const mcpConfigPath = writeAgentMcpConfig();
|
|
117
|
+
let agentResponse;
|
|
118
|
+
try {
|
|
119
|
+
agentResponse = await runStep(11, '🔧', 'Resolving conflicts', `asking the agent to resolve ${conflictedFiles.length} conflicted file(s)`, async () => (await agent.invoke({ prompt: buildConflictPrompt(conflictedFiles), cwd: worktreePath, mcpConfigPath, permissionMode: 'acceptEdits' })).text);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
unlinkSync(mcpConfigPath);
|
|
123
|
+
}
|
|
124
|
+
note(`agent: ${agentResponse.slice(0, 300).trim()}${agentResponse.length > 300 ? '…' : ''}`);
|
|
125
|
+
const stillConflicted = findUnresolvedConflictMarkers(worktreePath, conflictedFiles);
|
|
126
|
+
if (stillConflicted.length > 0) {
|
|
127
|
+
await escalate(forge, mrIid, `pipeline-worker: agent left ${stillConflicted.length} file(s) still conflicted (${stillConflicted.join(', ')}) — escalating to a human.`, state, repoRoot);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
await execFileAsync('git', ['add', '-A'], { cwd: worktreePath });
|
|
131
|
+
await commit(worktreePath, `merge: resolve conflicts with origin/${targetBranch}`);
|
|
132
|
+
}
|
|
133
|
+
await runStep(11, '⬆', 'Pushing the merge', `push ${branch} to origin`, () => push(worktreePath, 'origin', branch));
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
export async function watchPipeline(forge, config, agent, worktreePath, branch, targetBranch, mrIid, state, repoRoot) {
|
|
59
137
|
const intervalMs = config.pollIntervalSeconds * 1000;
|
|
60
138
|
state.phase = 'watch';
|
|
61
139
|
saveRunState(repoRoot, state);
|
|
62
140
|
let previousPipelineId;
|
|
63
141
|
for (;;) {
|
|
64
|
-
const
|
|
142
|
+
const outcome = await runStep(11, '👀', 'Watching pipeline', `poll CI every ${config.pollIntervalSeconds}s until it finishes`, () => pollForNextAction(forge, mrIid, intervalMs, previousPipelineId));
|
|
143
|
+
if (outcome.kind === 'conflict') {
|
|
144
|
+
const resolved = await tryResolveConflicts(forge, agent, config, worktreePath, branch, targetBranch, mrIid, state, repoRoot);
|
|
145
|
+
if (!resolved)
|
|
146
|
+
return; // already escalated inside tryResolveConflicts
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const pipeline = outcome.pipeline;
|
|
150
|
+
note(`pipeline ${pipeline.id}: ${pipeline.status} — ${pipeline.webUrl}`);
|
|
65
151
|
state.pipelineId = pipeline.id;
|
|
66
152
|
saveRunState(repoRoot, state);
|
|
67
153
|
if (pipeline.status === 'success') {
|
|
@@ -76,33 +162,37 @@ export async function watchPipeline(forge, config, agent, worktreePath, branch,
|
|
|
76
162
|
}
|
|
77
163
|
state.attempt += 1;
|
|
78
164
|
saveRunState(repoRoot, state);
|
|
165
|
+
step('💥', 'Pipeline failed', `attempt ${state.attempt}/${config.maxFixAttempts} — ${pipeline.webUrl}`);
|
|
79
166
|
if (state.attempt > config.maxFixAttempts) {
|
|
80
167
|
await escalate(forge, mrIid, `pipeline-worker: automated fix attempts exhausted (${state.attempt - 1} attempts). ` +
|
|
81
168
|
`Pipeline ${pipeline.webUrl} is still failing and needs a human to take over.`, state, repoRoot);
|
|
82
169
|
return;
|
|
83
170
|
}
|
|
84
|
-
const failedJobs = await
|
|
85
|
-
|
|
171
|
+
const { failedJobs, logs } = await runStep(11, '🔍', 'Diagnosing the failure', `reading logs for ${pipeline.webUrl}`, async () => {
|
|
172
|
+
const jobs = await forge.getFailedJobs(pipeline.id);
|
|
173
|
+
const jobLogs = await Promise.all(jobs.map(async (job) => ({ job, log: await forge.getJobLog(job.id) })));
|
|
174
|
+
return { failedJobs: jobs, logs: jobLogs };
|
|
175
|
+
});
|
|
176
|
+
note(failedJobs.length > 0 ? failedJobs.map((job) => job.name).join(', ') : 'no specific job names reported');
|
|
86
177
|
const mcpConfigPath = writeAgentMcpConfig();
|
|
178
|
+
let agentResponse;
|
|
87
179
|
try {
|
|
88
|
-
await agent.invoke({
|
|
89
|
-
prompt: buildFixPrompt(pipeline, logs),
|
|
90
|
-
cwd: worktreePath,
|
|
91
|
-
mcpConfigPath,
|
|
92
|
-
permissionMode: 'acceptEdits',
|
|
93
|
-
});
|
|
180
|
+
agentResponse = await runStep(11, '🔧', 'Fixing CI failure', `asking the agent to fix ${failedJobs.length} failed job(s)`, async () => (await agent.invoke({ prompt: buildFixPrompt(pipeline, logs), cwd: worktreePath, mcpConfigPath, permissionMode: 'acceptEdits' })).text);
|
|
94
181
|
}
|
|
95
182
|
finally {
|
|
96
183
|
unlinkSync(mcpConfigPath);
|
|
97
184
|
}
|
|
185
|
+
note(`agent: ${agentResponse.slice(0, 300).trim()}${agentResponse.length > 300 ? '…' : ''}`);
|
|
98
186
|
if (!(await hasChanges(worktreePath))) {
|
|
99
187
|
// Re-pushing an identical tree would never produce a new pipeline; stop here.
|
|
100
188
|
await escalate(forge, mrIid, `pipeline-worker: fix attempt ${state.attempt} produced no changes for pipeline ${pipeline.webUrl} — escalating to a human.`, state, repoRoot);
|
|
101
189
|
return;
|
|
102
190
|
}
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
|
|
191
|
+
await runStep(11, '⬆', 'Pushing the fix', `commit and push attempt ${state.attempt} to ${branch}`, async () => {
|
|
192
|
+
await stageAll(worktreePath);
|
|
193
|
+
await commit(worktreePath, `fix: address CI failure (attempt ${state.attempt})`);
|
|
194
|
+
await push(worktreePath, 'origin', branch);
|
|
195
|
+
});
|
|
106
196
|
previousPipelineId = pipeline.id;
|
|
107
197
|
}
|
|
108
198
|
}
|