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.
Files changed (52) hide show
  1. package/README.md +42 -36
  2. package/dist/agent/claude.d.ts +17 -0
  3. package/dist/agent/claude.js +79 -5
  4. package/dist/agent/claude.js.map +1 -1
  5. package/dist/agent/types.d.ts +2 -0
  6. package/dist/cli.js +1 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config/detectChecks.js +51 -2
  9. package/dist/config/detectChecks.js.map +1 -1
  10. package/dist/config/loader.js +26 -3
  11. package/dist/config/loader.js.map +1 -1
  12. package/dist/forge/github.js +12 -1
  13. package/dist/forge/github.js.map +1 -1
  14. package/dist/forge/gitlab.d.ts +1 -1
  15. package/dist/forge/gitlab.js +18 -5
  16. package/dist/forge/gitlab.js.map +1 -1
  17. package/dist/forge/types.d.ts +9 -0
  18. package/dist/git/commit.d.ts +20 -0
  19. package/dist/git/commit.js +37 -0
  20. package/dist/git/commit.js.map +1 -1
  21. package/dist/git/diff.d.ts +6 -0
  22. package/dist/git/diff.js +7 -1
  23. package/dist/git/diff.js.map +1 -1
  24. package/dist/git/resolveProjectPath.d.ts +22 -0
  25. package/dist/git/resolveProjectPath.js +37 -0
  26. package/dist/git/resolveProjectPath.js.map +1 -0
  27. package/dist/git/worktree.d.ts +24 -1
  28. package/dist/git/worktree.js +63 -3
  29. package/dist/git/worktree.js.map +1 -1
  30. package/dist/types.d.ts +16 -1
  31. package/dist/ui/steps.d.ts +34 -0
  32. package/dist/ui/steps.js +83 -0
  33. package/dist/ui/steps.js.map +1 -0
  34. package/dist/ui/welcome.d.ts +5 -0
  35. package/dist/ui/welcome.js +34 -0
  36. package/dist/ui/welcome.js.map +1 -0
  37. package/dist/workflow/captureIntent.js +71 -8
  38. package/dist/workflow/captureIntent.js.map +1 -1
  39. package/dist/workflow/openMergeRequest.d.ts +2 -0
  40. package/dist/workflow/openMergeRequest.js +24 -13
  41. package/dist/workflow/openMergeRequest.js.map +1 -1
  42. package/dist/workflow/orchestrate.js +48 -13
  43. package/dist/workflow/orchestrate.js.map +1 -1
  44. package/dist/workflow/runChecks.d.ts +6 -0
  45. package/dist/workflow/runChecks.js +33 -5
  46. package/dist/workflow/runChecks.js.map +1 -1
  47. package/dist/workflow/watchPipeline.d.ts +8 -1
  48. package/dist/workflow/watchPipeline.js +112 -22
  49. package/dist/workflow/watchPipeline.js.map +1 -1
  50. package/package.json +2 -4
  51. package/.env.example +0 -27
  52. 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
- summary: { type: 'string', description: 'One or two sentence description of what this change does and why' },
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: { type: 'string', description: 'A conventional-commit-style commit message for this change' },
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
- summary: z.string().min(1),
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.string().min(1),
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 and why, ' +
24
- 'a kebab-case branch name prefixed with "pipeline-worker/", and a conventional-commit-style commit message.\n\n' +
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,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"}
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
- 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` +
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
- return forge.createMergeRequest({
14
- sourceBranch: branch,
15
- targetBranch,
16
- title: intent.commitMessage,
17
- description,
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;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"}
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
- const { diffText, untrackedFiles } = await captureDiff(repoRoot);
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 applyDiffToWorktree(worktreePath, diffText, untrackedFiles, repoRoot);
45
- const intent = await captureIntent(agent, diffText, worktreePath);
46
- await renameBranch(worktreePath, intent.branchName);
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
- await commit(worktreePath, intent.commitMessage);
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
- console.log(`pipeline-worker: MR ${mr.webUrl} passed CI.`);
106
+ step('🎉', 'Done', `MR ${mr.webUrl} passed CI`);
72
107
  }
73
108
  else if (finalPhase === 'escalated') {
74
- console.log(`pipeline-worker: automated fixes exhausted escalated to a human on ${mr.webUrl}.`);
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":"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"}
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
- const { stdout, stderr } = await execFileAsync(cmd, args, { cwd, maxBuffer: 64 * 1024 * 1024 });
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;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"}
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 { stageAll, commit, push, hasChanges } from '../git/commit.js';
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: `previousPipelineId` is the one we
23
- * already handled, and it stays "latest" on the forge until the pipeline for
24
- * our fix push is created without this, the loop would re-fix stale logs
25
- * and burn attempts on a single real failure.
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 pollLatestPipeline(forge, mrIid, intervalMs, previousPipelineId) {
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(`Pipeline for MR ${mrIid} did not reach a terminal state within the polling window`);
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
- export async function watchPipeline(forge, config, agent, worktreePath, branch, mrIid, state, repoRoot) {
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 pipeline = await pollLatestPipeline(forge, mrIid, intervalMs, previousPipelineId);
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 forge.getFailedJobs(pipeline.id);
85
- const logs = await Promise.all(failedJobs.map(async (job) => ({ job, log: await forge.getJobLog(job.id) })));
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 stageAll(worktreePath);
104
- await commit(worktreePath, `fix: address CI failure (attempt ${state.attempt})`);
105
- await push(worktreePath, 'origin', branch);
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
  }